polystore 0.7.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,39 +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
-
26
- Available stores:
27
-
28
- - **Memory** `new Map()` (fe+be): an in-memory API to keep your KV store
29
- - **Local Storage** `localStorage` (fe): persist the data in the browser's localStorage
30
- - **Session Storage** `sessionStorage` (fe): persist the data in the browser's sessionStorage
31
- - **Cookies** `"cookie"` (fe): persist the data using cookies
32
- - **LocalForage** `localForage` (fe): persist the data on IndexedDB
33
- - **FS File** `new URL('file:///...')` (be): store the data in a single JSON file
34
- - **Redis Client** `redisClient` (be): use the Redis instance that you connect to
35
- - **Cloudflare KV** `env.KV_NAMESPACE` (be): use Cloudflare's KV store
36
- - (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.
37
44
 
38
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:
39
46
 
@@ -49,7 +56,7 @@ MyApi({ cache: env.KV_NAMESPACE }); // OR
49
56
 
50
57
  ## API
51
58
 
52
- 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:
53
60
 
54
61
  ```js
55
62
  import kv from "polystore";
@@ -163,7 +170,7 @@ if (await store.has('cookie-consent')) {
163
170
 
164
171
  ### .del()
165
172
 
166
- Remove a single key from the store:
173
+ Remove a single key from the store and return the key itself:
167
174
 
168
175
  ```js
169
176
  await store.del(key: string);
@@ -224,9 +231,11 @@ Remove all of the data from the store:
224
231
  await store.clear();
225
232
  ```
226
233
 
227
- ### .prefix() (TODO) (unstable)
234
+ ### .prefix()
228
235
 
229
- Create a sub-store where all the operations use the given prefix:
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:
230
239
 
231
240
  ```js
232
241
  const store = kv(new Map());
@@ -236,27 +245,35 @@ const session = store.prefix("session:");
236
245
  Then all of the operations will be converted internally to add the prefix when reading, writing, etc:
237
246
 
238
247
  ```js
239
- const val = await session.get("key1"); // .get('session:key1');
240
- await session.set("key2", "some data"); // .set('session:key2', ...);
241
- const val = await session.has("key3"); // .has('session:key3');
242
- await session.del("key4"); // .del('session:key4');
243
- 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
244
254
  // ['key1', 'key2', ...] Note no prefix here
245
255
  await session.clear(); // delete only keys with the prefix
246
256
  ```
247
257
 
248
- 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:
249
259
 
250
260
  ```js
261
+ // Two in-memory stores
251
262
  const store = kv(new Map());
252
263
  const session = kv(new Map());
264
+
265
+ // Two file-stores
266
+ const users = kv(new URL(`file://${import.meta.dirname}/users.json`));
267
+ const books = kv(new URL(`file://${import.meta.dirname}/books.json`));
253
268
  ```
254
269
 
255
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_.
256
271
 
257
- ## Stores
272
+ ## Clients
273
+
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"`.
258
275
 
259
- 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.
276
+ Polystore provides a unified API you can use `Promises`, `expires` and `.prefix()` even with those stores that do not support these operations natively.
260
277
 
261
278
  ### Memory
262
279
 
@@ -265,18 +282,31 @@ An in-memory KV store, with promises and expiration time:
265
282
  ```js
266
283
  import kv from "polystore";
267
284
 
268
- // This already works, by default if there's nothing it'll use
269
- // a new Map()
270
- const store = kv();
271
- await store.set("key1", "Hello world");
285
+ const store = kv(new Map());
286
+
287
+ await store.set("key1", "Hello world", { expires: "1h" });
272
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";
273
296
 
274
- // Or you can be explicit:
297
+ const store = kv();
275
298
  const store = kv(new Map());
276
- await store.set("key1", "Hello world");
277
- console.log(await store.get("key1"));
278
299
  ```
279
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
+
280
310
  ### Local Storage
281
311
 
282
312
  The traditional localStorage that we all know and love, this time with a unified API, and promises:
@@ -285,8 +315,10 @@ The traditional localStorage that we all know and love, this time with a unified
285
315
  import kv from "polystore";
286
316
 
287
317
  const store = kv(localStorage);
288
- await store.set("key1", "Hello world");
318
+
319
+ await store.set("key1", "Hello world", { expires: "1h" });
289
320
  console.log(await store.get("key1"));
321
+ // "Hello world"
290
322
  ```
291
323
 
292
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)!
@@ -299,8 +331,10 @@ Same as localStorage, but now for the session only:
299
331
  import kv from "polystore";
300
332
 
301
333
  const store = kv(sessionStorage);
302
- await store.set("key1", "Hello world");
334
+
335
+ await store.set("key1", "Hello world", { expires: "1h" });
303
336
  console.log(await store.get("key1"));
337
+ // "Hello world"
304
338
  ```
305
339
 
306
340
  ### Cookies
@@ -310,9 +344,11 @@ Supports native browser cookies, including setting the expire time:
310
344
  ```js
311
345
  import kv from "polystore";
312
346
 
313
- const store = kv("cookie"); // yes, just a plain string
314
- 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" });
315
350
  console.log(await store.get("key1"));
351
+ // "Hello world"
316
352
  ```
317
353
 
318
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).
@@ -328,8 +364,10 @@ import kv from "polystore";
328
364
  import localForage from "localforage";
