polystore 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/index.d.ts +161 -0
  2. package/index.js +143 -59
  3. package/package.json +14 -6
  4. package/readme.md +212 -73
package/index.d.ts CHANGED
@@ -52,28 +52,189 @@ declare class Store<TDefault extends Serializable = Serializable> {
52
52
  promise: Promise<Client> | null;
53
53
  client: Client;
54
54
  constructor(clientPromise?: any);
55
+ /**
56
+ * Save the data on an autogenerated key, can add expiration as well:
57
+ *
58
+ * ```js
59
+ * const key1 = await store.add("value1");
60
+ * const key2 = await store.add({ hello: "world" });
61
+ * const key3 = await store.add("value3", { expires: "1h" });
62
+ * ```
63
+ *
64
+ * **[→ Full .add() Docs](https://polystore.dev/documentation#add)**
65
+ */
55
66
  add(value: TDefault, options?: Options): Promise<string>;
56
67
  add<T extends TDefault>(value: T, options?: Options): Promise<string>;
68
+ /**
69
+ * Save the data on the given key, can add expiration as well:
70
+ *
71
+ * ```js
72
+ * const key = await store.set("key1", "value1");
73
+ * await store.set("key2", { hello: "world" });
74
+ * await store.set("key3", "value3", { expires: "1h" });
75
+ * ```
76
+ *
77
+ * **[→ Full .set() Docs](https://polystore.dev/documentation#set)**
78
+ */
57
79
  set(key: string, value: TDefault, options?: Options): Promise<string>;
58
80
  set<T extends TDefault>(key: string, value: T, options?: Options): Promise<string>;
81
+ /**
82
+ * Read a single value from the KV store:
83
+ *
84
+ * ```js
85
+ * const value1 = await store.get("key1");
86
+ * // null (doesn't exist or has expired)
87
+ * const value2 = await store.get("key2");
88
+ * // "value2"
89
+ * const value3 = await store.get("key3");
90
+ * // { hello: "world" }
91
+ * ```
92
+ *
93
+ * **[→ Full .get() Docs](https://polystore.dev/documentation#get)**
94
+ */
59
95
  get(key: string): Promise<TDefault | null>;
60
96
  get<T extends TDefault>(key: string): Promise<T | null>;
97
+ /**
98
+ * Check whether a key exists or not:
99
+ *
100
+ * ```js
101
+ * if (await store.has("key1")) { ... }
102
+ * ```
103
+ *
104
+ * If you are going to use the value, it's better to just read it:
105
+ *
106
+ * ```js
107
+ * const val = await store.get("key1");
108
+ * if (val) { ... }
109
+ * ```
110
+ *
111
+ * **[→ Full .has() Docs](https://polystore.dev/documentation#has)**
112
+ */
61
113
  has(key: string): Promise<boolean>;
114
+ /**
115
+ * Remove a single key and its value from the store:
116
+ *
117
+ * ```js
118
+ * const key = await store.del("key1");
119
+ * ```
120
+ *
121
+ * **[→ Full .del() Docs](https://polystore.dev/documentation#del)**
122
+ */
62
123
  del(key: string): Promise<string>;
124
+ /**
125
+ * An iterator that goes through all of the key:value pairs in the client
126
+ *
127
+ * ```js
128
+ * for await (const [key, value] of store) {
129
+ * console.log(key, value);
130
+ * }
131
+ * ```
132
+ *
133
+ * **[→ Full Iterator Docs](https://polystore.dev/documentation#iterator)**
134
+ */
63
135
  [Symbol.asyncIterator](): AsyncGenerator<[string, TDefault], void, unknown>;
64
136
  [Symbol.asyncIterator]<T extends TDefault>(): AsyncGenerator<[
65
137
  string,
66
138
  T
67
139
  ], void, unknown>;
140
+ /**
141
+ * Return an array of the entries, in the [key, value] format:
142
+ *
143
+ * ```js
144
+ * const entries = await store.entries();
145
+ * // [["key1", "value1"], ["key2", { hello: "world" }], ...]
146
+ *
147
+ * // To limit it to a given prefix, use `.prefix()`:
148
+ * const sessions = await store.prefix("session:").entries();
149
+ * ```
150
+ *
151
+ * **[→ Full .entries() Docs](https://polystore.dev/documentation#entries)**
152
+ */
68
153
  entries(): Promise<[string, TDefault][]>;
69
154
  entries<T extends TDefault>(): Promise<[string, T][]>;
155
+ /**
156
+ * Return an array of the keys in the store:
157
+ *
158
+ * ```js
159
+ * const keys = await store.keys();
160
+ * // ["key1", "key2", ...]
161
+ *
162
+ * // To limit it to a given prefix, use `.prefix()`:
163
+ * const sessions = await store.prefix("session:").keys();
164
+ * ```
165
+ *
166
+ * **[→ Full .keys() Docs](https://polystore.dev/documentation#keys)**
167
+ */
70
168
  keys(): Promise<string[]>;
169
+ /**
170
+ * Return an array of the values in the store:
171
+ *
172
+ * ```js
173
+ * const values = await store.values();
174
+ * // ["value1", { hello: "world" }, ...]
175
+ *
176
+ * // To limit it to a given prefix, use `.prefix()`:
177
+ * const sessions = await store.prefix("session:").values();
178
+ * ```
179
+ *
180
+ * **[→ Full .values() Docs](https://polystore.dev/documentation#values)**
181
+ */
71
182
  values(): Promise<TDefault[]>;
72
183
  values<T extends TDefault>(): Promise<T[]>;
184
+ /**
185
+ * Return an object with the keys:values in the store:
186
+ *
187
+ * ```js
188
+ * const obj = await store.all();
189
+ * // { key1: "value1", key2: { hello: "world" }, ... }
190
+ *
191
+ * // To limit it to a given prefix, use `.prefix()`:
192
+ * const sessions = await store.prefix("session:").all();
193
+ * ```
194
+ *
195
+ * **[→ Full .all() Docs](https://polystore.dev/documentation#all)**
196
+ */
73
197
  all(): Promise<Record<string, TDefault>>;
74
198
  all<T extends TDefault>(): Promise<Record<string, T>>;
199
+ /**
200
+ * Delete all of the records of the store:
201
+ *
202
+ * ```js
203
+ * await store.clear();
204
+ * ```
205
+ *
206
+ * It's useful for cache invalidation, clearing the data, and testing.
207
+ *
208
+ * **[→ Full .clear() Docs](https://polystore.dev/documentation#clear)**
209
+ */
75
210
  clear(): Promise<void>;
211
+ /**
212
+ * Create a substore where all the keys are stored with
213
+ * the given prefix:
214
+ *
215
+ * ```js
216
+ * const session = store.prefix("session:");
217
+ * await session.set("key1", "value1");
218
+ * console.log(await session.entries()); // session.
219
+ * // [["key1", "value1"]]
220
+ * console.log(await store.entries()); // store.
221
+ * // [["session:key1", "value1"]]
222
+ * ```
223
+ *
224
+ * **[→ Full .prefix() Docs](https://polystore.dev/documentation#prefix)**
225
+ */
76
226
  prefix(prefix?: string): Store<TDefault>;
227
+ /**
228
+ * Stop the connection to the store, if any:
229
+ *
230
+ * ```js
231
+ * await session.set("key1", "value1");
232
+ * await store.close();
233
+ * await session.set("key2", "value2"); // error
234
+ * ```
235
+ *
236
+ * **[→ Full .close() Docs](https://polystore.dev/documentation#close)**
237
+ */
77
238
  close(): Promise<void>;
78
239
  }
