polystore 0.8.0 → 0.9.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/readme.md CHANGED
@@ -1,40 +1,46 @@
1
1
  # Polystore [![npm install polystore](https://img.shields.io/badge/npm%20install-polystore-blue.svg)](https://www.npmjs.com/package/polystore) [![test badge](https://github.com/franciscop/polystore/workflows/tests/badge.svg "test badge")](https://github.com/franciscop/polystore/blob/master/.github/workflows/tests.yml) [![gzip size](https://badgen.net/bundlephobia/minzip/polystore?label=gzip&color=green)](https://github.com/franciscop/polystore/blob/master/src/index.js)
2
2
 
3
- A small compatibility layer for many KV stores like localStorage, Redis, FileSystem, etc:
3
+ A key-value library to unify the API of [many clients](#clients), like localStorage, Redis, FileSystem, etc:
4
4
 
5
5
  ```js
6
6
  import kv from "polystore";
7
- const store = kv(new Map()); // in-memory
8
- const store1 = kv(localStorage); // Persist in the browser
9
- const store2 = kv(redisClient); // Use a Redis client for backend persistence
10
- // etc.
11
- ```
12
-
13
- This is the [API](#api) with all of the methods (they are all `async`):
14
-
15
- - `.get(key): any`: retrieve a single value, or `null` if it doesn't exist or is expired.
16
- - `.set(key, value, options?)`: save a single value, which can be anything that is serializable.
17
- - `.add(value, options?)`: same as with `.set()`, but auto-generate the key.
18
- - `.has(key): boolean`: check whether the key is in the store or not.
19
- - `.del(key)`: delete a single value from the store.
20
- - `.keys(prefix?): string[]`: get a list of all the available strings in the store.
21
- - `.values(prefix?): any[]`: get a list of all the values in the store.
22
- - `.entries(prefix?): [string, any][]`: get a list of all the key-value pairs in the store.
23
- - `.clear()`: delete ALL of the data in the store, effectively resetting it.
24
- - `.close()`: (only _some_ stores) ends the connection to the store.
25
- - `.prefix(prefix): store`: create a new sub-instance of the store where all the keys have this prefix.
26
-
27
- Available stores:
28
-
29
- - **Memory** `new Map()` (fe+be): an in-memory API to keep your KV store
30
- - **Local Storage** `localStorage` (fe): persist the data in the browser's localStorage
31
- - **Session Storage** `sessionStorage` (fe): persist the data in the browser's sessionStorage
32
- - **Cookies** `"cookie"` (fe): persist the data using cookies
33
- - **LocalForage** `localForage` (fe): persist the data on IndexedDB
34
- - **FS File** `new URL('file:///...')` (be): store the data in a single JSON file
35
- - **Redis Client** `redisClient` (be): use the Redis instance that you connect to
36
- - **Cloudflare KV** `env.KV_NAMESPACE` (be): use Cloudflare's KV store
37
- - (WIP) **Consul KV** `new Consul()` (fe+be): use Hashicorp's Consul KV store (https://www.npmjs.com/package/consul#kv)
7
+ const store1 = kv(new Map()); // in-memory
8
+ const store2 = kv(localStorage); // Persist in the browser
9
+ const store3 = kv(redisClient); // Use a Redis client for backend persistence
10
+ const store4 = kv(yourOwnStore); // Create a store based on your code
11
+ // Many more here
12
+ ```
13
+
14
+ These are all the methods of the [API](#api) (they are all `async`):
15
+
16
+ - [`.get(key)`](#get): read a single value, or `null` if it doesn't exist or is expired.
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.
25
+ - [`.clear()`](#clear): delete ALL of the data in the store, effectively resetting it.
26
+ - [`.close()`](#close): (only _some_ stores) ends the connection to the store.
27
+ - [`.prefix(prefix)`](#prefix): create a sub-store that only manages the keys with the given prefix.
28
+
29
+ Available clients for the KV store:
30
+
31
+ - [**Memory** `new Map()`](#memory) (fe+be): an in-memory API to keep your KV store.
32
+ - [**Local Storage** `localStorage`](#local-storage) (fe): persist the data in the browser's localStorage.
33
+ - [**Session Storage** `sessionStorage`](#session-storage) (fe): persist the data in the browser's sessionStorage.
34
+ - [**Cookies** `"cookie"`](#cookies) (fe): persist the data using cookies
35
+ - [**LocalForage** `localForage`](#local-forage) (fe): persist the data on IndexedDB
36
+ - [**File** `new URL('file:///...')`](#file) (be): store the data in a single JSON file in your FS
37
+ - [**Redis Client** `redisClient`](#redis-client) (be): use the Redis instance that you connect to
38
+ - [**Cloudflare KV** `env.KV_NAMESPACE`](#cloudflare-kv) (be): use Cloudflare's KV store
39
+ - [**Level** `new Level('example', { valueEncoding: 'json' })`](#level): support the whole Level ecosystem
40
+ - [**Etcd** `new Etcd3()`](#etcd): the Microsoft's high performance KV store.
41
+ - [**_Custom_** `{}`](#creating-a-store) (?): create your own store with just 3 methods!
42
+
43
+ > This library should be as performant as the client you use with the item methods (GET/SET/ADD/HAS/DEL). For other and advanced cases, see [the performance considerations](#performance) and read the docs on your client.
38
44
 
39
45
  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 isomorphic (Node.js and the Browser) and tiny (~2KB). For example, let's say you create an API library, then you can accept the stores from your client:
40
46
 
@@ -50,7 +56,7 @@ MyApi({ cache: env.KV_NAMESPACE }); // OR
50
56
 
51
57
  ## API
52
58
 
53
- See how to initialize each store [in the Stores list documentation](#stores). But basically for every store, it's like this:
59
+ See how to initialize each store [in the Clients list documentation](#clients). But basically for every store, it's like this:
54
60
 
55
61
  ```js
56
62
  import kv from "polystore";
@@ -164,7 +170,7 @@ if (await store.has('cookie-consent')) {
164
170
 
165
171
  ### .del()
166
172
 
167
- Remove a single key from the store:
173
+ Remove a single key from the store and return the key itself:
168
174
 
169
175
  ```js
170
176
  await store.del(key: string);
@@ -225,9 +231,11 @@ Remove all of the data from the store:
225
231
  await store.clear();
226
232
  ```
227
233
 
228
- ### .prefix() (unstable)
234
+ ### .prefix()
229
235
 
230
- Create a sub-store where all the operations use the given prefix. This is **the only method** of the store that is sync and you don't need to await:
236
+ > There's [an in-depth explanation about Substores](#substores) that is very informative for production usage.
237
+
238
+ Creates **a new instance** of the Store, _with the same client_ as you provided, but now any key you read, write, etc. will be passed with the given prefix to the client. You only write `.prefix()` once and then don't need to worry about any prefix for any method anymore, it's all automatic. It's **the only method** that you don't need to await:
231
239
 
232
240
  ```js
233
241
  const store = kv(new Map());
@@ -237,16 +245,17 @@ const session = store.prefix("session:");
237
245
  Then all of the operations will be converted internally to add the prefix when reading, writing, etc:
238
246
 
239
247
  ```js
240
- const val = await session.get("key1"); // .get('session:key1');
241
- await session.set("key2", "some data"); // .set('session:key2', ...);
242
- const val = await session.has("key3"); // .has('session:key3');
243
- await session.del("key4"); // .del('session:key4');
244
- await session.keys(); // .keys('session:');
248
+ const session = store.prefix("session:");
249
+ const val = await session.get("key1"); // store.get('session:key1');
250
+ await session.set("key2", "some data"); // store.set('session:key2', ...);
251
+ const val = await session.has("key3"); // store.has('session:key3');
252
+ await session.del("key4"); // store.del('session:key4');
253
+ await session.keys(); // store.keys(); + filter
245
254
  // ['key1', 'key2', ...] Note no prefix here
246
255
  await session.clear(); // delete only keys with the prefix
247
256
  ```
248
257
 
249
- This will probably never be stable given the nature of some engines, so as an alternative please consider using two stores instead of prefixes:
258
+ Different clients have better/worse support for substores, and in some cases some operations might be slower. This should be documented on each client's documentation (see below). As an alternative, you can always create two different stores instead of a substore:
250
259
 
251
260
  ```js
252
261
  // Two in-memory stores
@@ -260,9 +269,11 @@ const books = kv(new URL(`file://${import.meta.dirname}/books.json`));
260
269
 
261
270
  The main reason this is not stable is because [_some_ store engines don't allow for atomic deletion of keys given a prefix](https://stackoverflow.com/q/4006324/938236). While we do still clear them internally in those cases, that is a non-atomic operation and it could have some trouble if some other thread is reading/writing the data _at the same time_.
262
271
 
263
- ## Stores
272
+ ## Clients
264
273
 
265
- Accepts directly the store, or a promise that resolves into a store. All of the stores, including those that natively _don't_ support it, are enhanced with `Promises` and `expires` times, so they all work the same way.
274
+ A client is the library that manages the low-level store operations. For example, the Redis Client, or the browser's `localStorage` API. In some exceptions it's just a string and we do a bit more work on Polystore, like with `"cookie"` or `"file:///users/me/data.json"`.
275
+
276
+ Polystore provides a unified API you can use `Promises`, `expires` and `.prefix()` even with those stores that do not support these operations natively.
266
277
 
267
278
  ### Memory
268
279
 
@@ -271,18 +282,31 @@ An in-memory KV store, with promises and expiration time:
271
282
  ```js
272
283
  import kv from "polystore";
273
284
 
274
- // This already works, by default if there's nothing it'll use
275
- // a new Map()
276
- const store = kv();
277
- await store.set("key1", "Hello world");
285
+ const store = kv(new Map());
286
+
287
+ await store.set("key1", "Hello world", { expires: "1h" });
278
288
  console.log(await store.get("key1"));
289
+ // "Hello world"
290
+ ```
291
+
292
+ It can also be initialized empty, then it'll use the in-memory store:
293
+
294
+ ```js
295
+ import kv from "polystore";
279
296
 
280
- // Or you can be explicit:
297
+ const store = kv();
281
298
  const store = kv(new Map());
282
- await store.set("key1", "Hello world");
283
- console.log(await store.get("key1"));
284
299
  ```
285
300
 
301
+ <details>
302
+ <summary>Why use polystore with <code>new Map()</code>?</summary>
303
+ <p>Besides the other benefits already documented elsewhere, these are the ones specifically for wrapping Map() with polystore:</p>
304
+ <ul>
305
+ <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>
306
+ <li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
307
+ </ul>
308
+ </details>
309
+
286
310
  ### Local Storage
287
311
 
288
312
  The traditional localStorage that we all know and love, this time with a unified API, and promises:
@@ -291,8 +315,10 @@ The traditional localStorage that we all know and love, this time with a unified
291
315
  import kv from "polystore";
292
316
 
293
317
  const store = kv(localStorage);
294
- await store.set("key1", "Hello world");
318
+
319
+ await store.set("key1", "Hello world", { expires: "1h" });
295
320
  console.log(await store.get("key1"));
321
+ // "Hello world"
296
322
  ```
297
323
 
298
324
  Same limitations as always apply to localStorage, if you think you are going to use too much storage try instead our integration with [Local Forage](#local-forage)!
@@ -305,8 +331,10 @@ Same as localStorage, but now for the session only:
305
331
  import kv from "polystore";
306
332
 
307
333
  const store = kv(sessionStorage);
308
- await store.set("key1", "Hello world");
334
+
335
+ await store.set("key1", "Hello world", { expires: "1h" });
309
336
  console.log(await store.get("key1"));
337
+ // "Hello world"
310
338
  ```
311
339
 
312
340
  ### Cookies
@@ -316,9 +344,11 @@ Supports native browser cookies, including setting the expire time:
316
344
  ```js
317
345
  import kv from "polystore";
318
346
 
319
- const store = kv("cookie"); // yes, just a plain string
320
- await store.set("key1", "Hello world");
347
+ const store = kv("cookie"); // just a plain string
348
+
349
+ await store.set("key1", "Hello world", { expires: "1h" });
321
350
  console.log(await store.get("key1"));
351
+ // "Hello world"
322
352
  ```
323
353
 
324
354
  It is fairly limited for how powerful cookies are, but in exchange it has the same API as any other method or KV store. It works with browser-side Cookies (no http-only).
@@ -334,8 +364,10 @@ import kv from "polystore";
334
364
  import localForage from "localforage";
335
365
 
336
366
  const store = kv(localForage);
367
+
337
368
  await store.set("key1", "Hello world", { expires: "1h" });
338
369
  console.log(await store.get("key1"));
370
+ // "Hello world"
339
371
  ```
340
372
 
341
373
  ### Redis Client
@@ -346,25 +378,37 @@ Supports the official Node Redis Client. You can pass either the client or the p
346
378
  import kv from "polystore";
347
379
  import { createClient } from "redis";
348
380
 
349
- // Note: no need for await or similar
350
381
  const store = kv(createClient().connect());
351
- await store.set("key1", "Hello world");
382
+
383
+ await store.set("key1", "Hello world", { expires: "1h" });
352
384
  console.log(await store.get("key1"));
385
+ // "Hello world"
353
386
  ```
354
387
 
388
+ You don't need to `await` for the connect or similar, this will process it properly.
389
+
355
390
  > Note: the Redis client expire resolution is in the seconds, so times shorter than 1 second like `expires: 0.02` (20 ms) don't make sense for this storage method and won't properly save them.
356
391
 
357
- ### FS File
392
+ ### File
393
+
394
+ Treat a JSON file in your filesystem as the source for the KV store:
358
395
 
359
396
  ```js
360
397
  import kv from "polystore";
361
398
 
362
- // Create a url with the file protocol:
363
399
  const store = kv(new URL("file:///Users/me/project/cache.json"));
364
400
 
401
+ await store.set("key1", "Hello world", { expires: "1h" });
402
+ console.log(await store.get("key1"));
403
+ // "Hello world"
404
+ ```
405
+
406
+ You can also create multiple stores:
407
+
408
+ ```js
365
409
  // Paths need to be absolute, but you can use process.cwd() to make
366
410
  // it relative to the current process:
367
- const store = kv(new URL(`file://${process.cwd()}/cache.json`));
411
+ const store1 = kv(new URL(`file://${process.cwd()}/cache.json`));
368
412
  const store2 = kv(new URL(`file://${import.meta.dirname}/data.json`));
369
413
  ```
370
414
 
@@ -379,18 +423,16 @@ export default {
379
423
  async fetch(request, env, ctx) {
380
424
  const store = kv(env.YOUR_KV_NAMESPACE);
381
425
 
382
- await store.set("KEY", "VALUE");
383
- const value = await store.get("KEY");
426
+ await store.set("key1", "Hello world", { expires: "1h" });
427
+ console.log(await store.get("key1"));
428
+ // "Hello world"
384
429
 
385
- if (!value) {
386
- return new Response("Value not found", { status: 404 });
387
- }
388
- return new Response(value);
430
+ return new Response("My response");
389
431
  },
390
432
  };
391
433
  ```
392
434
 
393
- Why? The Cloudflare native KV store only accepts strings and has you manually calculating timeouts, but as usual with `polystore` you can set/get any serializable value and set the timeout in a familiar format:
435
+ Why use polystore? The Cloudflare native KV store only accepts strings and has you manually calculating timeouts, but as usual with `polystore` you can set/get any serializable value and set the timeout in a familiar format:
394
436
 
395
437
  ```js
396
438
  // GOOD - with polystore
@@ -404,9 +446,57 @@ await env.YOUR_KV_NAMESPACE.put("user", serialValue, {
404
446
  });
405
447
  ```
406
448
 
407
- ## Expiration explained
449
+ ### Level
408
450
 
409
- While different engines do expiration slightly differently internally, in creating polystore we want to ensure certain constrains, which _can_ affect performance. For example, if you do this operation:
451
+ Support [the Level ecosystem](https://github.com/Level/level), which is itself composed of modular methods:
452
+
453
+ ```js
454
+ import kv from "polystore";
455
+ import { Level } from "level";
456
+
457
+ const store = kv(new Level("example", { valueEncoding: "json" }));
458
+
459
+ await store.set("key1", "Hello world", { expires: "1h" });
460
+ console.log(await store.get("key1"));
461
+ // "Hello world"
462
+ ```
463
+
464
+ ### Etcd
465
+
466
+ Connect to Microsoft's Etcd Key-Value store:
467
+
468
+ ```js
469
+ import kv from "polystore";
470
+ import { Etcd3 } from "etcd3";
471
+
472
+ const store = kv(new Etcd3());
473
+
474
+ await store.set("key1", "Hello world", { expires: "1h" });
475
+ console.log(await store.get("key1"));
476
+ // "Hello world"
477
+ ```
478
+
479
+ ### Custom store
480
+
481
+ Please see the [creating a store](#creating-a-store) section for more details!
482
+
483
+ ## Performance
484
+
485
+ > TL;DR: if you only use the item operations (add,set,get,has,del) and your store supports expiration natively, you have nothing to worry about!
486
+
487
+ While all of our stores support `expires`, `.prefix()` and group operations, the nature of those makes them to have different performance characteristics.
488
+
489
+ **Expires** we polyfill expiration when the underlying library does not support it. The impact on read/write operations and on data size of each key should be minimal. However, it can have a big impact in storage size, since the expired keys are not evicted automatically. Note that when attempting to read an expired key, polystore **will delete that key**. However, if an expired key is never read, it would remain in the datastore and could create some old-data issues. This is **especially important where sensitive data is involved**! To fix this, the easiest way is calling `await store.entries();` on a cron job and that should evict all of the old keys (this operation is O(n) though, so not suitable for calling it on EVERY API call, see the next point).
490
+
491
+ **Group operations** these are there mostly for small datasets only, for one-off scripts or for dev purposes, since by their own nature they can _never_ be high performance. But this is normal if you think about traditional DBs, reading a single record by its ID is O(1), while reading all of the IDs in the DB into an array is going to be O(n). Same applies with polystore.
492
+
493
+ **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.
494
+
495
+ ## Expires
496
+
497
+ > 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_, which is relevant especially for sensitive information. You'd want to set up a cron job to evict it manually, since for large datasets it might be more expensive (O(n)).
498
+
499
+ We unify all of the clients diverse expiration methods into a single, easy one with `expires`:
410
500
 
411
501
  ```js
412
502
  // in-memory store
@@ -421,7 +511,7 @@ console.log(await store.get("a")); // 'b'
421
511
  // Make sure the key is expired
422
512
  await delay(2000); // 2s
423
513
 
424
- // Not only the .get() is null, but `.has()` returns false, and .keys() ignores it
514
+ // The group methods also ignore expired keys
425
515
  console.log(await store.keys()); // []
426
516
  console.log(await store.has("a")); // false
427
517
  console.log(await store.get("a")); // null
@@ -433,25 +523,144 @@ However, in some stores this does come with some potential performance disadvant
433
523
 
434
524
  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.
435
525
 
526
+ ## Substores
527
+
528
+ > There's some [basic `.prefix()` API info](#prefix) for everyday usage, this section is the in-depth explanation.
529
+
530
+ What `.prefix()` does is it creates **a new instance** of the Store, _with the same client_ as you provided, but now any key you read, write, etc. will be passed with the given prefix to the client. The issue is that support from the underlying clients is inconsistent.
531
+
532
+ When dealing with large or complex amounts of data in a KV store, some times it's useful to divide them by categories. Some examples might be:
533
+
534
+ - You use KV as a cache, and have different categories of data.
535
+ - You use KV as a session store, and want to differentiate different kinds of sessions.
536
+ - You use KV as a primary data store, and have different types of datasets.
537
+
538
+ For these and more situations, you can use `.prefix()` to simplify your life further.
539
+
436
540
  ## Creating a store
437
541
 
438
- A store needs at least 4 methods with these signatures:
542
+ To create a store, you define a class with these methods:
439
543
 
440
544
  ```js
441
- const store = {};
442
- store.get = (key: string) => Promise<any>;
443
- store.set = (key: string, value: any, { expires: number }) => Promise<string>;
444
- store.entries = (prefix: string = "") => Promise<[key:string, value:any][]>;
445
- store.clear = () => Promise<null>;
545
+ class MyClient {
546
+ // If this is set to `true`, the CLIENT (you) handle the expiration, so
547
+ // the `.set()` and `.add()` receive a `expires` that is a `null` or `number`:
548
+ EXPIRES = false;
549
+
550
+ // Mandatory methods (2 item-methods, 2 group-methods)
551
+ get (key): Promise<any>;
552
+ set (key, value, { expires: null|number }): Promise<null>;
553
+ entries (prefix): Promise<[string, any][]>;
554
+
555
+ // Optional item methods (for optimization or customization)
556
+ add (prefix, data, { expires: null|number }): Promise<string>;
557
+ has (key): Promise<boolean>;
558
+ del (key): Promise<null>;
559
+
560
+ // Optional group methods
561
+ keys (prefix): Promise<string[]>;
562
+ values (prefix): Promise<any[]>;
563
+ clear (prefix): Promise<null>;
564
+
565
+ // Optional misc method
566
+ close (): Promise<null>;
567
+ }
446
568
  ```
447
569
 
448
- All of the other methods will be implemented on top of these if not available, but you can provide those as well for optimizations, incompatible APIs, etc. For example, `.set('a', null)` _should_ delete the key `a`, and for this you may provide a native implementation:
570
+ Note that this is NOT the public API, it's the internal **client** API. It's simpler than the public API since we do some of the heavy lifting as an intermediate layer (e.g. the `expires` will always be a `null` or `number`, never `undefined` or a `string`), but also it differs from polystore's API, like `.add()` has a different signature, and the group methods all take a explicit prefix.
571
+
572
+ **Expires**: if you set the `EXPIRES = true`, then you are indicating that the client WILL manage the lifecycle of the data. This includes all methods, for example if an item is expired, then its key should not be returned in `.keys()`, it's value should not be returned in `.values()`, and the method `.has()` will return `false`. The good news is that you will always receive the option `expires`, which is either `null` (no expiration) or a `number` indicating the time when it will expire.
573
+
574
+ **Prefix**: we manage the `prefix` as an invisible layer on top, you only need to be aware of it in the `.add()` method, as well as in the group methods:
449
575
 
450
576
  ```js
451
- const native = myNativeStore();
577
+ // What the user of polystore does:
578
+ const store = await kv(client).prefix("hello:").prefix("world:");
452
579
 
453
- const store = {};
454
- store.get = (key) => native.getItem(key);
455
- // ...
456
- store.del = (key) => native.deleteItem(key);
580
+ // User calls this, then the client is called with that:
581
+ const value = await store.get("a");
582
+ // client.get("hello:world:a");
583
+
584
+ // User calls this, then the client is called with that:
585
+ const value = await store.entries();
586
+ // client.entries("hello:world:");
587
+ ```
588
+
589
+ > Note: all of the _group methods_ that return keys, should return them **with the prefix stripped**:
590
+
591
+ ```js
592
+ // Example if your client works around a simple object {}, we want to remove
593
+ // the `prefix` from the beginning of the keys returned:
594
+ client.keys = (prefix) => {
595
+ return Object.keys(subStore)
596
+ .filter((key) => key.startsWith(prefix))
597
+ .map((key) => key.slice(prefix.length)); // <= Important!
598
+ };
599
+ ```
600
+
601
+ You can and should just concatenate the `key + options.prefix`. We don't do it for two reasons: in some cases, like `.add()`, there's no key that we can use to concatenate, and also you might
602
+
603
+ For example, if the user of `polystore` does `kv(client).prefix('hello:').get('a')`, your store will be directly called with `client.get('a', { prefix: 'hello:' })`. You can safely concatenate `options.prefix + key` since this library always ensures that the prefix is defined and defaults to `''`. We don't concatenate it interally because in some cases (like in `.add()`) it makes more sense that this is handled by the client as an optimization.
604
+
605
+ While the signatures are different, you can check each entries on the output of Polystore API to see what is expected for the methods of the client to do, e.g. `.clear()` will remove all of the items that match the prefix (or everything if there's no prefix).
606
+
607
+ **Example: Plain Object client**
608
+
609
+ This is a good example of how simple a store can be, however do not use it literally since it behaves the same as the already-supported `new Map()`, only use it as the base for your own clients:
610
+
611
+ ```js
612
+ const dataSource = {};
613
+
614
+ class MyClient {
615
+ get(key) {
616
+ return dataSource[key];
617
+ }
618
+
619
+ // No need to stringify it or anything for a plain object storage
620
+ set(key, value) {
621
+ dataSource[key] = value;
622
+ }
623
+
624
+ // Filter them by the prefix, note that `prefix` will always be a string
625
+ entries(prefix) {
626
+ const entries = Object.entries(dataSource);
627
+ if (!prefix) return entries;
628
+ return entries.filter(([key, value]) => key.startsWith(prefix));
629
+ }
630
+ }
631
+ ```
632
+
633
+ We don't set `EXPIRES` to true since plain objects do NOT support expiration natively. So by not adding the `EXPIRES` property, it's the same as setting it to `false`, and polystore will manage all the expirations as a layer on top of the data. We could be more explicit and set it to `EXPIRES = false`, but it's not needed in this case.
634
+
635
+ **Example: custom ID generation**
636
+
637
+ You might want to provide your custom key generation algorithm, which I'm going to call `customId()` for example purposes. The only place where `polystore` generates IDs is in `add`, so you can provide your client with a custom generator:
638
+
639
+ ```js
640
+ class MyClient {
641
+
642
+ // Add the opt method .add() to have more control over the ID generation
643
+ async add (prefix, data, { expires }) {
644
+ const id = customId();
645
+ const key = prefix + id;
646
+ return this.set(key, data, { expires });
647
+ }
648
+
649
+ //
650
+ async set (...) {
651
+ // ...
652
+ }
653
+ }
654
+ ```
655
+
656
+ That way, when using the store, you can simply use `.add()` to generate it:
657
+
658
+ ```js
659
+ import kv from "polystore";
660
+
661
+ const store = kv(MyClient);
662
+ const id = await store.add({ hello: "world" });
663
+ // this is your own custom id
664
+ const id2 = await store.prefix("hello:").add({ hello: "world" });
665
+ // this is `hello:{your own custom id}`
457
666
  ```
@@ -0,0 +1,49 @@
1
+ // Use Cloudflare's KV store
2
+ export default class Cloudflare {
3
+ // Indicate that the file handler does NOT handle expirations
4
+ EXPIRES = true;
5
+
6
+ // Check whether the given store is a FILE-type
7
+ static test(client) {
8
+ return (
9
+ client?.constructor?.name === "KvNamespace" ||
10
+ client?.constructor?.name === "EdgeKVNamespace"
11
+ );
12
+ }
13
+
14
+ constructor(client) {
15
+ this.client = client;
16
+ }
17
+
18
+ async get(key) {
19
+ const data = await this.client.get(key);
20
+ if (!data) return null;
21
+ return JSON.parse(data);
22
+ }
23
+
24
+ async set(key, value, { expires } = {}) {
25
+ const expirationTtl = expires ? Math.round(expires) : undefined;
26
+ this.client.put(key, JSON.stringify(value), { expirationTtl });
27
+ return key;
28
+ }
29
+
30
+ async del(key) {
31
+ return this.client.delete(key);
32
+ }
33
+
34
+ async keys(prefix = "") {
35
+ const raw = await this.client.list({ prefix });
36
+ return raw.keys.map((k) => k.name);
37
+ }
38
+
39
+ async entries(prefix = "") {
40
+ const keys = await this.keys(prefix);
41
+ const values = await Promise.all(keys.map((k) => this.get(k)));
42
+ return keys.map((key, i) => [key, values[i]]);
43
+ }
44
+
45
+ async clear(prefix = "") {
46
+ const list = await this.keys(prefix);
47
+ return Promise.all(list.map((k) => this.del(k)));
48
+ }
49
+ }
@@ -0,0 +1,55 @@
1
+ // A client that uses a single file (JSON) as a store
2
+ export default class Cookie {
3
+ // Indicate if this client handles expirations (true = it does)
4
+ EXPIRES = true;
5
+
6
+ // Check if this is the right class for the given client
7
+ static test(client) {
8
+ return client === "cookie" || client === "cookies";
9
+ }
10
+
11
+ // For cookies, an empty value is the same as null, even `""`
12
+ get(key) {
13
+ return this.all()[key] || null;
14
+ }
15
+
16
+ set(key, data = null, { expires } = {}) {
17
+ // Setting it to null deletes it
18
+ if (data === null) {
19
+ data = "";
20
+ expires = -100;
21
+ }
22
+
23
+ let expireStr = "";
24
+ // NOTE: 0 is already considered here!
25
+ if (expires !== null) {
26
+ const now = new Date().getTime();
27
+ const time = new Date(now + expires * 1000).toUTCString();
28
+ expireStr = `; expires=${time}`;
29
+ }
30
+
31
+ const value = encodeURIComponent(JSON.stringify(data));
32
+ document.cookie = encodeURIComponent(key) + "=" + value + expireStr;
33
+ return key;
34
+ }
35
+
36
+ // Group methods
37
+ all(prefix = "") {
38
+ const all = {};
39
+ for (let entry of document.cookie.split(";")) {
40
+ const [key, data] = entry.split("=");
41
+ const name = decodeURIComponent(key.trim());
42
+ if (!name.startsWith(prefix)) continue;
43
+ try {
44
+ all[name] = JSON.parse(decodeURIComponent(data.trim()));
45
+ } catch (error) {
46
+ // no-op (some 3rd party can set cookies independently)
47
+ }
48
+ }
49
+ return all;
50
+ }
51
+
52
+ entries(prefix = "") {
53
+ return Object.entries(this.all(prefix));
54
+ }
55
+ }