329
365
 
330
366
  const store = kv(localForage);
367
+
331
368
  await store.set("key1", "Hello world", { expires: "1h" });
332
369
  console.log(await store.get("key1"));
370
+ // "Hello world"
333
371
  ```
334
372
 
335
373
  ### Redis Client
@@ -340,25 +378,38 @@ Supports the official Node Redis Client. You can pass either the client or the p
340
378
  import kv from "polystore";
341
379
  import { createClient } from "redis";
342
380
 
343
- // Note: no need for await or similar
344
381
  const store = kv(createClient().connect());
345
- await store.set("key1", "Hello world");
382
+
383
+ await store.set("key1", "Hello world", { expires: "1h" });
346
384
  console.log(await store.get("key1"));
385
+ // "Hello world"
347
386
  ```
348
387
 
388
+ You don't need to `await` for the connect or similar, this will process it properly.
389
+
349
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.
350
391
 
351
- ### FS File
392
+ ### File
393
+
394
+ Treat a JSON file in your filesystem as the source for the KV store:
352
395
 
353
396
  ```js
354
397
  import kv from "polystore";
355
398
 
356
- // Create a url with the file protocol:
357
399
  const store = kv(new URL("file:///Users/me/project/cache.json"));
358
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
359
409
  // Paths need to be absolute, but you can use process.cwd() to make
360
410
  // it relative to the current process:
361
- const store = kv(new URL(`file://${process.cwd()}/cache.json`));
411
+ const store1 = kv(new URL(`file://${process.cwd()}/cache.json`));
412
+ const store2 = kv(new URL(`file://${import.meta.dirname}/data.json`));
362
413
  ```
363
414
 
364
415
  ### Cloudflare KV
@@ -372,18 +423,16 @@ export default {
372
423
  async fetch(request, env, ctx) {
373
424
  const store = kv(env.YOUR_KV_NAMESPACE);
374
425
 
375
- await store.set("KEY", "VALUE");
376
- 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"
377
429
 
378
- if (!value) {
379
- return new Response("Value not found", { status: 404 });
380
- }
381
- return new Response(value);
430
+ return new Response("My response");
382
431
  },
383
432
  };
384
433
  ```
385
434
 
386
- 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:
387
436
 
388
437
  ```js
389
438
  // GOOD - with polystore
@@ -397,9 +446,57 @@ await env.YOUR_KV_NAMESPACE.put("user", serialValue, {
397
446
  });
398
447
  ```
399
448
 
400
- ## Expiration explained
449
+ ### Level
450
+
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)).
401
498
 
402
- 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:
499
+ We unify all of the clients diverse expiration methods into a single, easy one with `expires`:
403
500
 
404
501
  ```js
405
502
  // in-memory store
@@ -414,7 +511,7 @@ console.log(await store.get("a")); // 'b'
414
511
  // Make sure the key is expired
415
512
  await delay(2000); // 2s
416
513
 
417
- // Not only the .get() is null, but `.has()` returns false, and .keys() ignores it
514
+ // The group methods also ignore expired keys
418
515
  console.log(await store.keys()); // []
419
516
  console.log(await store.has("a")); // false
420
517
  console.log(await store.get("a")); // null
@@ -426,25 +523,144 @@ However, in some stores this does come with some potential performance disadvant
426
523
 
427
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.
428
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
+
429
540
  ## Creating a store
430
541
 
431
- A store needs at least 4 methods with these signatures:
542
+ To create a store, you define a class with these methods:
432
543
 
433
544
  ```js
434
- const store = {};
435
- store.get = (key: string) => Promise<any>;
436
- store.set = (key: string, value: any, { expires: number }) => Promise<string>;
437
- store.entries = (prefix: string = "") => Promise<[key:string, value:any][]>;
438
- 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
+ }
439
568
  ```
440
569
 
441
- 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:
442
575
 
443
576
  ```js
444
- const native = myNativeStore();
577
+ // What the user of polystore does:
578
+ const store = await kv(client).prefix("hello:").prefix("world:");
445
579
 
446
- const store = {};
447
- store.get = (key) => native.getItem(key);
448
- // ...
449
- 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}`
450
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
+ }