polystore 0.10.0 → 0.11.1

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,15 +1,18 @@
1
1
  {
2
2
  "name": "polystore",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
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",
7
7
  "bugs": "https://github.com/franciscop/polystore/issues",
8
8
  "funding": "https://www.paypal.me/franciscopresencia/19",
9
9
  "author": "Francisco Presencia <public@francisco.io> (https://francisco.io/)",
10
- "main": "src/index.js",
11
10
  "type": "module",
11
+ "main": "src/index.js",
12
12
  "types": "src/index.d.ts",
13
+ "files": [
14
+ "src/"
15
+ ],
13
16
  "scripts": {
14
17
  "size": "echo $(gzip -c src/index.js | wc -c) bytes",
15
18
  "start": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch --coverage --detectOpenHandles",
@@ -24,6 +27,7 @@
24
27
  "value"
25
28
  ],
26
29
  "license": "MIT",
30
+ "dependencies": {},
27
31
  "devDependencies": {
28
32
  "@deno/kv": "^0.8.1",
29
33
  "check-dts": "^0.8.0",
package/readme.md CHANGED
@@ -241,6 +241,26 @@ Remove a single key from the store and return the key itself:
241
241
  await store.del(key: string);
242
242
  ```
243
243
 
244
+ It will ignore the operation if the key or value don't exist already (but won't thorw).
245
+
246
+ ### _Iterator_
247
+
248
+ You can iterate over the whole store with an async iterator:
249
+
250
+ ```js
251
+ for await (const [key, value] of store) {
252
+ // ...
253
+ }
254
+ ```
255
+
256
+ This is very useful for performance resons. You can also iterate on a subset of the entries with .prefix:
257
+
258
+ ```js
259
+ for await (const [key, value] of store.prefix("session:")) {
260
+ // ...
261
+ }
262
+ ```
263
+
244
264
  ### .keys()
245
265
 
246
266
  Get all of the keys in the store, optionally filtered by a prefix:
@@ -615,7 +635,7 @@ class MyClient {
615
635
  // Mandatory methods
616
636
  get (key): Promise<any>;
617
637
  set (key, value, { expires: null|number }): Promise<null>;
618
- entries (prefix): Promise<[string, any][]>;
638
+ iterate(prefix): AyncIterator<[string, any]>
619
639
 
620
640
  // Optional item methods (for optimization or customization)
621
641
  add (prefix, data, { expires: null|number }): Promise<string>;
@@ -623,6 +643,7 @@ class MyClient {
623
643
  del (key): Promise<null>;
624
644
 
625
645
  // Optional group methods
646
+ entries (prefix): Promise<[string, any][]>;
626
647
  keys (prefix): Promise<string[]>;
627
648
  values (prefix): Promise<any[]>;
628
649
  clear (prefix): Promise<null>;
@@ -647,8 +668,9 @@ const value = await store.get("a");
647
668
  // client.get("hello:world:a");
648
669
 
649
670
  // User calls this, then the client is called with that:
650
- const value = await store.entries();
651
- // client.entries("hello:world:");
671
+ for await (const entry of store.iterate()) {
672
+ }
673
+ // client.iterate("hello:world:");
652
674
  ```
653
675
 
654
676
  > Note: all of the _group methods_ that return keys, should return them **with the prefix**:
@@ -680,10 +702,12 @@ class MyClient {
680
702
  }
681
703
 
682
704
  // Filter them by the prefix, note that `prefix` will always be a string
683
- entries(prefix) {
684
- const entries = Object.entries(dataSource);
685
- if (!prefix) return entries;
686
- return entries.filter(([key, value]) => key.startsWith(prefix));
705
+ *iterate(prefix) {
706
+ for (const [key, value] of Object.entries(dataSource)) {
707
+ if (key.startsWith(prefix)) {
708
+ yield [key, value];
709
+ }
710
+ }
687
711
  }
688
712
  }
689
713
  ```
@@ -31,9 +31,33 @@ export default class Cloudflare {
31
31
  return this.client.delete(key);
32
32
  }
33
33
 
34
+ // Since we have pagination, we don't want to get all of the
35
+ // keys at once if we can avoid it
36
+ async *iterate(prefix = "") {
37
+ let cursor;
38
+ do {
39
+ const raw = await this.client.list({ prefix, cursor });
40
+ const keys = raw.keys.map((k) => k.name);
41
+ for (let key of keys) {
42
+ const value = await this.get(key);
43
+ // By the time this specific value is read, it could
44
+ // already be gone!
45
+ if (!value) continue;
46
+ yield [key, value];
47
+ }
48
+ cursor = raw.list_complete ? null : raw.cursor;
49
+ } while (cursor);
50
+ }
51
+
34
52
  async keys(prefix = "") {
35
- const raw = await this.client.list({ prefix });
36
- return raw.keys.map((k) => k.name);
53
+ const keys = [];
54
+ let cursor;
55
+ do {
56
+ const raw = await this.client.list({ prefix, cursor });
57
+ keys.push(...raw.keys.map((k) => k.name));
58
+ cursor = raw.list_complete ? null : raw.cursor;
59
+ } while (cursor);
60
+ return keys;
37
61
  }
38
62
 
39
63
  async entries(prefix = "") {
@@ -8,9 +8,25 @@ export default class Cookie {
8
8
  return client === "cookie" || client === "cookies";
9
9
  }
10
10
 
11
+ // Group methods
12
+ #read() {
13
+ const all = {};
14
+ for (let entry of document.cookie.split(";")) {
15
+ try {
16
+ const [rawKey, rawValue] = entry.split("=");
17
+ const key = decodeURIComponent(rawKey.trim());
18
+ const value = JSON.parse(decodeURIComponent(rawValue.trim()));
19
+ all[key] = value;
20
+ } catch (error) {
21
+ // no-op (some 3rd party can set cookies independently)
22
+ }
23
+ }
24
+ return all;
25
+ }
26
+
11
27
  // For cookies, an empty value is the same as null, even `""`
12
28
  get(key) {
13
- return this.all()[key] || null;
29
+ return this.#read()[key] || null;
14
30
  }
15
31
 
16
32
  set(key, data = null, { expires } = {}) {
@@ -33,23 +49,10 @@ export default class Cookie {
33
49
  return key;
34
50
  }
35
51
 
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
- }
52
+ async *iterate(prefix = "") {
53
+ for (let [key, value] of Object.entries(this.#read())) {
54
+ if (!key.startsWith(prefix)) continue;
55
+ yield [key, value];
48
56
  }
49
- return all;
50
- }
51
-
52
- entries(prefix = "") {
53
- return Object.entries(this.all(prefix));
54
57
  }
55
58
  }
@@ -21,6 +21,13 @@ export default class Etcd {
21
21
  await this.client.put(key).value(JSON.stringify(value));
22
22
  }
23
23
 
24
+ async *iterate(prefix = "") {
25
+ const keys = await this.client.getAll().prefix(prefix).keys();
26
+ for (const key of keys) {
27
+ yield [key, await this.get(key)];
28
+ }
29
+ }
30
+
24
31
  async entries(prefix = "") {
25
32
  const keys = await this.client.getAll().prefix(prefix).keys();
26
33
  const values = await Promise.all(keys.map((k) => this.get(k)));
@@ -53,10 +53,12 @@ export default class File {
53
53
  return key;
54
54
  }
55
55
 
56
- // Group methods
57
- async entries(prefix = "") {
56
+ async *iterate(prefix = "") {
58
57
  const data = await this.#read();
59
- return Object.entries(data).filter((p) => p[0].startsWith(prefix));
58
+ const entries = Object.entries(data).filter((p) => p[0].startsWith(prefix));
59
+ for (const entry of entries) {
60
+ yield entry;
61
+ }
60
62
  }
61
63
 
62
64
  async clear(prefix = "") {
@@ -22,12 +22,18 @@ export default class Forage {
22
22
  return key;
23
23
  }
24
24
 
25
+ async *iterate(prefix = "") {
26
+ const keys = await this.client.keys();
27
+ const list = keys.filter((k) => k.startsWith(prefix));
28
+ for (const key of list) {
29
+ yield [key, await this.get(key)];
30
+ }
31
+ }
32
+
25
33
  async entries(prefix = "") {
26
34
  const all = await this.client.keys();
27
35
  const keys = all.filter((k) => k.startsWith(prefix));
28
- const values = await Promise.all(
29
- keys.map((key) => this.client.getItem(key))
30
- );
36
+ const values = await Promise.all(keys.map((key) => this.get(key)));
31
37
  return keys.map((key, i) => [key, values[i]]);
32
38
  }
33
39
 
@@ -26,6 +26,14 @@ export default class Level {
26
26
  return this.client.del(key);
27
27
  }
28
28
 
29
+ async *iterate(prefix = "") {
30
+ const keys = await this.client.keys().all();
31
+ const list = keys.filter((k) => k.startsWith(prefix));
32
+ for (const key of list) {
33
+ yield [key, await this.get(key)];
34
+ }
35
+ }
36
+
29
37
  async entries(prefix = "") {
30
38
  const keys = await this.client.keys().all();
31
39
  const list = keys.filter((k) => k.startsWith(prefix));
@@ -21,6 +21,14 @@ export default class Memory {
21
21
  this.client.delete(key);
22
22
  }
23
23
 
24
+ *iterate(prefix = "") {
25
+ const entries = this.entries();
26
+ for (const entry of entries) {
27
+ if (!entry[0].startsWith(prefix)) continue;
28
+ yield entry;
29
+ }
30
+ }
31
+
24
32
  // Group methods
25
33
  entries(prefix = "") {
26
34
  const entries = [...this.client.entries()];
@@ -37,12 +37,33 @@ export default class Redis {
37
37
  return this.client.keys(prefix + "*");
38
38
  }
39
39
 
40
+ // Go through each of the [key, value] in the set
41
+ async *iterate(prefix = "") {
42
+ const MATCH = prefix + "*";
43
+ for await (const key of this.client.scanIterator({ MATCH })) {
44
+ const value = await this.get(key);
45
+ yield [key, value];
46
+ }
47
+ }
48
+
49
+ // Optimizing the retrieval of them all in bulk by loading the values
50
+ // in parallel
40
51
  async entries(prefix = "") {
41
- const keys = await this.client.keys(prefix + "*");
52
+ const keys = await this.keys(prefix);
42
53
  const values = await Promise.all(keys.map((k) => this.get(k)));
43
54
  return keys.map((k, i) => [k, values[i]]);
44
55
  }
45
56
 
57
+ // Optimizing the retrieval of them by not getting their values
58
+ async keys(prefix = "") {
59
+ const MATCH = prefix + "*";
60
+ const keys = [];
61
+ for await (const key of this.client.scanIterator({ MATCH })) {
62
+ keys.push(key);
63
+ }
64
+ return keys;
65
+ }
66
+
46
67
  async clear(prefix = "") {
47
68
  if (!prefix) return this.client.flushAll();
48
69
 
@@ -25,6 +25,13 @@ export default class WebStorage {
25
25
  return key;
26
26
  }
27
27
 
28
+ *iterate(prefix = "") {
29
+ const entries = this.entries(prefix);
30
+ for (const entry of entries) {
31
+ yield entry;
32
+ }
33
+ }
34
+
28
35
  // Group methods
29
36
  entries(prefix = "") {
30
37
  const entries = Object.entries(this.client);
package/src/index.js CHANGED
@@ -43,9 +43,9 @@ class Store {
43
43
 
44
44
  // #region #validate()
45
45
  #validate(client) {
46
- if (!client.set || !client.get || !client.entries) {
46
+ if (!client.set || !client.get || !client.iterate) {
47
47
  throw new Error(
48
- "A client should have at least a .get(), .set() and .entries()"
48
+ "A client should have at least a .get(), .set() and .iterate()"
49
49
  );
50
50
  }
51
51
 
@@ -68,6 +68,31 @@ class Store {
68
68
  }
69
69
  }
70
70
 
71
+ #unix(expires) {
72
+ const now = new Date().getTime();
73
+ return expires === null ? null : now + expires * 1000;
74
+ }
75
+
76
+ // Check if the given data is fresh or not; if
77
+ #isFresh(data, key) {
78
+ // Should never happen, but COULD happen; schedule it for
79
+ // removal and mark it as stale
80
+ if (!data || !data.value || typeof data !== "object") {
81
+ if (key) this.del(key);
82
+ return false;
83
+ }
84
+
85
+ // It never expires, so keep it
86
+ if (data.expires === null) return true;
87
+
88
+ // It's fresh, keep it
89
+ if (data.expires > Date.now()) return true;
90
+
91
+ // It's expired, remove it
92
+ if (key) this.del(key);
93
+ return false;
94
+ }
95
+
71
96
  // #region .add()
72
97
  /**
73
98
  * Save the data on an autogenerated key, can add expiration as well:
@@ -94,9 +119,10 @@ class Store {
94
119
  }
95
120
 
96
121
  // In the data we need the timestamp since we need it "absolute":
97
- const now = new Date().getTime();
98
- const expDiff = expires === null ? null : now + expires * 1000;
99
- return this.client.add(this.PREFIX, { expires: expDiff, value });
122
+ return this.client.add(this.PREFIX, {
123
+ expires: this.#unix(expires),
124
+ value,
125
+ });
100
126
  }
101
127
 
102
128
  const id = createId();
@@ -126,7 +152,7 @@ class Store {
126
152
  const expires = parse(options.expire ?? options.expires);
127
153
 
128
154
  // Quick delete
129
- if (value === null) {
155
+ if (value === null || (typeof expires === "number" && expires <= 0)) {
130
156
  await this.del(id);
131
157
  return key;
132
158
  }
@@ -137,16 +163,8 @@ class Store {
137
163
  return key;
138
164
  }
139
165
 
140
- // Already expired, then delete it
141
- if (expires === 0) {
142
- await this.del(id);
143
- return key;
144
- }
145
-
146
166
  // In the data we need the timestamp since we need it "absolute":
147
- const now = new Date().getTime();
148
- const expDiff = expires === null ? null : now + expires * 1000;
149
- await this.client.set(id, { expires: expDiff, value });
167
+ await this.client.set(id, { expires: this.#unix(expires), value });
150
168
  return key;
151
169
  }
152
170
 
@@ -180,23 +198,8 @@ class Store {
180
198
  // so we can assume it's the raw user data
181
199
  if (this.client.EXPIRES) return data;
182
200
 
183
- // Make sure that if there's no data by now, empty is returned
184
- if (!data) return null;
185
-
186
- // We manage expiration manually, so we know it should have this structure
187
- // TODO: ADD A CHECK HERE
188
- const { expires, value } = data;
189
-
190
- // It never expires
191
- if (expires === null) return value ?? null;
192
-
193
- // Already expired! Return nothing, and remove the whole key
194
- if (expires <= new Date().getTime()) {
195
- await this.del(key);
196
- return null;
197
- }
198
-
199
- return value;
201
+ if (!this.#isFresh(data, key)) return null;
202
+ return data.value;
200
203
  }
201
204
 
202
205
  // #region .has()
@@ -256,6 +259,19 @@ class Store {
256
259
  return key;
257
260
  }
258
261
 
262
+ async *[Symbol.asyncIterator]() {
263
+ await this.promise;
264
+
265
+ for await (const [name, data] of this.client.iterate(this.PREFIX)) {
266
+ const key = name.slice(this.PREFIX.length);
267
+ if (this.client.EXPIRES) {
268
+ yield [key, data];
269
+ } else if (this.#isFresh(data, key)) {
270
+ yield [key, data.value];
271
+ }
272
+ }
273
+ }
274
+
259
275
  // #region .entries()
260
276
  /**
261
277
  * Return an array of the entries, in the [key, value] format:
@@ -274,36 +290,25 @@ class Store {
274
290
  async entries() {
275
291
  await this.promise;
276
292
 
277
- const entries = await this.client.entries(this.PREFIX);
278
- const list = entries.map(([key, data]) => [
279
- key.slice(this.PREFIX.length),
280
- data,
281
- ]);
293
+ let list = [];
294
+ if (this.client.entries) {
295
+ list = (await this.client.entries(this.PREFIX)).map(([key, value]) => [
296
+ key.slice(this.PREFIX.length),
297
+ value,
298
+ ]);
299
+ } else {
300
+ for await (const [key, value] of this.client.iterate(this.PREFIX)) {
301
+ list.push([key.slice(this.PREFIX.length), value]);
302
+ }
303
+ }
282
304
 
283
305
  // The client already manages the expiration, so we can assume
284
306
  // that at this point, all entries are not-expired
285
307
  if (this.client.EXPIRES) return list;
286
308
 
287
309
  // We need to do manual expiration checking
288
- const now = new Date().getTime();
289
310
  return list
290
- .filter(([key, data]) => {
291
- // Should never happen
292
- if (!data || data.value === null) return false;
293
-
294
- // It never expires, so keep it
295
- const { expires } = data;
296
- if (expires === null) return true;
297
-
298
- // It's expired, so remove it
299
- if (expires <= now) {
300
- this.del(key);
301
- return false;
302
- }
303
-
304
- // It's not expired, keep it
305
- return true;
306
- })
311
+ .filter(([key, data]) => this.#isFresh(data, key))
307
312
  .map(([key, data]) => [key, data.value]);
308
313
  }
309
314
 
@@ -356,23 +361,8 @@ class Store {
356
361
  if (this.client.values) {
357
362
  const list = this.client.values(this.PREFIX);
358
363
  if (this.client.EXPIRES) return list;
359
- const now = new Date().getTime();
360
364
  return list
361
- .filter((data) => {
362
- // There's no data, so remove this
363
- if (!data || data.value === null) return false;
364
-
365
- // It never expires, so keep it
366
- const { expires } = data;
367
- if (expires === null) return true;
368
-
369
- // It's expired, so remove it
370
- // We cannot unfortunately evict it since we don't know the key!
371
- if (expires <= now) return false;
372
-
373
- // It's not expired, keep it
374
- return true;
375
- })
365
+ .filter((data) => this.#isFresh(data))
376
366
  .map((data) => data.value);
377
367
  }
378
368
 
package/src/index.test.js CHANGED
@@ -11,8 +11,6 @@ import kv from "./index.js";
11
11
  import customFull from "./test/customFull.js";
12
12
  import customSimple from "./test/customSimple.js";
13
13
 
14
- global.setImmediate = global.setImmediate || ((cb) => setTimeout(cb, 0));
15
-
16
14
  const stores = [];
17
15
  stores.push(["kv()", kv()]);
18
16
  stores.push(["kv(new Map())", kv(new Map())]);
@@ -30,6 +28,7 @@ if (process.env.REDIS) {
30
28
  stores.push(["kv(redis)", kv(createClient())]);
31
29
  }
32
30
  if (process.env.ETCD) {
31
+ // Note: need to add to .env "ETCD=true" and run `etcd` in the terminal
33
32
  stores.push(["kv(new Etcd3())", kv(new Etcd3())]);
34
33
  }
35
34
 
@@ -41,7 +40,7 @@ const delay = (t) => new Promise((done) => setTimeout(done, t));
41
40
  class Base {
42
41
  get() {}
43
42
  set() {}
44
- entries() {}
43
+ *iterate() {}
45
44
  }
46
45
 
47
46
  global.console = {
@@ -56,7 +55,7 @@ describe("potato", () => {
56
55
 
57
56
  it("an empty object is not a valid store", async () => {
58
57
  await expect(() => kv({}).get("any")).rejects.toThrow({
59
- message: "A client should have at least a .get(), .set() and .entries()",
58
+ message: "A client should have at least a .get(), .set() and .iterate()",
60
59
  });
61
60
  });
62
61
 
@@ -343,6 +342,42 @@ for (let [name, store] of stores) {
343
342
  expect(await store.get("a")).toBe("b");
344
343
  });
345
344
 
345
+ describe("iteration", () => {
346
+ beforeEach(async () => {
347
+ await store.clear();
348
+ });
349
+
350
+ it("supports raw iteration", async () => {
351
+ await store.set("a", "b");
352
+ await store.set("c", "d");
353
+
354
+ const entries = [];
355
+ for await (const entry of store) {
356
+ entries.push(entry);
357
+ }
358
+ expect(entries).toEqual([
359
+ ["a", "b"],
360
+ ["c", "d"],
361
+ ]);
362
+ });
363
+
364
+ it("supports raw prefix iteration", async () => {
365
+ await store.set("a:a", "b");
366
+ await store.set("b:a", "d");
367
+ await store.set("a:c", "d");
368
+ await store.set("b:c", "d");
369
+
370
+ const entries = [];
371
+ for await (const entry of store.prefix("a:")) {
372
+ entries.push(entry);
373
+ }
374
+ expect(entries.sort()).toEqual([
375
+ ["a", "b"],
376
+ ["c", "d"],
377
+ ]);
378
+ });
379
+ });
380
+
346
381
  describe("expires", () => {
347
382
  // The mock implementation does NOT support expiration 😪
348
383
  if (name === "kv(new KVNamespace())") return;
@@ -19,6 +19,13 @@ export default class MyClient {
19
19
  delete dataSource[key];
20
20
  }
21
21
 
22
+ *iterate(prefix) {
23
+ const entries = this.entries(prefix);
24
+ for (const entry of entries) {
25
+ yield entry;
26
+ }
27
+ }
28
+
22
29
  // Filter them by the prefix, note that `prefix` will always be a string
23
30
  entries(prefix) {
24
31
  const entries = Object.entries(dataSource);
@@ -15,9 +15,13 @@ export default class MyClient {
15
15
  }
16
16
 
17
17
  // Filter them by the prefix, note that `prefix` will always be a string
18
- entries(prefix) {
19
- const entries = Object.entries(dataSource);
20
- if (!prefix) return entries;
21
- return entries.filter(([key, value]) => key.startsWith(prefix));
18
+ *iterate(prefix) {
19
+ const raw = Object.entries(dataSource);
20
+ const entries = prefix
21
+ ? raw.filter(([key, value]) => key.startsWith(prefix))
22
+ : raw;
23
+ for (const entry of entries) {
24
+ yield entry;
25
+ }
22
26
  }
23
27
  }
package/src/test/setup.js CHANGED
@@ -14,3 +14,10 @@ if (typeof TextDecoder === "undefined") {
14
14
  value: util.TextDecoder,
15
15
  });
16
16
  }
17
+
18
+ if (typeof setImmediate === "undefined") {
19
+ Object.defineProperty(window, "setImmediate", {
20
+ writable: true,
21
+ value: (cb) => setTimeout(cb, 0),
22
+ });
23
+ }
@@ -1 +0,0 @@
1
- custom: https://www.paypal.me/franciscopresencia/19
@@ -1,24 +0,0 @@
1
- name: tests
2
-
3
- on: [push]
4
-
5
- jobs:
6
- build:
7
- runs-on: ubuntu-latest
8
-
9
- strategy:
10
- matrix:
11
- node-version: [18.x, 20.x]
12
-
13
- steps:
14
- - uses: actions/checkout@v4
15
- - name: Use Node.js ${{ matrix.node-version }}
16
- uses: actions/setup-node@v4
17
- with:
18
- node-version: ${{ matrix.node-version }}
19
- - name: install dependencies
20
- run: npm install
21
- - name: npm test
22
- run: npm test
23
- env:
24
- CI: true
Binary file
Binary file
Binary file
package/assets/home.html DELETED
@@ -1,376 +0,0 @@
1
- <br />
2
-
3
- <section class="hero flex center nowrap">
4
- <div>
5
- <h1>Polystore</h1>
6
- <p style="max-width: 620px">
7
- A library to unify KV-stores. Allows you to write code that works on any
8
- KV store, both on the front-end and backend. Supports substores and
9
- intuitive expiration times. Get started:
10
- </p>
11
- <pre class="small">npm install polystore</pre>
12
- <div class="buttons">
13
- <a class="button" href="/documentation">Documentation</a>
14
- <a
15
- class="pseudo button"
16
- href="https://superpeer.com/francisco/-/javascript-and-react-help"
17
- >Professional JS help</a
18
- >
19
- </div>
20
- </div>
21
- <div style="width: 570px; max-width: 100%">
22
- <pre><code class="language-js">import kv from &quot;polystore&quot;;
23
- import { createClient } from "redis";
24
-
25
- const REDIS = process.env.REDIS_URL;
26
- const store = kv(createClient(REDIS));
27
-
28
- await store.set(key, data, { expires: "1h" });
29
- console.log(await store.get(key));
30
- // { hello: &quot;world&quot; }</code></pre>
31
- </div>
32
- </section>
33
-
34
- <div class="separator">
35
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 20">
36
- <circle cx="10" cy="10" r="3" />
37
- <line x1="20" y1="10" x2="260" y2="10" />
38
- <circle cx="270" cy="10" r="3" />
39
- <circle class="empty" cx="283" cy="10" r="3" />
40
- <circle class="empty" cx="300" cy="10" r="5" />
41
- <circle class="empty" cx="317" cy="10" r="3" />
42
- <circle cx="330" cy="10" r="3" />
43
- <line x1="340" y1="10" x2="580" y2="10" />
44
- <circle cx="590" cy="10" r="3" />
45
- </svg>
46
- </div>
47
-
48
- <section class="features flex one two-500 three-900">
49
- <div>
50
- <div class="feature">
51
- <header>
52
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
53
- <path
54
- d="M18 8h1a4 4 0 010 8h-1M2 8h16v9a4 4 0 01-4 4H6a4 4 0 01-4-4V8zM6 1v3M10 1v3M14 1v3"
55
- />
56
- </svg>
57
- <h3>Easy peasy</h3>
58
- </header>
59
- <p>
60
- It's a KV store. It has <code>add()</code>, <code>set()</code>,
61
- <code>get()</code>, <code>has()</code>, <code>del()</code>
62
- <a href="/documentation#api" target="_blank">and more</a>.
63
- </p>
64
- </div>
65
- </div>
66
-
67
- <div>
68
- <div class="feature">
69
- <header>
70
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
71
- <path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
72
- <path d="M14 2v6h6M16 13H8M16 17H8M10 9H8" />
73
- </svg>
74
- <h3>Documented</h3>
75
- </header>
76
- <p>
77
- <a href="/documentation#getting-started">Getting started</a>,
78
- <a href="/documentation#api">API</a>,
79
- <a href="/documentation#clients">Clients</a> and
80
- <a href="/documentation#creating-a-store">custom stores</a> for your
81
- convenience.
82
- </p>
83
- </div>
84
- </div>
85
- <div>
86
- <div class="feature">
87
- <header>
88
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
89
- <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
90
- <polyline points="22 4 12 14.01 9 11.01"></polyline>
91
- </svg>
92
- <h3>Tested and Typed</h3>
93
- </header>
94
- <p>
95
- <a
96
- href="https://github.com/franciscop/polystore/actions"
97
- target="_blank"
98
- >600+ tests</a
99
- >, TS definitions and JSDocs for the best experience using the library.
100
- </p>
101
- </div>
102
- </div>
103
- <div>
104
- <div class="feature">
105
- <header>
106
- <svg
107
- xmlns="http://www.w3.org/2000/svg"
108
- width="24"
109
- height="24"
110
- viewBox="0 0 24 24"
111
- fill="none"
112
- stroke="currentColor"
113
- stroke-width="2"
114
- stroke-linecap="round"
115
- stroke-linejoin="round"
116
- class="feather feather-globe"
117
- >
118
- <circle cx="12" cy="12" r="10"></circle>
119
- <line x1="2" y1="12" x2="22" y2="12"></line>
120
- <path
121
- d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"
122
- ></path>
123
- </svg>
124
- <h3>Universal Javascript</h3>
125
- </header>
126
- <p>
127
- Use it with React, Angular, Plain JS, Node.js, Bun, Tauri, Electron,
128
- etc.
129
- </p>
130
- </div>
131
- </div>
132
-
133
- <div>
134
- <div class="feature">
135
- <header>
136
- <svg
137
- xmlns="http://www.w3.org/2000/svg"
138
- width="24"
139
- height="24"
140
- viewBox="0 0 24 24"
141
- fill="none"
142
- stroke="currentColor"
143
- stroke-width="2"
144
- stroke-linecap="round"
145
- stroke-linejoin="round"
146
- >
147
- <path
148
- d="M20.24 12.24a6 6 0 00-8.49-8.49L5 10.5V19h8.5zM16 8L2 22M17.5 15H9"
149
- />
150
- </svg>
151
- <h3>Tiny Footprint</h3>
152
- </header>
153
- <p>
154
- At
155
- <a href="https://bundlephobia.com/package/polystore" target="_blank"
156
- >just <strong>3kb</strong></a
157
- >
158
- (min+gzip), the impact on your app loading time is minimal.
159
- </p>
160
- </div>
161
- </div>
162
-
163
- <div>
164
- <div class="feature">
165
- <header>
166
- <svg
167
- xmlns="http://www.w3.org/2000/svg"
168
- width="24"
169
- height="24"
170
- viewBox="0 0 24 24"
171
- fill="none"
172
- stroke="currentColor"
173
- stroke-width="2"
174
- stroke-linecap="round"
175
- stroke-linejoin="round"
176
- class="feather feather-clock"
177
- >
178
- <circle cx="12" cy="12" r="10"></circle>
179
- <polyline points="12 6 12 12 16 14"></polyline>
180
- </svg>
181
- <h3>Intuitive expirations</h3>
182
- </header>
183
- <p>Write the expiration as <code>100s</code>, <code>1week</code>, etc.</p>
184
- and forget time-related bugs.
185
- </div>
186
- </div>
187
- </section>
188
-
189
- <div class="separator">
190
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 20">
191
- <circle cx="10" cy="10" r="3" />
192
- <line x1="20" y1="10" x2="260" y2="10" />
193
- <circle cx="270" cy="10" r="3" />
194
- <circle class="empty" cx="283" cy="10" r="3" />
195
- <circle class="empty" cx="300" cy="10" r="5" />
196
- <circle class="empty" cx="317" cy="10" r="3" />
197
- <circle cx="330" cy="10" r="3" />
198
- <line x1="340" y1="10" x2="580" y2="10" />
199
- <circle cx="590" cy="10" r="3" />
200
- </svg>
201
- </div>
202
-
203
- <section class="hero flex one two-900 center">
204
- <div>
205
- <div class="content">
206
- <h2>🧩 Support for many clients</h2>
207
- <p>
208
- We support 10+ clients with documentation for each of them. Adding your
209
- own client is also easy, you only need to define 3 methods:
210
- <code>.get()</code>, <code>.set()</code> and <code>.entries()</code>.
211
- </p>
212
- <p>
213
- <a class="pseudo button" href="/documentation#clients">
214
- Clients Docs
215
- </a>
216
- </p>
217
- </div>
218
- </div>
219
- <div>
220
- <pre><code class="language-js">import kv from &quot;polystore&quot;;
221
-
222
- const store1 = kv(new Map());
223
- const store2 = kv(localStorage);
224
- const store3 = kv(redisClient);
225
- const store4 = kv(&quot;cookie&quot;);
226
- const store5 = kv(&quot;file:///users/me/kv.json&quot;);
227
- const store6 = kv(yourOwnStore);</code></pre>
228
- </div>
229
- </section>
230
-
231
- <br />
232
-
233
- <section class="hero flex one two-900 center">
234
- <div>
235
- <div class="content">
236
- <h2>🏖️ Clean and intuitive API</h2>
237
- <p>
238
- A set of high-performance item operations with <code>.add()</code>,
239
- <code>.set()</code>, <code>.get()</code>, <code>.has()</code> or
240
- <code>.del()</code>. We also provide group operations to manage your
241
- data easily.
242
- </p>
243
- <p>
244
- <a class="pseudo button" href="/documentation#api">API Docs</a>
245
- </p>
246
- </div>
247
- </div>
248
- <div>
249
- <pre><code class="language-js">import kv from "polystore";
250
- const store = kv(new Map());
251
-
252
- const key1 = await store.add(&quot;value1&quot;);
253
- const key2 = await store.set(&quot;key2&quot;, &quot;value2&quot;);
254
- const val1 = await store.get(&quot;key1&quot;);
255
- const isthere = await store.has(&quot;key1&quot;);
256
- await store.del(key1);</code></pre>
257
- </div>
258
- </section>
259
-
260
- <section class="hero flex one two-900 center">
261
- <div>
262
- <div class="content">
263
- <h2>🛗 Create substores</h2>
264
- <p>
265
- Create a new substore with <code>.prefix()</code>, then you can ignore
266
- anything related to the prefix and treat it as if it was a brand new
267
- store.
268
- </p>
269
- <p>
270
- <a class="pseudo button" href="/documentation#substores"
271
- >Substore Docs
272
- </a>
273
- </p>
274
- </div>
275
- </div>
276
- <div>
277
- <pre><code class="language-js">const session = store.prefix("session:");
278
- session.set("key1", "value1");
279
-
280
- console.log(await session.all());
281
- // { "key1": "value1" }
282
-
283
- console.log(await store.all());
284
- // { "session:key1": "value1" }</code></pre>
285
- </div>
286
- </section>
287
-
288
- <br />
289
-
290
- <section class="hero flex one two-900 center">
291
- <div>
292
- <div class="content">
293
- <h2>⏰ Easy expiration time</h2>
294
- <p>
295
- Simply write <code>{ expires: "1day" }</code> with ANY client and forget
296
- about calculating TTL, Unix time, seconds vs milliseconds bugs, etc.
297
- </p>
298
- <p>
299
- <a class="pseudo button" href="/documentation#expiration-explained">
300
- Documentation
301
- </a>
302
- </p>
303
- </div>
304
- </div>
305
- <div>
306
- <pre><code class="language-js">await store.get("key"); // null
307
-
308
- await store.set("key", "value", { expires: "1s" });
309
- await store.get("key"); // "value"
310
-
311
- await sleep(2000);
312
-
313
- await store.get("key"); // null</code></pre>
314
- </div>
315
- </section>
316
-
317
- <br />
318
-
319
- <section class="hero flex one two-900 center">
320
- <div>
321
- <div class="content">
322
- <h2>✨ Magic Autocomplete</h2>
323
- <p>
324
- Added jsdocs so the expected parameters and return value will be clearly
325
- defined in your IDE/Code Editor.
326
- </p>
327
- <p>
328
- We added the <em>description</em>, a representative <em>example</em> and
329
- even a <em>link</em> for more information for
330
- <strong>every one</strong> of the methods available! We want you to have
331
- the best development experience possible with Polystore!
332
- </p>
333
- </div>
334
- </div>
335
- <div>
336
- <div class="splash">
337
- <img
338
- width="535"
339
- src="https://raw.githubusercontent.com/franciscop/polystore/master/assets/autocomplete.webp"
340
- />
341
- </div>
342
- </div>
343
- </section>
344
-
345
- <div class="separator">
346
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 20">
347
- <circle cx="10" cy="10" r="3" />
348
- <line x1="20" y1="10" x2="260" y2="10" />
349
- <circle cx="270" cy="10" r="3" />
350
- <circle class="empty" cx="283" cy="10" r="3" />
351
- <circle class="empty" cx="300" cy="10" r="5" />
352
- <circle class="empty" cx="317" cy="10" r="3" />
353
- <circle cx="330" cy="10" r="3" />
354
- <line x1="340" y1="10" x2="580" y2="10" />
355
- <circle cx="590" cy="10" r="3" />
356
- </svg>
357
- </div>
358
-
359
- <div style="text-align: center">
360
- <p>
361
- Created by <a href="https://francisco.io/" target="_blank">Francisco</a> and
362
- <a
363
- href="https://github.com/franciscop/polystore/graphs/contributors"
364
- target="_blank"
365
- >other contributors</a
366
- >.
367
- </p>
368
- <p>
369
- Need help?
370
- <a
371
- href="https://superpeer.com/francisco/-/javascript-and-react-help"
372
- target="_blank"
373
- >Book a call</a
374
- >.
375
- </p>
376
- </div>
package/assets/splash.png DELETED
Binary file
@@ -1,11 +0,0 @@
1
- {
2
- "title": "🏬 Polystore - A universal library for standardizing any KV-store",
3
- "home": "assets/home.html",
4
- "homepage": "https://polystore.dev/",
5
- "menu": {
6
- "Documentation": "/documentation",
7
- "Issues": "https://github.com/franciscop/polystore/issues",
8
- "Get help": "https://superpeer.com/francisco/-/javascript-and-react-help",
9
- "Github": "https://github.com/franciscop/polystore"
10
- }
11
- }