79
240
  declare function createStore(): Store<Serializable>;
package/index.js CHANGED
@@ -29,13 +29,14 @@ var Api = class extends Client {
29
29
  const exp = typeof expires === "number" ? `?expires=${expires}` : "";
30
30
  await this.#api(key, exp, "PUT", this.encode(value));
31
31
  };
32
- del = async (key) => {
33
- await this.#api(key, "", "DELETE");
34
- };
32
+ del = (key) => this.#api(key, "", "DELETE");
35
33
  async *iterate(prefix = "") {
36
- const data = await this.#api("", `?prefix=${encodeURIComponent(prefix)}`);
34
+ const data = await this.#api(
35
+ "",
36
+ `?prefix=${encodeURIComponent(prefix)}`
37
+ );
37
38
  for (let [key, value] of Object.entries(data || {})) {
38
- if (value !== null && value !== void 0) {
39
+ if (value !== null) {
39
40
  yield [prefix + key, value];
40
41
  }
41
42
  }
@@ -48,13 +49,15 @@ var Cloudflare = class extends Client {
48
49
  EXPIRES = true;
49
50
  // Check whether the given store is a FILE-type
50
51
  static test = (client) => client?.constructor?.name === "KvNamespace" || client?.constructor?.name === "EdgeKVNamespace";
51
- get = async (key) => this.decode(await this.client.get(key));
52
- set = (key, data, opts) => {
52
+ get = async (key) => {
53
+ return this.decode(await this.client.get(key));
54
+ };
55
+ set = async (key, data, opts) => {
53
56
  const expirationTtl = opts.expires ? Math.round(opts.expires) : void 0;
54
57
  if (expirationTtl && expirationTtl < 60) {
55
58
  throw new Error("Cloudflare's min expiration is '60s'");
56
59
  }
57
- return this.client.put(key, this.encode(data), { expirationTtl });
60
+ await this.client.put(key, this.encode(data), { expirationTtl });
58
61
  };
59
62
  del = (key) => this.client.delete(key);
60
63
  // Since we have pagination, we don't want to get all of the
@@ -84,7 +87,7 @@ var Cloudflare = class extends Client {
84
87
  entries = async (prefix = "") => {
85
88
  const keys = await this.keys(prefix);
86
89
  const values = await Promise.all(keys.map((k) => this.get(k)));
87
- return keys.map((k, i) => [k, values[i]]);
90
+ return keys.map((k, i) => [k, values[i]]).filter((p) => p[1] !== null);
88
91
  };
89
92
  };
90
93
 
@@ -140,8 +143,13 @@ var Etcd = class extends Client {
140
143
  EXPIRES = false;
141
144
  // Check if this is the right class for the given client
142
145
  static test = (client) => client?.constructor?.name === "Etcd3";
143
- get = (key) => this.client.get(key).json();
144
- set = (key, value) => this.client.put(key).value(this.encode(value));
146
+ get = async (key) => {
147
+ const data = await this.client.get(key).json();
148
+ return data;
149
+ };
150
+ set = async (key, value) => {
151
+ await this.client.put(key).value(this.encode(value));
152
+ };
145
153
  del = (key) => this.client.delete().key(key).exec();
146
154
  async *iterate(prefix = "") {
147
155
  const keys = await this.client.getAll().prefix(prefix).keys();
@@ -149,7 +157,9 @@ var Etcd = class extends Client {
149
157
  yield [key, await this.get(key)];
150
158
  }
151
159
  }
152
- keys = (prefix = "") => this.client.getAll().prefix(prefix).keys();
160
+ keys = (prefix = "") => {
161
+ return this.client.getAll().prefix(prefix).keys();
162
+ };
153
163
  entries = async (prefix = "") => {
154
164
  const keys = await this.keys(prefix);
155
165
  const values = await Promise.all(keys.map((k) => this.get(k)));
@@ -274,13 +284,16 @@ var Folder = class extends Client {
274
284
  });
275
285
  })();
276
286
  file = (key) => this.folder + key + ".json";
277
- get = (key) => {
278
- return this.fsp.readFile(this.file(key), "utf8").then(this.decode, noFileOk);
287
+ get = async (key) => {
288
+ const file = await this.fsp.readFile(this.file(key), "utf8").catch(noFileOk);
289
+ return this.decode(file);
279
290
  };
280
- set = (key, value) => {
281
- return this.fsp.writeFile(this.file(key), this.encode(value), "utf8");
291
+ set = async (key, value) => {
292
+ await this.fsp.writeFile(this.file(key), this.encode(value), "utf8");
293
+ };
294
+ del = async (key) => {
295
+ await this.fsp.unlink(this.file(key)).catch(noFileOk);
282
296
  };
283
- del = (key) => this.fsp.unlink(this.file(key)).catch(noFileOk);
284
297
  async *iterate(prefix = "") {
285
298
  const all = await this.fsp.readdir(this.folder);
286
299
  const keys = all.filter((f) => f.startsWith(prefix) && f.endsWith(".json"));
@@ -288,7 +301,7 @@ var Folder = class extends Client {
288
301
  const key = name.slice(0, -".json".length);
289
302
  try {
290
303
  const data = await this.get(key);
291
- yield [key, data];
304
+ if (data !== null && data !== void 0) yield [key, data];
292
305
  } catch {
293
306
  continue;
294
307
  }
@@ -428,25 +441,32 @@ var SQLite = class extends Client {
428
441
  // This one is doing manual time management internally even though
429
442
  // sqlite does not natively support expirations. This is because it does
430
443
  // support creating a `expires_at:Date` column that makes managing
431
- // expirations much easier, so it's really "somewhere in between"
444
+ // expirations much easier, so it's really "somewhere in between"
432
445
  EXPIRES = true;
433
- static test = (client) => typeof client?.prepare === "function";
434
- constructor(c) {
435
- if (typeof c?.prepare("SELECT 1").get === "function") {
436
- super({
437
- run: (sql, ...args) => c.prepare(sql).run(...args),
438
- get: (sql, ...args) => c.prepare(sql).get(...args),
439
- all: (sql, ...args) => c.prepare(sql).all(...args)
440
- });
441
- return;
442
- }
443
- super(c);
444
- }
445
- get = (id) => {
446
- const row = this.client.get(
447
- `SELECT value, expires_at FROM kv WHERE id = ?`,
448
- id
446
+ // The table name to use
447
+ table = "kv";
448
+ // Make sure the folder already exists, so attempt to create it
449
+ // It fails if it already exists, hence the catch case
450
+ promise = (async () => {
451
+ if (!/^[a-zA-Z_]+$/.test(this.table)) {
452
+ throw new Error(`Invalid table name ${this.table}`);
453
+ }
454
+ this.client.exec(`
455
+ CREATE TABLE IF NOT EXISTS ${this.table} (
456
+ id TEXT PRIMARY KEY,
457
+ value TEXT NOT NULL,
458
+ expires_at INTEGER
459
+ )
460
+ `);
461
+ this.client.exec(
462
+ `CREATE INDEX IF NOT EXISTS idx_${this.table}_expires_at ON ${this.table} (expires_at)`
449
463
  );
464
+ })();
465
+ static test = (client) => {
466
+ return typeof client?.prepare === "function" && typeof client?.exec === "function";
467
+ };
468
+ get = (id) => {
469
+ const row = this.client.prepare(`SELECT value, expires_at FROM kv WHERE id = ?`).get(id);
450
470
  if (!row) return null;
451
471
  if (row.expires_at && row.expires_at < Date.now()) {
452
472
  this.del(id);
@@ -457,21 +477,15 @@ var SQLite = class extends Client {
457
477
  set = (id, data, { expires } = {}) => {
458
478
  const value = this.encode(data);
459
479
  const expires_at = expires ? Date.now() + expires * 1e3 : null;
460
- this.client.run(
461
- `INSERT INTO kv (id, value, expires_at)
462
- VALUES (?, ?, ?)
463
- ON CONFLICT(id)
464
- DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at`,
465
- id,
466
- value,
467
- expires_at
468
- );
480
+ this.client.prepare(
481
+ `INSERT INTO kv (id, value, expires_at) VALUES (?, ?, ?) ON CONFLICT(id) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at`
482
+ ).run(id, value, expires_at);
469
483
  };
470
- del = (id) => {
471
- this.client.run(`DELETE FROM kv WHERE id = ?`, id);
484
+ del = async (id) => {
485
+ await this.client.prepare(`DELETE FROM kv WHERE id = ?`).run(id);
472
486
  };
473
487
  has = (id) => {
474
- const row = this.client.get(`SELECT expires_at FROM kv WHERE id = ?`, id);
488
+ const row = this.client.prepare(`SELECT expires_at FROM kv WHERE id = ?`).get(id);
475
489
  if (!row) return false;
476
490
  if (row.expires_at && row.expires_at < Date.now()) {
477
491
  this.del(id);
@@ -481,32 +495,27 @@ var SQLite = class extends Client {
481
495
  };
482
496
  *iterate(prefix = "") {
483
497
  this.#clearExpired();
484
- const sql = `
485
- SELECT id, value FROM kv
486
- WHERE (expires_at IS NULL OR expires_at > ?)
487
- ${prefix ? "AND id LIKE ?" : ""}
498
+ const sql = `SELECT id, value FROM kv WHERE (expires_at IS NULL OR expires_at > ?) ${prefix ? "AND id LIKE ?" : ""}
488
499
  `;
489
500
  const params = prefix ? [Date.now(), `${prefix}%`] : [Date.now()];
490
- for (const row of this.client.all(sql, ...params)) {
501
+ for (const row of this.client.prepare(sql).all(...params)) {
491
502
  yield [row.id, this.decode(row.value)];
492
503
  }
493
504
  }
494
505
  keys = (prefix = "") => {
495
506
  this.#clearExpired();
496
- const sql = `
497
- SELECT id FROM kv
498
- WHERE (expires_at IS NULL OR expires_at > ?)
499
- ${prefix ? "AND id LIKE ?" : ""}
507
+ const sql = `SELECT id FROM kv WHERE (expires_at IS NULL OR expires_at > ?)
508
+ ${prefix ? "AND id LIKE ?" : ""}
500
509
  `;
501
510
  const params = prefix ? [Date.now(), `${prefix}%`] : [Date.now()];
502
- const rows = this.client.all(sql, ...params);
511
+ const rows = this.client.prepare(sql).all(...params);
503
512
  return rows.map((r) => r.id);
504
513
  };
505
514
  #clearExpired = () => {
506
- this.client.run(`DELETE FROM kv WHERE expires_at < ?`, Date.now());
515
+ this.client.prepare(`DELETE FROM kv WHERE expires_at < ?`).run(Date.now());
507
516
  };
508
517
  clearAll = () => {
509
- this.client.run(`DELETE FROM kv`);
518
+ this.client.exec(`DELETE FROM kv`);
510
519
  };
511
520
  close = () => {
512
521
  this.client.close?.();
@@ -686,6 +695,22 @@ var Store = class _Store {
686
695
  return data.value;
687
696
  }
688
697
  }
698
+ /**
699
+ * Check whether a key exists or not:
700
+ *
701
+ * ```js
702
+ * if (await store.has("key1")) { ... }
703
+ * ```
704
+ *
705
+ * If you are going to use the value, it's better to just read it:
706
+ *
707
+ * ```js
708
+ * const val = await store.get("key1");
709
+ * if (val) { ... }
710
+ * ```
711
+ *
712
+ * **[→ Full .has() Docs](https://polystore.dev/documentation#has)**
713
+ */
689
714
  async has(key) {
690
715
  await this.promise;
691
716
  const id = this.PREFIX + key;
@@ -694,6 +719,15 @@ var Store = class _Store {
694
719
  }
695
720
  return await this.get(key) !== null;
696
721
  }
722
+ /**
723
+ * Remove a single key and its value from the store:
724
+ *
725
+ * ```js
726
+ * const key = await store.del("key1");
727
+ * ```
728
+ *
729
+ * **[→ Full .del() Docs](https://polystore.dev/documentation#del)**
730
+ */
697
731
  async del(key) {
698
732
  await this.promise;
699
733
  const id = this.PREFIX + key;
@@ -752,6 +786,19 @@ var Store = class _Store {
752
786
  return list;
753
787
  }
754
788
  }
789
+ /**
790
+ * Return an array of the keys in the store:
791
+ *
792
+ * ```js
793
+ * const keys = await store.keys();
794
+ * // ["key1", "key2", ...]
795
+ *
796
+ * // To limit it to a given prefix, use `.prefix()`:
797
+ * const sessions = await store.prefix("session:").keys();
798
+ * ```
799
+ *
800
+ * **[→ Full .keys() Docs](https://polystore.dev/documentation#keys)**
801
+ */
755
802
  async keys() {
756
803
  await this.promise;
757
804
  if (this.client.keys) {
@@ -776,6 +823,17 @@ var Store = class _Store {
776
823
  const entries = await this.entries();
777
824
  return Object.fromEntries(entries);
778
825
  }
826
+ /**
827
+ * Delete all of the records of the store:
828
+ *
829
+ * ```js
830
+ * await store.clear();
831
+ * ```
832
+ *
833
+ * It's useful for cache invalidation, clearing the data, and testing.
834
+ *
835
+ * **[→ Full .clear() Docs](https://polystore.dev/documentation#clear)**
836
+ */
779
837
  async clear() {
780
838
  await this.promise;
781
839
  if (!this.PREFIX && this.client.clearAll) {
@@ -787,6 +845,21 @@ var Store = class _Store {
787
845
  const keys = await this.keys();
788
846
  await Promise.all(keys.map((key) => this.del(key)));
789
847
  }
848
+ /**
849
+ * Create a substore where all the keys are stored with
850
+ * the given prefix:
851
+ *
852
+ * ```js
853
+ * const session = store.prefix("session:");
854
+ * await session.set("key1", "value1");
855
+ * console.log(await session.entries()); // session.
856
+ * // [["key1", "value1"]]
857
+ * console.log(await store.entries()); // store.
858
+ * // [["session:key1", "value1"]]
859
+ * ```
860
+ *
861
+ * **[→ Full .prefix() Docs](https://polystore.dev/documentation#prefix)**
862
+ */
790
863
  prefix(prefix = "") {
791
864
  const store = new _Store(
792
865
  Promise.resolve(this.promise).then(() => this.client)
@@ -794,6 +867,17 @@ var Store = class _Store {
794
867
  store.PREFIX = this.PREFIX + prefix;
795
868
  return store;
796
869
  }
870
+ /**
871
+ * Stop the connection to the store, if any:
872
+ *
873
+ * ```js
874
+ * await session.set("key1", "value1");
875
+ * await store.close();
876
+ * await session.set("key2", "value2"); // error
877
+ * ```
878
+ *
879
+ * **[→ Full .close() Docs](https://polystore.dev/documentation#close)**
880
+ */
797
881
  async close() {
798
882
  await this.promise;
799
883
  if (this.client.close) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polystore",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "A small compatibility layer for many popular KV stores like localStorage, Redis, FileSystem, etc.",
5
5
  "homepage": "https://polystore.dev/",
6
6
  "repository": "https://github.com/franciscop/polystore.git",
@@ -16,11 +16,13 @@
16
16
  "index.d.ts"
17
17
  ],
18
18
  "scripts": {
19
- "analyze": "npm run build && esbuild src/index.ts --bundle --packages=external --format=esm --minify --outfile=index.min.js && gzip-size index.min.js && rm index.min.js",
19
+ "analyze": "npm run build && esbuild src/index.ts --bundle --packages=external --format=esm --minify --outfile=index.min.js && echo 'Final size:' && gzip-size index.min.js && rm index.min.js",
20
20
  "build": "bunx tsup src/index.ts --format esm --dts --out-dir . --target node24",
21
21
  "lint": "npx tsc --noEmit",
22
22
  "start": "bun test --watch",
23
- "test": "bun test",
23
+ "test": "npm run test:bun && npm run test:jest",
24
+ "test:bun": "bun test ./test/index.test.ts",
25
+ "test:jest": "jest ./test/index.test.ts --detectOpenHandles --forceExit",
24
26
  "run:db": "etcd",
25
27
  "run:server": "bun ./src/server.ts"
26
28
  },
@@ -35,21 +37,27 @@
35
37
  "license": "MIT",
36
38
  "devDependencies": {
37
39
  "@deno/kv": "^0.8.1",
40
+ "@types/better-sqlite3": "^7.6.13",
38
41
  "@types/bun": "^1.3.3",
42
+ "@types/jest": "^30.0.0",
39
43
  "@types/jsdom": "^27.0.0",
40
- "better-sqlite3": "^12.5.0",
44
+ "better-sqlite3": "^12.6.0",
41
45
  "check-dts": "^0.8.0",
42
- "cross-fetch": "^4.0.0",
46
+ "cross-fetch": "^4.1.0",
43
47
  "dotenv": "^16.3.1",
44
48
  "edge-mock": "^0.0.15",
45
49
  "esbuild": "^0.27.0",
46
50
  "etcd3": "^1.1.2",
47
51
  "gzip-size-cli": "^5.1.0",
52
+ "jest": "^30.2.0",
48
53
  "jsdom": "^27.2.0",
49
54
  "level": "^8.0.1",
50
55
  "localforage": "^1.10.0",
51
56
  "redis": "^4.6.10",
52
- "tsup": "^8.5.1"
57
+ "ts-jest": "^29.4.6",
58
+ "ts-node": "^10.9.2",
59
+ "tsup": "^8.5.1",
60
+ "typescript": "^5.9.3"
53
61
  },
54
62
  "documentation": {
55
63
  "title": "🏬 Polystore - A universal library for standardizing any KV-store",
package/readme.md CHANGED
@@ -8,45 +8,44 @@ const store1 = kv(new Map()); // in-memory
8
8
  const store2 = kv(localStorage); // Persist in the browser
9
9
  const store3 = kv(redisClient); // Use a Redis client for backend persistence
10
10
  const store4 = kv(yourOwnStore); // Create a store based on your code
11
- // Many more here
11
+ // Many more clients available
12
12
  ```
13
13
 
14
14
  These are all the methods of the [API](#api) (they are all `async`):
15
15
 
16
16
  - [`.get(key)`](#get): read a single value, or `null` if it doesn't exist or is expired.
17
17
  - [`.set(key, value, options?)`](#set): save a single value that is serializable.
18
- - [`.add(value, options?)`](#add): same as `.set()`, but auto-generates the key.
19
- - [`.has(key)`](#has): check whether a key exists or not.
20
- - [`.del(key)`](#del): delete a single value from the store.
21
- - [`.keys()`](#keys): get a list of all the available strings in the store.
22
- - [`.values()`](#values): get a list of all the values in the store.
23
- - [`.entries()`](#entries): get a list of all the key-value pairs.
24
- - [`.all()`](#all): get an object with the key:values mapped.
18
+ - [`.add(value, options?)`](#add): save a single value with an auto-generated key.
19
+ - [`.has(key)`](#has): check whether a key exists and is not expired.
20
+ - [`.del(key)`](#del): delete a single key/value from the store.
21
+ - [Iterator](#iterator): go through all of the key/values one by one.
22
+ - [`.keys()`](#keys): get a list of all the available keys in the store.
23
+ - [`.values()`](#values): get a list of all the available values in the store.
24
+ - [`.entries()`](#entries): get a list of all the available key-value pairs.
25
+ - [`.all()`](#all): get an object of all the key:values mapped.
25
26
  - [`.clear()`](#clear): delete ALL of the data in the store, effectively resetting it.
26
27
  - [`.close()`](#close): (only _some_ stores) ends the connection to the store.
27
28
  - [`.prefix(prefix)`](#prefix): create a sub-store that manages the keys with that prefix.
28
29
 
29
- > This library has very high performance with the item methods (GET/SET/ADD/HAS/DEL). For other methods or to learn more, see [the performance considerations](#performance) and read the docs on your specific client.
30
-
31
30
  Available clients for the KV store:
32
31
 
33
32
  - [**Memory** `new Map()`](#memory) (fe+be): an in-memory API to keep your KV store.
34
33
  - [**Local Storage** `localStorage`](#local-storage) (fe): persist the data in the browser's localStorage.
35
34
  - [**Session Storage** `sessionStorage`](#session-storage) (fe): persist the data in the browser's sessionStorage.
36
- - [**Cookies** `"cookie"`](#cookies) (fe): persist the data using cookies
37
- - [**LocalForage** `localForage`](#local-forage) (fe): persist the data on IndexedDB
38
- - [**Fetch API** `"https://..."`](#fetch-api) (fe+be): call an API to save/retrieve the data
39
- - [**File** `"file:///[...].json"`](#file) (be): store the data in a single JSON file in your FS
40
- - [**Folder** `"file:///[...]/"`](#folder) (be): store each key in a folder as json files
41
- - [**Redis Client** `redisClient`](#redis-client) (be): use the Redis instance that you connect to
42
- - [**Cloudflare KV** `env.KV_NAMESPACE`](#cloudflare-kv) (be): use Cloudflare's KV store
43
- - [**Level** `new Level('example', { valueEncoding: 'json' })`](#level) (fe+be): support the whole Level ecosystem
35
+ - [**Cookies** `"cookie"`](#cookies) (fe): persist the data using cookies.
36
+ - [**LocalForage** `localForage`](#local-forage) (fe): persist the data on IndexedDB.
37
+ - [**Redis** `redisClient`](#redis) (be): use the Redis instance that you connect to.
38
+ - [**SQLite** `sqlite`](#sqlite) (be): use a SQLite instance with a table called `kv`.
39
+ - [**Fetch API** `"https://..."`](#fetch-api) (fe+be): call an API to save/retrieve the data.
40
+ - [**File** `"file:///[...].json"`](#file) (be): store the data in a single JSON file in your FS.
41
+ - [**Folder** `"file:///[...]/"`](#folder) (be): store each key in a folder as json files.
42
+ - [**Cloudflare KV** `env.KV_NAMESPACE`](#cloudflare-kv) (be): use Cloudflare's KV store.
43
+ - [**Postgres** `pool`](#postgres) (be): use PostgreSQL with the pg library.
44
+ - [**Level** `new Level('example', { valueEncoding: 'json' })`](#level) (fe+be): support the whole Level ecosystem.
44
45
  - [**Etcd** `new Etcd3()`](#etcd) (be): the Microsoft's high performance KV store.
45
- - [**Postgres** `pool`](#postgres) (be): use PostgreSQL with the pg library
46
- - [**Prisma** `prisma.store`](#prisma) (be): use Prisma ORM as a key-value store
47
46
  - [**_Custom_** `{}`](#creating-a-store) (fe+be): create your own store with just 3 methods!
48
47
 
49
- I made this library to be used as a "building block" of other libraries, so that _your library_ can accept many cache stores effortlessly! It's universal (Node.js, Bun and the Browser) and tiny (~3KB). For example, let's say you create an API library, then you can accept the stores from your client:
48
+ I made this library to be used as a "building block" of other libraries, so that _your library_ can accept many cache stores effortlessly! It's universal (Node.js, Bun and the Browser), idempotent (`kv(kv(A))` ~= `kv(A)`) and tiny (~4KB). For example, let's say you create an API library, then you can accept the stores from your client:
50
49
 
51
50
  ```js
52
51
  import MyApi from "my-api";
@@ -79,7 +78,7 @@ const REDIS = process.env.REDIS_URL;
79
78
  const store = kv(createClient({ url: REDIS }).connect());
80
79
  ```
81
80
 
82
- Now your store is ready to use! Add, set, get, del different keys. [See full API](#api).
81
+ This follows the recommended naming, the default exported `kv()` for the base function, then `store` for the store instance. Now your store is ready to use! Add, set, get, del different keys. [See full API](#api).
83
82
 
84
83
  ```js
85
84
  const key = await store.add("Hello");
@@ -92,7 +91,7 @@ await store.del(key);
92
91
 
93
92
  ## API
94
93
 
95
- See how to initialize each store [in the Clients list documentation](#clients). But basically for every store, it's like this:
94
+ The base `kv()` initialization is shared across clients ([see full clients list](#clients)); single argument that receives the client or a string representing the client:
96
95
 
97
96
  ```js
98
97
  import kv from "polystore";
@@ -103,9 +102,10 @@ const store = kv(MyClientOrStoreInstance);
103
102
  // use the store
104
103
  ```
105
104
 
106
- The above represents the recommended naming; the default export, `kv` in this case, is a wrapper that will generate a "store" that then you use all around your codebase.
105
+ > [!IMPORTANT]
106
+ > The library delivers excellent performance for item-level operations (GET, SET, ADD, HAS, DEL). For other methods or detailed guidance, check the performance considerations and consult your specific client’s documentation.
107
107
 
108
- You can enforce the **types** for the store values directly at the store creation, or at the method level:
108
+ You can enforce **types** for store values either at store creation or at the method level:
109
109
 
110
110
  ```ts
111
111
  const store = kv<number>(new Map());
@@ -122,9 +122,12 @@ store.set<number>("abc", 10);
122
122
  store.set<number>("abc", "hello"); // FAILS
123
123
  ````
124
124
 
125
- > If you try to enforce data structure at _both_ the store level AND method level, then the method data type _should_ be a subclass of the store data structure, e.g. `kv<string | number>().get<string>("a")` will work, but `kv<string>().get<number>("a")` will _not_ work.
125
+ > [!WARNING]
126
+ > If you enforce types at _both_ the store level and method level, the method type must be a subset of the store type. For example, `kv<string | number>().get<string>("a")` works, but `kv<string>().get<number>("a")` _won't work_.
127
+
128
+ Store values must be JSON-like data. The Serializable type represents values composed of `string`, `number`, `boolean`, `null`, and `arrays` and plain `objects` whose values are serializable. Class instances or non-plain objects will lose their prototypes and methods when stored.
126
129
 
127
- The type should always be `Serializable`, which is `number | string | boolean | Object | Array` (values can be `null` inside Object+Array). These types, along with the Store and Client, are exported as well:
130
+ These are the exported types, `Client`, `Serializable` and `Store`:
128
131
 
129
132
  ```ts
130
133
  import kv from "polystore";
@@ -137,9 +140,9 @@ const value: Serializable = store.get('hello');
137
140
 
138
141
  ### .get()
139
142
 
140
- Retrieve a single value from the store. Will return `null` if the value is not set in the store, or if it was set but has already expired:
143
+ Retrieves a single value from the store. If the key has never been set, was deleted, or has expired, it returns `null`:
141
144
 
142
- ```js
145
+ ```ts
143
146
  const value = await store.get(key: string);
144
147
 
145
148
  console.log(await store.get("key1")); // "Hello World"
@@ -147,13 +150,75 @@ console.log(await store.get("key2")); // ["my", "grocery", "list"]
147
150
  console.log(await store.get("key3")); // { name: "Francisco" }
148
151
  ```
149
152
 
150
- If the value is returned, it can be a simple type like `boolean`, `string` or `number`, or it can be a plain `Object` or `Array`, or any combination of those.
153
+ You can specify the type either at [the store level](#api) or at the method level:
154
+
155
+ ```ts
156
+ console.log(await store.get<string>("key1")); // "Hello World"
157
+ console.log(await store.get<string[]>("key2")); // ["my", "grocery", "list"]
158
+ console.log(await store.get<User>("key3")); // { name: "Francisco" }
159
+ ```
160
+
161
+ <details>
162
+ <summary>The value and its types must be <b>Serializable</b></summary>
163
+
164
+ The value returned by `.get()` must be **serializable**. Valid types include:
165
+
166
+ * `string`, `number`, `boolean`
167
+ * Arrays or plain objects whose values are themselves serializable
168
+ * Nested `null` values are allowed
169
+
170
+ Class instances or non-plain objects will lose their prototypes and methods when stored. Only JSON-serializable data is safe. Examples:
171
+
172
+ ```ts
173
+ // Valid
174
+ type ValueType = string;
175
+ type ValueType = string | number;
176
+ type ValueType = { id: number; name: string; age: number | null };
177
+
178
+ // Invalid
179
+ type ValueType = Date; // Loses prototype
180
+ type ValueType = null; // Only allowed as nested value
181
+ type ValueType = Infinity; // Not serializable
182
+ ```
183
+
184
+ In short, only JSON-serializable data is safe to store.
185
+ </details>
186
+
187
+ When there's no value (either never set, deleted, or expired), `.get()` will return `null`:
188
+
189
+ ```ts
190
+ // Never set
191
+ console.log(await store.get("key1")); // null
192
+
193
+ // Deleted
194
+ await store.set("key2", "Hello");
195
+ console.log(await store.get("key2")); // "Hello"
196
+ await store.del('key2');
197
+ console.log(await store.get("key2")); // null
198
+
199
+ // Expired
200
+ await store.set("key3", "Hello", { expires: '1s' });
201
+ console.log(await store.get("key3")); // "Hello"
202
+ await new Promise((done) => setTimeout(done, 2000)); // Wait 2 seconds
203
+ console.log(await store.get("key3")); // null (already expired)
204
+ ```
205
+
206
+ > [!WARNING]
207
+ > Attempting to read an expired key that wasn't automatically evicted will trigger a delete internally. This should not affect you directly, but it's good to know since you might expect a `read` operation not to modify the underlying data. See the [Eviction](#eviction) section and your specific client for details.
208
+
209
+ If you are using a substore with `.prefix()`, `.get()` will respect it:
210
+
211
+ ```ts
212
+ const session = store.prefix('session:');
213
+
214
+ console.log(await session.get('key1'));
215
+ // Same as store.get("session:key1")
216
+ ```
151
217
 
152
- When there's no value (either never set, or expired), `null` will be returned from the operation.
153
218
 
154
219
  ### .set()
155
220
 
156
- Create or update a value in the store. Will return a promise that resolves with the key when the value has been saved. The value needs to be serializable:
221
+ Create or update a value in the store. Will return a promise that resolves with the key when the value has been saved:
157
222
 
158
223
  ```js
159
224
  await store.set(key: string, value: any, options?: { expires: number|string });
@@ -163,7 +228,39 @@ await store.set("key2", ["my", "grocery", "list"], { expires: "1h" });
163
228
  await store.set("key3", { name: "Francisco" }, { expires: 60 * 60 });
164
229
  ```
165
230
 
166
- The value can be a simple type like `boolean`, `string` or `number`, or it can be a plain `Object` or `Array`, or a combination of those. It **cannot** be a more complex or non-serializable values like a `Date()`, `Infinity`, `undefined` (casted to `null`), a `Symbol`, etc.
231
+ You can specify the type either at [the store level](#api) or at the method level:
232
+
233
+ ```ts
234
+ await store.set<string>("key1", "Hello World");
235
+ await store.set<string[]>("key2", ["my", "grocery", "list"]);
236
+ await store.set<User>("key3", { name: "Francisco" });
237
+ ```
238
+
239
+ <details>
240
+ <summary>The value and its types must be <b>Serializable</b></summary>
241
+
242
+ The value returned by `.get()` must be **serializable**. Valid types include:
243
+
244
+ * `string`, `number`, `boolean`
245
+ * Arrays or plain objects whose values are themselves serializable
246
+ * Nested `null` values are allowed
247
+
248
+ Class instances or non-plain objects will lose their prototypes and methods when stored. Only JSON-serializable data is safe. Examples:
249
+
250
+ ```ts
251
+ // Valid
252
+ type ValueType = string;
253
+ type ValueType = string | number;
254
+ type ValueType = { id: number; name: string; age: number | null };
255
+
256
+ // Invalid
257
+ type ValueType = Date; // Loses prototype
258
+ type ValueType = null; // Only allowed as nested value
259
+ type ValueType = Infinity; // Not serializable
260
+ ```
261
+
262
+ In short, only JSON-serializable data is safe to store.
263
+ </details>
167
264
 
168
265
  - By default the keys _don't expire_.
169
266
  - Setting the `value` to `null`, or the `expires` to `0` is the equivalent of deleting the key+value.
@@ -648,7 +745,7 @@ console.log(await store.get("key1"));
648
745
  </ul>
649
746
  </details>
650
747
 
651
- ### Redis Client
748
+ ### Redis
652
749
 
653
750
  Supports the official Node Redis Client. You can pass either the client or the promise:
654
751
 
@@ -676,6 +773,86 @@ You don't need to `await` for the connect or similar, this will process it prope
676
773
  </ul>
677
774
  </details>
678
775
 
776
+
777
+
778
+ ### SQLite
779
+
780
+ Supports both **`bun:sqlite`** and **`better-sqlite3`** directly. Pass an already-opened database instance to `kv()` and Polystore will use the `kv` table to store keys, values, and expirations:
781
+
782
+ > [!IMPORTANT]
783
+ > The table `kv` will be created if it doesn't already exist
784
+
785
+ ```js
786
+ import kv from "polystore";
787
+ import Database from "better-sqlite3";
788
+ // Or: import Database from "bun:sqlite";
789
+
790
+ const db = new Database("data.db")
791
+ const store = kv(db);
792
+
793
+ await store.set("key1", "Hello world", { expires: "1h" });
794
+ console.log(await store.get("key1"));
795
+ // "Hello world"
796
+ ```
797
+
798
+ <details>
799
+ <summary>Why use polystore with <code>SQLite</code>?</summary>
800
+ <p>These benefits apply when wrapping a SQLite DB with polystore:</p>
801
+ <ul>
802
+ <li><strong>Intuitive expirations</strong>: specify expiration times like <code>10min</code> or <code>2h</code> without manual date handling.</li>
803
+ <li><strong>Substores</strong>: use prefixes to isolate sets of keys cleanly.</li>
804
+ <li><strong>Simple persistence</strong>: full on-disk durability with a minimal driver.</li>
805
+ </ul>
806
+ </details>
807
+
808
+ #### SQLite schema
809
+
810
+ This is the required schema:
811
+
812
+ ```sql
813
+ CREATE TABLE kv (
814
+ id TEXT PRIMARY KEY,
815
+ value TEXT,
816
+ expires_at INTEGER
817
+ );
818
+ ```
819
+
820
+ You can create these with failsafes to make it much easier to initialize:
821
+
822
+ ```js
823
+ db.exec(`
824
+ CREATE TABLE IF NOT EXISTS kv (
825
+ id TEXT PRIMARY KEY,
826
+ value TEXT NOT NULL,
827
+ expires_at INTEGER
828
+ )
829
+ `);
830
+ db.exec(
831
+ `CREATE INDEX IF NOT EXISTS idx_kv_expires_at ON kv (expires_at)`,
832
+ );
833
+ ````
834
+
835
+ #### SQLite expirations
836
+
837
+ If `expires` is provided, Polystore will convert it to a timestamp and persist it in `expires_at`. We handle reading/writing rows and expiration checks.
838
+
839
+ However, these are not auto-evicted since SQLite doesn't have native expiration eviction. To avoid having stale data that is not used anymore, it's recommended you set a periodic check and clear expired records manually.
840
+
841
+ There's many ways of doing this, but a simple/basic one is this:
842
+
843
+ ```js
844
+ // Clear expired keys once every 10 minutes
845
+ setInterval(() => {
846
+ db.prepare(`DELETE FROM kv WHERE expires_at < ?`).run(Date.now());
847
+ }, 10 * 60 * 1000);
848
+ ````
849
+
850
+ Note that Polystore is self-reliant and won't have any problem even if you don't set that script, it will never render a stale record. It's just for both your convenience and privacy reasons.
851
+
852
+
853
+
854
+
855
+
679
856
  ### Fetch API
680
857
 
681
858
  Calls an API to get/put the data:
@@ -910,6 +1087,7 @@ You'll need to be running the etcd store for this to work as expected.
910
1087
  </ul>
911
1088
  </details>
912
1089
 
1090
+
913
1091
  ### Postgres
914
1092
 
915
1093
  Use PostgreSQL with the `pg` library as a key-value store:
@@ -962,45 +1140,6 @@ This maps prefixes to table names for better performance on group operations.
962
1140
  </ul>
963
1141
  </details>
964
1142
 
965
- ### Prisma
966
-
967
- Use Prisma as a key-value store by passing a table model directly:
968
-
969
- ```js
970
- import kv from "polystore";
971
- import { PrismaClient } from "@prisma/client";
972
-
973
- const prisma = new PrismaClient();
974
- const store = kv(prisma.session);
975
-
976
- await store.set("key1", "Hello world", { expires: "1h" });
977
- console.log(await store.get("key1"));
978
- // "Hello world"
979
- ```
980
-
981
- Your Prisma schema needs a model with three columns: `id` (String), `value` (String/Text), and `expiresAt` (DateTime, nullable):
982
-
983
- ```prisma
984
- model session {
985
- id String @id
986
- value String @db.Text
987
- expiresAt DateTime?
988
- }
989
- ```
990
-
991
- All three columns are required. The `expiresAt` column should be nullable (`DateTime?`) to support records without expiration.
992
-
993
- <details>
994
- <summary>Why use polystore with Prisma?</summary>
995
- <p>These benefits are for wrapping Prisma with polystore:</p>
996
- <ul>
997
- <li><strong>Unified API</strong>: use the same API across all your storage backends.</li>
998
- <li><strong>Database-backed persistence</strong>: leverage your existing database for key-value storage.</li>
999
- <li><strong>Intuitive expirations</strong>: use plain English to specify the expiration time like <code>10min</code>. <a href="#expiration">Expirations</a>.</li>
1000
- <li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
1001
- </ul>
1002
- </details>
1003
-
1004
1143
  ### Custom store
1005
1144
 
1006
1145
  Please see the [creating a store](#creating-a-store) section for all the details!