polystore 0.18.0 → 0.19.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/index.d.ts CHANGED
@@ -1,4 +1,9 @@
1
+ type Prefix = string;
1
2
  type Expires = number | null | string;
3
+ type Options = {
4
+ prefix?: Prefix;
5
+ expires?: Expires;
6
+ };
2
7
  type StoreData<T extends Serializable = Serializable> = {
3
8
  value: T;
4
9
  expires: number | null;
@@ -8,7 +13,7 @@ type Serializable = string | number | boolean | null | (Serializable | null)[] |
8
13
  };
9
14
  interface ClientExpires {
10
15
  TYPE: string;
11
- EXPIRES: true;
16
+ HAS_EXPIRATION: true;
12
17
  promise?: Promise<any>;
13
18
  test?: (client: any) => boolean;
14
19
  get<T extends Serializable>(key: string): Promise<T | null> | T | null;
@@ -27,7 +32,7 @@ interface ClientExpires {
27
32
  }
28
33
  interface ClientNonExpires {
29
34
  TYPE: string;
30
- EXPIRES: false;
35
+ HAS_EXPIRATION: false;
31
36
  promise?: Promise<any>;
32
37
  test?: (client: any) => boolean;
33
38
  get<T extends Serializable>(key: string): Promise<StoreData<T> | null> | StoreData<T> | null;
@@ -46,13 +51,14 @@ interface ClientNonExpires {
46
51
  }
47
52
  type Client = ClientExpires | ClientNonExpires;
48
53
 
49
- declare class Store<TDefault extends Serializable = Serializable> {
54
+ declare class Store<TD extends Serializable = Serializable> {
50
55
  #private;
51
- PREFIX: string;
56
+ PREFIX: Prefix;
57
+ EXPIRES: Expires;
52
58
  promise: Promise<Client> | null;
53
59
  client: Client;
54
60
  type: string;
55
- constructor(clientPromise?: any);
61
+ constructor(clientPromise?: any, options?: Options);
56
62
  /**
57
63
  * Save the data on an autogenerated key, can add expiration as well:
58
64
  *
@@ -64,8 +70,8 @@ declare class Store<TDefault extends Serializable = Serializable> {
64
70
  *
65
71
  * **[→ Full .add() Docs](https://polystore.dev/documentation#add)**
66
72
  */
67
- add(value: TDefault, ttl?: Expires): Promise<string>;
68
- add<T extends TDefault>(value: T, ttl?: Expires): Promise<string>;
73
+ add(value: TD, options?: Options): Promise<string>;
74
+ add<T extends TD>(value: T, options?: Options): Promise<string>;
69
75
  /**
70
76
  * Save the data on the given key, can add expiration as well:
71
77
  *
@@ -77,8 +83,8 @@ declare class Store<TDefault extends Serializable = Serializable> {
77
83
  *
78
84
  * **[→ Full .set() Docs](https://polystore.dev/documentation#set)**
79
85
  */
80
- set(key: string, value: TDefault, ttl?: Expires): Promise<string>;
81
- set<T extends TDefault>(key: string, value: T, ttl?: Expires): Promise<string>;
86
+ set(key: string, value: TD, options?: Options): Promise<string>;
87
+ set<T extends TD>(key: string, value: T, options?: Options): Promise<string>;
82
88
  /**
83
89
  * Read a single value from the KV store:
84
90
  *
@@ -93,8 +99,8 @@ declare class Store<TDefault extends Serializable = Serializable> {
93
99
  *
94
100
  * **[→ Full .get() Docs](https://polystore.dev/documentation#get)**
95
101
  */
96
- get(key: string): Promise<TDefault | null>;
97
- get<T extends TDefault>(key: string): Promise<T | null>;
102
+ get(key: string): Promise<TD | null>;
103
+ get<T extends TD>(key: string): Promise<T | null>;
98
104
  /**
99
105
  * Check whether a key exists or not:
100
106
  *
@@ -144,8 +150,8 @@ declare class Store<TDefault extends Serializable = Serializable> {
144
150
  *
145
151
  * **[→ Full Iterator Docs](https://polystore.dev/documentation#iterator)**
146
152
  */
147
- [Symbol.asyncIterator](): AsyncGenerator<[string, TDefault], void, unknown>;
148
- [Symbol.asyncIterator]<T extends TDefault>(): AsyncGenerator<[
153
+ [Symbol.asyncIterator](): AsyncGenerator<[string, TD], void, unknown>;
154
+ [Symbol.asyncIterator]<T extends TD>(): AsyncGenerator<[
149
155
  string,
150
156
  T
151
157
  ], void, unknown>;
@@ -162,8 +168,8 @@ declare class Store<TDefault extends Serializable = Serializable> {
162
168
  *
163
169
  * **[→ Full .entries() Docs](https://polystore.dev/documentation#entries)**
164
170
  */
165
- entries(): Promise<[string, TDefault][]>;
166
- entries<T extends TDefault>(): Promise<[string, T][]>;
171
+ entries(): Promise<[string, TD][]>;
172
+ entries<T extends TD>(): Promise<[string, T][]>;
167
173
  /**
168
174
  * Return an array of the keys in the store:
169
175
  *
@@ -191,8 +197,8 @@ declare class Store<TDefault extends Serializable = Serializable> {
191
197
  *
192
198
  * **[→ Full .values() Docs](https://polystore.dev/documentation#values)**
193
199
  */
194
- values(): Promise<TDefault[]>;
195
- values<T extends TDefault>(): Promise<T[]>;
200
+ values(): Promise<TD[]>;
201
+ values<T extends TD>(): Promise<T[]>;
196
202
  /**
197
203
  * Return an object with the keys:values in the store:
198
204
  *
@@ -206,20 +212,24 @@ declare class Store<TDefault extends Serializable = Serializable> {
206
212
  *
207
213
  * **[→ Full .all() Docs](https://polystore.dev/documentation#all)**
208
214
  */
209
- all(): Promise<Record<string, TDefault>>;
210
- all<T extends TDefault>(): Promise<Record<string, T>>;
215
+ all(): Promise<Record<string, TD>>;
216
+ all<T extends TD>(): Promise<Record<string, T>>;
211
217
  /**
212
- * Delete all of the records of the store:
218
+ * Create a substore where all the keys are stored with
219
+ * the given prefix:
213
220
  *
214
221
  * ```js
215
- * await store.clear();
222
+ * const session = store.prefix("session:");
223
+ * await session.set("key1", "value1");
224
+ * console.log(await session.entries()); // session.
225
+ * // [["key1", "value1"]]
226
+ * console.log(await store.entries()); // store.
227
+ * // [["session:key1", "value1"]]
216
228
  * ```
217
229
  *
218
- * It's useful for cache invalidation, clearing the data, and testing.
219
- *
220
- * **[→ Full .clear() Docs](https://polystore.dev/documentation#clear)**
230
+ * **[→ Full .prefix() Docs](https://polystore.dev/documentation#prefix)**
221
231
  */
222
- clear(): Promise<void>;
232
+ prefix(prefix?: Prefix): Store<TD>;
223
233
  /**
224
234
  * Create a substore where all the keys are stored with
225
235
  * the given prefix:
@@ -235,7 +245,29 @@ declare class Store<TDefault extends Serializable = Serializable> {
235
245
  *
236
246
  * **[→ Full .prefix() Docs](https://polystore.dev/documentation#prefix)**
237
247
  */
238
- prefix(prefix?: string): Store<TDefault>;
248
+ expires(expires?: Expires): Store<TD>;
249
+ /**
250
+ * Delete all of the records of the store:
251
+ *
252
+ * ```js
253
+ * await store.clear();
254
+ * ```
255
+ *
256
+ * It's useful for cache invalidation, clearing the data, and testing.
257
+ *
258
+ * **[→ Full .clear() Docs](https://polystore.dev/documentation#clear)**
259
+ */
260
+ clear(): Promise<void>;
261
+ /**
262
+ * Remove all expired records from the store.
263
+ *
264
+ * ```js
265
+ * await store.prune();
266
+ * ```
267
+ *
268
+ * Only affects stores where expiration is managed by this wrapper.
269
+ */
270
+ prune(): Promise<void>;
239
271
  /**
240
272
  * Stop the connection to the store, if any:
241
273
  *
@@ -250,6 +282,6 @@ declare class Store<TDefault extends Serializable = Serializable> {
250
282
  close(): Promise<void>;
251
283
  }
252
284
  declare function createStore(): Store<Serializable>;
253
- declare function createStore<T extends Serializable = Serializable>(client?: any): Store<T>;
285
+ declare function createStore<T extends Serializable = Serializable>(client?: any, options?: Options): Store<T>;
254
286
 
255
287
  export { type Client, type Serializable, Store, createStore as default };
package/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/clients/Client.ts
2
2
  var Client = class {
3
3
  TYPE;
4
- EXPIRES = false;
4
+ HAS_EXPIRATION = false;
5
5
  client;
6
6
  encode = (val) => JSON.stringify(val, null, 2);
7
7
  decode = (val) => val ? JSON.parse(val) : null;
@@ -14,7 +14,7 @@ var Client = class {
14
14
  var Api = class extends Client {
15
15
  TYPE = "API";
16
16
  // Indicate that the file handler DOES handle expirations
17
- EXPIRES = true;
17
+ HAS_EXPIRATION = true;
18
18
  static test = (client) => typeof client === "string" && /^https?:\/\//.test(client);
19
19
  #api = async (key, opts = "", method = "GET", body) => {
20
20
  const url = `${this.client.replace(/\/$/, "")}/${encodeURIComponent(key)}${opts}`;
@@ -49,7 +49,7 @@ var Api = class extends Client {
49
49
  var Cloudflare = class extends Client {
50
50
  TYPE = "CLOUDFLARE";
51
51
  // It handles expirations natively
52
- EXPIRES = true;
52
+ HAS_EXPIRATION = true;
53
53
  static testKeys = ["getWithMetadata", "get", "list", "delete"];
54
54
  get = async (key) => {
55
55
  const value = await this.client.get(key);
@@ -98,7 +98,7 @@ var Cloudflare = class extends Client {
98
98
  var Cookie = class extends Client {
99
99
  TYPE = "COOKIE";
100
100
  // It handles expirations natively
101
- EXPIRES = true;
101
+ HAS_EXPIRATION = true;
102
102
  // Check if this is the right class for the given client
103
103
  static test = (client) => {
104
104
  return client === "cookie" || client === "cookies";
@@ -145,7 +145,7 @@ var Cookie = class extends Client {
145
145
  var Etcd = class extends Client {
146
146
  TYPE = "ETCD3";
147
147
  // It desn't handle expirations natively
148
- EXPIRES = false;
148
+ HAS_EXPIRATION = false;
149
149
  // Check if this is the right class for the given client
150
150
  static testKeys = ["leaseClient", "watchClient", "watchManager"];
151
151
  get = async (key) => {
@@ -172,7 +172,7 @@ var Etcd = class extends Client {
172
172
  var File = class extends Client {
173
173
  TYPE = "FILE";
174
174
  // It desn't handle expirations natively
175
- EXPIRES = false;
175
+ HAS_EXPIRATION = false;
176
176
  fsp;
177
177
  file = "";
178
178
  #lock = Promise.resolve();
@@ -266,7 +266,7 @@ var noFileOk = (error) => {
266
266
  var Folder = class extends Client {
267
267
  TYPE = "FOLDER";
268
268
  // It desn't handle expirations natively
269
- EXPIRES = false;
269
+ HAS_EXPIRATION = false;
270
270
  fsp;
271
271
  folder;
272
272
  // Check if this is the right class for the given client
@@ -312,7 +312,7 @@ var Folder = class extends Client {
312
312
  var Forage = class extends Client {
313
313
  TYPE = "FORAGE";
314
314
  // It desn't handle expirations natively
315
- EXPIRES = false;
315
+ HAS_EXPIRATION = false;
316
316
  // Check if this is the right class for the given client
317
317
  static test = (client) => client?.defineDriver && client?.dropInstance && client?.INDEXEDDB;
318
318
  get = (key) => this.client.getItem(key);
@@ -346,7 +346,7 @@ var notFound = (error) => {
346
346
  var Level = class extends Client {
347
347
  TYPE = "LEVEL";
348
348
  // It desn't handle expirations natively
349
- EXPIRES = false;
349
+ HAS_EXPIRATION = false;
350
350
  // Check if this is the right class for the given client
351
351
  static testKeys = ["attachResource", "detachResource", "prependOnceListener"];
352
352
  get = (key) => this.client.get(key, { valueEncoding }).catch(notFound);
@@ -381,7 +381,7 @@ var Level = class extends Client {
381
381
  var Memory = class extends Client {
382
382
  TYPE = "MEMORY";
383
383
  // It desn't handle expirations natively
384
- EXPIRES = false;
384
+ HAS_EXPIRATION = false;
385
385
  // Check if this is the right class for the given client
386
386
  static test = (client) => client instanceof Map;
387
387
  get = (key) => this.client.get(key) ?? null;
@@ -399,7 +399,7 @@ var Memory = class extends Client {
399
399
  var Redis = class extends Client {
400
400
  TYPE = "REDIS";
401
401
  // Indicate if this client handles expirations (true = it does)
402
- EXPIRES = true;
402
+ HAS_EXPIRATION = true;
403
403
  // Check if this is the right class for the given client
404
404
  static test = (client) => client && client.pSubscribe && client.sSubscribe;
405
405
  get = async (key) => this.decode(await this.client.get(key));
@@ -446,7 +446,7 @@ var SQLite = class extends Client {
446
446
  // sqlite does not natively support expirations. This is because it does
447
447
  // support creating a `expires_at:Date` column that makes managing
448
448
  // expirations much easier, so it's really "somewhere in between"
449
- EXPIRES = true;
449
+ HAS_EXPIRATION = true;
450
450
  // The table name to use
451
451
  table = "kv";
452
452
  // Make sure the folder already exists, so attempt to create it
@@ -530,7 +530,7 @@ ${prefix ? "AND id LIKE ?" : ""}
530
530
  var WebStorage = class extends Client {
531
531
  TYPE = "STORAGE";
532
532
  // It desn't handle expirations natively
533
- EXPIRES = false;
533
+ HAS_EXPIRATION = false;
534
534
  // Check if this is the right class for the given client
535
535
  static test(client) {
536
536
  if (typeof Storage === "undefined") return false;
@@ -610,10 +610,16 @@ function unix(expires) {
610
610
  // src/index.ts
611
611
  var Store = class _Store {
612
612
  PREFIX = "";
613
+ EXPIRES = null;
613
614
  promise;
614
615
  client;
615
616
  type = "UNKNOWN";
616
- constructor(clientPromise = /* @__PURE__ */ new Map()) {
617
+ constructor(clientPromise = /* @__PURE__ */ new Map(), options = {
618
+ prefix: "",
619
+ expires: null
620
+ }) {
621
+ this.PREFIX = options.prefix || "";
622
+ this.EXPIRES = parse(options.expires || null);
617
623
  this.promise = Promise.resolve(clientPromise).then(async (client) => {
618
624
  this.client = this.#find(client);
619
625
  this.#validate(this.client);
@@ -645,7 +651,7 @@ var Store = class _Store {
645
651
  if (!client.set || !client.get || !client.iterate) {
646
652
  throw new Error("Client should have .get(), .set() and .iterate()");
647
653
  }
648
- if (client.EXPIRES) return;
654
+ if (client.HAS_EXPIRATION) return;
649
655
  for (let method of ["has", "keys", "values"]) {
650
656
  if (client[method]) {
651
657
  const msg = `You can only define client.${method}() when the client manages the expiration.`;
@@ -653,50 +659,48 @@ var Store = class _Store {
653
659
  }
654
660
  }
655
661
  }
656
- // Check if the given data is fresh or not; if
662
+ // Check if the given data is fresh or not
657
663
  #isFresh(data, key) {
658
664
  if (!data || typeof data !== "object" || !("value" in data)) {
659
- if (key) this.del(key);
660
665
  return false;
661
666
  }
662
- if (data.expires === null) return true;
663
- if (data.expires > Date.now()) return true;
664
- if (key) this.del(key);
665
- return false;
667
+ return data.expires === null || data.expires > Date.now();
666
668
  }
667
- async add(value, ttl) {
669
+ // Normalize returns the instance's `prefix` and `expires`
670
+ #expiration(expires) {
671
+ return parse(expires !== void 0 ? expires : this.EXPIRES);
672
+ }
673
+ async add(value, options) {
668
674
  await this.promise;
669
- let expires = parse(ttl);
675
+ const expires = this.#expiration(options?.expires);
676
+ const prefix = options?.prefix || this.PREFIX;
670
677
  if (this.client.add) {
671
- if (this.client.EXPIRES) {
672
- return await this.client.add(this.PREFIX, value, expires);
678
+ if (this.client.HAS_EXPIRATION) {
679
+ return this.client.add(prefix, value, expires);
673
680
  }
674
- expires = unix(expires);
675
- const key2 = await this.client.add(this.PREFIX, { expires, value });
676
- return key2;
681
+ return this.client.add(prefix, { expires: unix(expires), value });
677
682
  }
678
- const key = createId();
679
- return this.set(key, value, expires);
683
+ return this.set(createId(), value, { prefix, expires });
680
684
  }
681
- async set(key, value, ttl) {
685
+ async set(key, value, options) {
682
686
  await this.promise;
683
- const id = this.PREFIX + key;
684
- let expires = parse(ttl);
687
+ const expires = this.#expiration(options?.expires);
688
+ const prefix = options?.prefix || this.PREFIX;
689
+ const id = prefix + key;
685
690
  if (value === null || typeof expires === "number" && expires <= 0) {
686
691
  return this.del(key);
687
692
  }
688
- if (this.client.EXPIRES) {
693
+ if (this.client.HAS_EXPIRATION) {
689
694
  await this.client.set(id, value, expires);
690
695
  return key;
691
696
  }
692
- expires = unix(expires);
693
- await this.client.set(id, { expires, value });
697
+ await this.client.set(id, { expires: unix(expires), value });
694
698
  return key;
695
699
  }
696
700
  async get(key) {
697
701
  await this.promise;
698
702
  const id = this.PREFIX + key;
699
- if (this.client.EXPIRES) {
703
+ if (this.client.HAS_EXPIRATION) {
700
704
  const data = await this.client.get(id) ?? null;
701
705
  if (data === null) return null;
702
706
  return data;
@@ -747,7 +751,7 @@ var Store = class _Store {
747
751
  await this.client.del(id);
748
752
  return key;
749
753
  }
750
- if (this.client.EXPIRES) {
754
+ if (this.client.HAS_EXPIRATION) {
751
755
  await this.client.set(id, null, 0);
752
756
  } else {
753
757
  await this.client.set(id, null);
@@ -769,7 +773,7 @@ var Store = class _Store {
769
773
  }
770
774
  async *[Symbol.asyncIterator]() {
771
775
  await this.promise;
772
- if (this.client.EXPIRES) {
776
+ if (this.client.HAS_EXPIRATION) {
773
777
  for await (const [name, data] of this.client.iterate(this.PREFIX)) {
774
778
  const key = name.slice(this.PREFIX.length);
775
779
  yield [key, data];
@@ -787,7 +791,7 @@ var Store = class _Store {
787
791
  await this.promise;
788
792
  const trim = (key) => key.slice(this.PREFIX.length);
789
793
  if (this.client.entries) {
790
- if (this.client.EXPIRES) {
794
+ if (this.client.HAS_EXPIRATION) {
791
795
  const entries = await this.client.entries(this.PREFIX);
792
796
  return entries.map(([k, v]) => [trim(k), v]);
793
797
  } else {
@@ -795,7 +799,7 @@ var Store = class _Store {
795
799
  return entries.map(([k, v]) => [trim(k), v]).filter(([key, data]) => this.#isFresh(data, key)).map(([key, data]) => [key, data.value]);
796
800
  }
797
801
  }
798
- if (this.client.EXPIRES) {
802
+ if (this.client.HAS_EXPIRATION) {
799
803
  const list = [];
800
804
  for await (const [k, v] of this.client.iterate(this.PREFIX)) {
801
805
  list.push([trim(k), v]);
@@ -837,7 +841,7 @@ var Store = class _Store {
837
841
  async values() {
838
842
  await this.promise;
839
843
  if (this.client.values) {
840
- if (this.client.EXPIRES) return this.client.values(this.PREFIX);
844
+ if (this.client.HAS_EXPIRATION) return this.client.values(this.PREFIX);
841
845
  const list = await this.client.values(this.PREFIX);
842
846
  return list.filter((data) => this.#isFresh(data)).map((data) => data.value);
843
847
  }
@@ -848,6 +852,52 @@ var Store = class _Store {
848
852
  const entries = await this.entries();
849
853
  return Object.fromEntries(entries);
850
854
  }
855
+ /**
856
+ * Create a substore where all the keys are stored with
857
+ * the given prefix:
858
+ *
859
+ * ```js
860
+ * const session = store.prefix("session:");
861
+ * await session.set("key1", "value1");
862
+ * console.log(await session.entries()); // session.
863
+ * // [["key1", "value1"]]
864
+ * console.log(await store.entries()); // store.
865
+ * // [["session:key1", "value1"]]
866
+ * ```
867
+ *
868
+ * **[→ Full .prefix() Docs](https://polystore.dev/documentation#prefix)**
869
+ */
870
+ prefix(prefix = "") {
871
+ const store = new _Store(
872
+ Promise.resolve(this.promise).then(() => this.client)
873
+ );
874
+ store.PREFIX = this.PREFIX + prefix;
875
+ store.EXPIRES = this.EXPIRES;
876
+ return store;
877
+ }
878
+ /**
879
+ * Create a substore where all the keys are stored with
880
+ * the given prefix:
881
+ *
882
+ * ```js
883
+ * const session = store.prefix("session:");
884
+ * await session.set("key1", "value1");
885
+ * console.log(await session.entries()); // session.
886
+ * // [["key1", "value1"]]
887
+ * console.log(await store.entries()); // store.
888
+ * // [["session:key1", "value1"]]
889
+ * ```
890
+ *
891
+ * **[→ Full .prefix() Docs](https://polystore.dev/documentation#prefix)**
892
+ */
893
+ expires(expires = null) {
894
+ const store = new _Store(
895
+ Promise.resolve(this.promise).then(() => this.client)
896
+ );
897
+ store.EXPIRES = parse(expires);
898
+ store.PREFIX = this.PREFIX;
899
+ return store;
900
+ }
851
901
  /**
852
902
  * Delete all of the records of the store:
853
903
  *
@@ -871,26 +921,25 @@ var Store = class _Store {
871
921
  await Promise.all(keys.map((key) => this.del(key)));
872
922
  }
873
923
  /**
874
- * Create a substore where all the keys are stored with
875
- * the given prefix:
924
+ * Remove all expired records from the store.
876
925
  *
877
926
  * ```js
878
- * const session = store.prefix("session:");
879
- * await session.set("key1", "value1");
880
- * console.log(await session.entries()); // session.
881
- * // [["key1", "value1"]]
882
- * console.log(await store.entries()); // store.
883
- * // [["session:key1", "value1"]]
927
+ * await store.prune();
884
928
  * ```
885
929
  *
886
- * **[→ Full .prefix() Docs](https://polystore.dev/documentation#prefix)**
930
+ * Only affects stores where expiration is managed by this wrapper.
887
931
  */
888
- prefix(prefix = "") {
889
- const store = new _Store(
890
- Promise.resolve(this.promise).then(() => this.client)
891
- );
892
- store.PREFIX = this.PREFIX + prefix;
893
- return store;
932
+ async prune() {
933
+ await this.promise;
934
+ if (this.client.HAS_EXPIRATION) return;
935
+ for await (const [name, data] of this.client.iterate(
936
+ this.PREFIX
937
+ )) {
938
+ const key = name.slice(this.PREFIX.length);
939
+ if (!this.#isFresh(data, key)) {
940
+ await this.del(key);
941
+ }
942
+ }
894
943
  }
895
944
  /**
896
945
  * Stop the connection to the store, if any:
@@ -910,8 +959,8 @@ var Store = class _Store {
910
959
  }
911
960
  }
912
961
  };
913
- function createStore(client) {
914
- return new Store(client);
962
+ function createStore(client, options) {
963
+ return new Store(client, options);
915
964
  }
916
965
  export {
917
966
  createStore as default
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polystore",
3
- "version": "0.18.0",
3
+ "version": "0.19.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",
@@ -11,9 +11,17 @@
11
11
  "sideEffects": false,
12
12
  "main": "index.js",
13
13
  "types": "index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./index.d.ts",
17
+ "import": "./index.js"
18
+ },
19
+ "./express": "./src/express.js"
20
+ },
14
21
  "files": [
15
22
  "index.js",
16
- "index.d.ts"
23
+ "index.d.ts",
24
+ "src/express.js"
17
25
  ],
18
26
  "scripts": {
19
27
  "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",
package/readme.md CHANGED
@@ -14,18 +14,20 @@ const store4 = kv(yourOwnStore); // Create a store based on your code
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
- - [`.set(key, value, ttl?)`](#set): save a single value that is serializable.
18
- - [`.add(value, ttl?)`](#add): save a single value with an auto-generated key.
17
+ - [`.set(key, value, options?)`](#set): save a single value that is serializable.
18
+ - [`.add(value, options?)`](#add): save a single value with an auto-generated key.
19
19
  - [`.has(key)`](#has): check whether a key exists and is not expired.
20
20
  - [`.del(key)`](#del): delete a single key/value from the store.
21
+ - [`.prefix(prefix)`](#prefix): create a sub-store that manages the keys with that prefix.
22
+ - [`.expires(expires)`](#expires): create a sub-store with a different default expiration.
21
23
  - [Iterator](#iterator): go through all of the key/values one by one.
22
24
  - [`.keys()`](#keys): get a list of all the available keys in the store.
23
25
  - [`.values()`](#values): get a list of all the available values in the store.
24
26
  - [`.entries()`](#entries): get a list of all the available key-value pairs.
25
27
  - [`.all()`](#all): get an object of all the key:values mapped.
26
- - [`.clear()`](#clear): delete ALL of the data in the store, effectively resetting it.
28
+ - [`.clear()`](#clear): delete **all** of the data in the store, effectively resetting it.
29
+ - [`.prune()`](#prune): delete only the **expired** data from the store.
27
30
  - [`.close()`](#close): (only _some_ stores) ends the connection to the store.
28
- - [`.prefix(prefix)`](#prefix): create a sub-store that manages the keys with that prefix.
29
31
 
30
32
  Available clients for the KV store:
31
33
 
@@ -97,7 +99,7 @@ The base `kv()` initialization is shared across clients ([see full clients list]
97
99
  import kv from "polystore";
98
100
 
99
101
  // Initialize it; NO "new"; NO "await", just a plain function wrap:
100
- const store = kv(MyClientOrStoreInstance);
102
+ const store = kv(MyClientInstance, { expires: null, prefix: "" });
101
103
 
102
104
  // use the store
103
105
  ```
@@ -120,23 +122,24 @@ store.get<number>("abc"); // number | null
120
122
  store.set<number>("abc", 10);
121
123
 
122
124
  store.set<number>("abc", "hello"); // FAILS
123
- ````
125
+ ```
124
126
 
125
127
  > [!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_.
128
+ > 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")` will show an error as expected.
127
129
 
128
130
  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.
129
131
 
130
- These are the exported types, `Client`, `Serializable` and `Store`:
132
+ These are the exported types, `Client`, `Serializable`, `Store` and `Options`:
131
133
 
132
134
  ```ts
133
135
  import kv from "polystore";
134
- import type { Client, Serializable, Store } from "polystore";
136
+ import type { Client, Serializable, Store, Options } from "polystore";
135
137
 
136
138
  const client: Client = ...; // See #creating-a-store
137
- const store: Store = kv(client);
138
- const value: Serializable = store.get('hello');
139
- ````
139
+ const store: Store = kv(client, opts as Options);
140
+ const key = await store.set('hello', 'b', opts as Options)
141
+ const value: Serializable = await store.get('hello');
142
+ ```
140
143
 
141
144
  ### .get()
142
145
 
@@ -150,7 +153,7 @@ console.log(await store.get("key2")); // ["my", "grocery", "list"]
150
153
  console.log(await store.get("key3")); // { name: "Francisco" }
151
154
  ```
152
155
 
153
- You can specify the type either at [the store level](#api) or at the method level:
156
+ You can specify the type of the return:
154
157
 
155
158
  ```ts
156
159
  console.log(await store.get<string>("key1")); // "Hello World"
@@ -197,15 +200,12 @@ await store.del('key2');
197
200
  console.log(await store.get("key2")); // null
198
201
 
199
202
  // Expired
200
- await store.set("key3", "Hello", "1s");
203
+ await store.set("key3", "Hello", { expires: "1s" });
201
204
  console.log(await store.get("key3")); // "Hello"
202
205
  await new Promise((done) => setTimeout(done, 2000)); // Wait 2 seconds
203
206
  console.log(await store.get("key3")); // null (already expired)
204
207
  ```
205
208
 
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
209
  If you are using a substore with `.prefix()`, `.get()` will respect it:
210
210
 
211
211
  ```ts
@@ -220,12 +220,16 @@ console.log(await session.get('key1'));
220
220
 
221
221
  Create or update a value in the store. Will return a promise that resolves with the key when the value has been saved:
222
222
 
223
- ```js
224
- await store.set(key: string, value: any, expires?: number|string);
223
+ ```ts
224
+ const key = await store.set(
225
+ key: string,
226
+ value: any,
227
+ options?: { expires?: number|string; prefix?: string }
228
+ );
225
229
 
226
230
  await store.set("key1", "Hello World");
227
- await store.set("key2", ["my", "grocery", "list"], "1h");
228
- await store.set("key3", { name: "Francisco" }, 60 * 60);
231
+ await store.set("key2", ["my", "grocery", "list"], { expires: "1h" });
232
+ await store.set("key3", { name: "Francisco" }, { expires: 60 * 60 });
229
233
  ```
230
234
 
231
235
  You can specify the type either at [the store level](#api) or at the method level:
@@ -268,7 +272,7 @@ In short, only JSON-serializable data is safe to store.
268
272
 
269
273
  #### Expires
270
274
 
271
- When the `ttl` option is set, it can be a number (**seconds**) or a string representing some time:
275
+ When the `expires` option is set, it can be a number (**seconds**) or a string representing some time:
272
276
 
273
277
  ```js
274
278
  // Valid "expire" values:
@@ -292,14 +296,17 @@ These are all the units available:
292
296
  Create a value in the store with an auto-generated key. Will return a promise that resolves with the key when the value has been saved. The value needs to be serializable:
293
297
 
294
298
  ```js
295
- const key:string = await store.add(value: any, expires?: number|string);
299
+ const key:string = await store.add(
300
+ value: any,
301
+ options?: { expires?: number|string; prefix?: string }
302
+ );
296
303
 
297
304
  const key1 = await store.add("Hello World");
298
- const key2 = await store.add(["my", "grocery", "list"], "1h");
299
- const key3 = await store.add({ name: "Francisco" }, 60 * 60);
305
+ const key2 = await store.add(["my", "grocery", "list"], { expires: "1h" });
306
+ const key3 = await store.add({ name: "Francisco" }, { expires: 60 * 60 });
300
307
  ```
301
308
 
302
- The value and expires are similar to [`.set()`](#set), except for the lack of the first argument, since `.add()` will generate the key automatically.
309
+ The value and options are similar to [`.set()`](#set), except for the lack of the first argument, since `.add()` automatically generates the key.
303
310
 
304
311
  The default key is 24 AlphaNumeric characters (upper+lower case), however this can change if you are using a `.prefix()` or some clients might generate it differently (only custom clients can do that right now).
305
312
 
@@ -346,7 +353,7 @@ In many cases, internally the check for `.has()` is the same as `.get()`, so if
346
353
 
347
354
  ```js
348
355
  const val = await store.get("key1");
349
- if (val) { ... }
356
+ if (val !== null) { ... }
350
357
  ```
351
358
 
352
359
  An example of an exception of the above is when you use it as a cache, then you can write code like this:
@@ -357,7 +364,7 @@ An example of an exception of the above is when you use it as a cache, then you
357
364
  async function fetchUser(id) {
358
365
  if (!(await store.has(id))) {
359
366
  const { data } = await axios.get(`/users/${id}`);
360
- await store.set(id, data, "1h");
367
+ await store.set(id, data, { expires: "1h" });
361
368
  }
362
369
  return store.get(id);
363
370
  }
@@ -379,7 +386,7 @@ const has3 = await store.has("session:key1");
379
386
  Remove a single key from the store and return the key itself:
380
387
 
381
388
  ```js
382
- await store.del(key: string);
389
+ const key = await store.del(key: string);
383
390
  ```
384
391
 
385
392
  It will ignore the operation if the key or value don't exist already (but won't throw). The API makes it easy to delete multiple keys at once:
@@ -387,7 +394,7 @@ It will ignore the operation if the key or value don't exist already (but won't
387
394
  ```js
388
395
  const keys = ["key1", "key2"];
389
396
  await Promise.all(keys.map(store.del));
390
- console.log(done);
397
+ console.log("done");
391
398
  ```
392
399
 
393
400
  An example with a prefix:
@@ -411,7 +418,7 @@ for await (const [key, value] of store) {
411
418
  }
412
419
  ```
413
420
 
414
- This is very useful for performance resons since it will retrieve the data sequentially, avoiding blocking the client while retrieving it all at once. The main disadvantage is if you keep writing data asynchronously while the async iterator is running.
421
+ This is very useful for performance reasons since it will retrieve the data sequentially, avoiding blocking the client while retrieving it all at once. The main disadvantage is if you keep writing data asynchronously while the async iterator is running.
415
422
 
416
423
  You can also iterate on a subset of the entries with `.prefix()` (the prefix is stripped from the key here, see [.`prefix()`](#prefix)):
417
424
 
@@ -428,11 +435,11 @@ for await (const [key, value] of store.prefix("session:")) {
428
435
  }
429
436
  ```
430
437
 
431
- There are also methods to retrieve all of the keys, values, or entries at once below, but those [have worse performance](#performance).
438
+ There are also methods to retrieve all the keys, values, or entries at once below, but those [have worse performance](#performance).
432
439
 
433
440
  ### .keys()
434
441
 
435
- Get all of the keys in the store as a simple array of strings:
442
+ Get all the keys in the store as a simple array of strings:
436
443
 
437
444
  ```js
438
445
  await store.keys();
@@ -446,11 +453,11 @@ const sessions = await store.prefix("session:").keys();
446
453
  // ["keyA", "keyB"]
447
454
  ```
448
455
 
449
- > We ensure that all of the keys returned by this method are _not_ expired, while discarding any potentially expired key. See [**expirations**](#expirations) for more details.
456
+ > We ensure that all the keys returned by this method are _not_ expired, while discarding any potentially expired key. See [**expirations**](#expirations) for more details.
450
457
 
451
458
  ### .values()
452
459
 
453
- Get all of the values in the store as a simple array with all the values:
460
+ Get all the values in the store as a simple array with all the values:
454
461
 
455
462
  ```js
456
463
  await store.values();
@@ -467,11 +474,11 @@ const companies = await store.prefix("company:").values();
467
474
  // A list of all the companies
468
475
  ```
469
476
 
470
- > We ensure that all of the values returned by this method are _not_ expired, while discarding any potentially expired key. See [**expirations**](#expirations) for more details.
477
+ > We ensure that all the values returned by this method are _not_ expired, while discarding any potentially expired key. See [**expirations**](#expirations) for more details.
471
478
 
472
479
  ### .entries()
473
480
 
474
- Get all of the entries (key:value tuples) in the store:
481
+ Get all the entries (key:value tuples) in the store:
475
482
 
476
483
  ```js
477
484
  const entries = await store.entries();
@@ -512,31 +519,57 @@ const sessionObj = await store.prefix('session:').all();
512
519
 
513
520
  ### .clear()
514
521
 
515
- Remove all of the data from the client and resets it to the original state:
522
+ Remove all data from the current store and resets it to the original state:
516
523
 
517
524
  ```js
518
525
  await store.clear();
519
526
  ```
520
527
 
528
+ If called on a `.prefix()` substore, only keys with that prefix are removed:
529
+
530
+ ```ts
531
+ const sessions = store.prefix("session:");
532
+
533
+ await sessions.clear();
534
+ // removes only session:* keys
535
+ ````
536
+
537
+ ### .prune()
538
+
539
+ > [!IMPORTANT]
540
+ > For stores with native expiration (Redis, Cloudflare KV, etc.), `.prune()` usually does nothing because the underlying client already removes expired keys automatically.
541
+
542
+ Remove only expired records from the store.
543
+
544
+ ```ts
545
+ await store.prune();
546
+ ```
547
+
548
+ This method is only useful for stores that do not support native expiration (such as Map, localStorage, files, etc.). In those cases expired records are hidden by the API but may still exist internally until they are removed.
549
+
550
+ Calling `.prune()` scans the store and deletes all expired entries.
551
+
552
+ This operation is O(n) and should typically be run in a scheduled job or maintenance task rather than on every request.
553
+
521
554
  ### .close()
522
555
 
523
- Close the connetion (if any) from the client:
556
+ Close the connection (if any) from the client:
524
557
 
525
558
  ```js
526
559
  await store.close();
527
- ````
560
+ ```
528
561
 
529
562
  ### .prefix()
530
563
 
531
- > There's [an in-depth explanation about Substores](#substores) that is very informative for production usage.
532
-
533
- 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:
564
+ 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. You don't need to await for it:
534
565
 
535
566
  ```js
536
567
  const store = kv(new Map());
537
568
  const session = store.prefix("session:");
538
569
  ```
539
570
 
571
+ > There's [an in-depth explanation about Substores](#substores) that is very informative for production usage.
572
+
540
573
  Then all of the operations will be converted internally to add the prefix when reading, writing, etc:
541
574
 
542
575
  ```js
@@ -567,6 +600,31 @@ const books = kv(`file://${import.meta.dirname}/books.json`);
567
600
 
568
601
  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_.
569
602
 
603
+ ### .expires()
604
+
605
+ Create a substore with a different default expiration time.
606
+
607
+ ```ts
608
+ const store = kv(new Map()); // No expiration
609
+ const cache = store.expires("10min"); // 10 min expiration
610
+ await cache.set("a", "b"); // 10 mins
611
+ await cache.set("c", "e", { expires: '5s' }); // 5 seconds, overwrites it
612
+ ```
613
+
614
+ > There's [an in-depth explanation about Expirations](#expirations) that is very informative for production usage.
615
+
616
+ But applied automatically to all writes in the substore. Explicit expiration always overrides the default:
617
+
618
+ ```ts
619
+ await cache.set("a", "b", { expires: "1h" });
620
+ ```
621
+
622
+ You can combine it with .prefix():
623
+
624
+ ```ts
625
+ const sessions = store.prefix("session:").expires("1day");
626
+ ```
627
+
570
628
  ## Clients
571
629
 
572
630
  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"`.
@@ -599,7 +657,7 @@ import kv from "polystore";
599
657
 
600
658
  const store = kv(new Map());
601
659
 
602
- await store.set("key1", "Hello world", "1h");
660
+ await store.set("key1", "Hello world", { expires: "1h" });
603
661
  console.log(await store.get("key1"));
604
662
  // "Hello world"
605
663
  ```
@@ -615,9 +673,9 @@ console.log(await store.get("key1"));
615
673
 
616
674
  ```js
617
675
  // GOOD - with polystore
618
- await store.set("key1", { name: "Francisco" }, "2days");
676
+ await store.set("key1", { name: "Francisco" }, { expires: "2days" });
619
677
 
620
- // COMPLEX - With sessionStorage
678
+ // COMPLEX - With plain Map
621
679
  const data = new Map();
622
680
  data.set("key1", { name: "Francisco" });
623
681
  // Expiration not supported
@@ -632,7 +690,7 @@ import kv from "polystore";
632
690
 
633
691
  const store = kv(localStorage);
634
692
 
635
- await store.set("key1", "Hello world", "1h");
693
+ await store.set("key1", "Hello world", { expires: "1h" });
636
694
  console.log(await store.get("key1"));
637
695
  // "Hello world"
638
696
  ```
@@ -651,7 +709,7 @@ Same limitations as always apply to localStorage, if you think you are going to
651
709
 
652
710
  ```js
653
711
  // GOOD - with polystore
654
- await store.set("key1", { name: "Francisco" }, "2days");
712
+ await store.set("key1", { name: "Francisco" }, { expires: "2days" });
655
713
 
656
714
  // COMPLEX - With localStorage
657
715
  const serialValue = JSON.stringify({ name: "Francisco" });
@@ -668,7 +726,7 @@ import kv from "polystore";
668
726
 
669
727
  const store = kv(sessionStorage);
670
728
 
671
- await store.set("key1", "Hello world", "1h");
729
+ await store.set("key1", "Hello world", { expires: "1h" });
672
730
  console.log(await store.get("key1"));
673
731
  // "Hello world"
674
732
  ```
@@ -685,7 +743,7 @@ console.log(await store.get("key1"));
685
743
 
686
744
  ```js
687
745
  // GOOD - with polystore
688
- await store.set("key1", { name: "Francisco" }, "2days");
746
+ await store.set("key1", { name: "Francisco" }, { expires: "2days" });
689
747
 
690
748
  // COMPLEX - With sessionStorage
691
749
  const serialValue = JSON.stringify({ name: "Francisco" });
@@ -702,7 +760,7 @@ import kv from "polystore";
702
760
 
703
761
  const store = kv("cookie"); // just a plain string
704
762
 
705
- await store.set("key1", "Hello world", "1h");
763
+ await store.set("key1", "Hello world", { expires: "1h" });
706
764
  console.log(await store.get("key1"));
707
765
  // "Hello world"
708
766
  ```
@@ -731,7 +789,7 @@ import localForage from "localforage";
731
789
 
732
790
  const store = kv(localForage);
733
791
 
734
- await store.set("key1", "Hello world", "1h");
792
+ await store.set("key1", "Hello world", { expires: "1h" });
735
793
  console.log(await store.get("key1"));
736
794
  // "Hello world"
737
795
  ```
@@ -755,7 +813,7 @@ import { createClient } from "redis";
755
813
 
756
814
  const store = kv(createClient().connect());
757
815
 
758
- await store.set("key1", "Hello world", "1h");
816
+ await store.set("key1", "Hello world", { expires: "1h" });
759
817
  console.log(await store.get("key1"));
760
818
  // "Hello world"
761
819
  ```
@@ -790,7 +848,7 @@ import Database from "better-sqlite3";
790
848
  const db = new Database("data.db")
791
849
  const store = kv(db);
792
850
 
793
- await store.set("key1", "Hello world", "1h");
851
+ await store.set("key1", "Hello world", { expires: "1h" });
794
852
  console.log(await store.get("key1"));
795
853
  // "Hello world"
796
854
  ```
@@ -830,13 +888,13 @@ db.exec(`
830
888
  db.exec(
831
889
  `CREATE INDEX IF NOT EXISTS idx_kv_expires_at ON kv (expires_at)`,
832
890
  );
833
- ````
891
+ ```
834
892
 
835
893
  #### SQLite expirations
836
894
 
837
895
  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
896
 
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.
897
+ Expired rows remain in the table unless you delete them manually. To avoid having stale data that is not used anymore, it's recommended you set a periodic check and clear expired records manually.
840
898
 
841
899
  There's many ways of doing this, but a simple/basic one is this:
842
900
 
@@ -845,7 +903,7 @@ There's many ways of doing this, but a simple/basic one is this:
845
903
  setInterval(() => {
846
904
  db.prepare(`DELETE FROM kv WHERE expires_at < ?`).run(Date.now());
847
905
  }, 10 * 60 * 1000);
848
- ````
906
+ ```
849
907
 
850
908
  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
909
 
@@ -862,19 +920,19 @@ import kv from "polystore";
862
920
 
863
921
  const store = kv("https://kv.example.com/");
864
922
 
865
- await store.set("key1", "Hello world", "1h");
923
+ await store.set("key1", "Hello world", { expires: "1h" });
866
924
  console.log(await store.get("key1"));
867
925
  // "Hello world"
868
926
  ```
869
927
 
870
- > Note: the API 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.
928
+ > Note: the API client expire resolution is in 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.
871
929
 
872
930
  > Note: see the [reference implementation in src/server.js](https://github.com/franciscop/polystore/blob/master/src/server.js)
873
931
 
874
932
 
875
933
  ### File
876
934
 
877
- Treat a JSON file in your filesystem as the source for the KV store. Pass it an absolute `file://` url or a `new URL('file://...')` instance:
935
+ Treat a JSON file in your filesystem as the source for the KV store. Pass it an absolute `file://` url or a `new URL("file://...")` instance:
878
936
 
879
937
  ```js
880
938
  import kv from "polystore";
@@ -882,7 +940,7 @@ import kv from "polystore";
882
940
  // Path is "/Users/me/project/cache.json"
883
941
  const store = kv("file:///Users/me/project/cache.json");
884
942
 
885
- await store.set("key1", "Hello world", "1h");
943
+ await store.set("key1", "Hello world", { expires: "1h" });
886
944
  console.log(await store.get("key1"));
887
945
  // "Hello world"
888
946
  ```
@@ -916,7 +974,7 @@ const store1 = kv(new URL(`file://${process.cwd()}/cache.json`));
916
974
 
917
975
  ```js
918
976
  // GOOD - with polystore
919
- await store.set("key1", { name: "Francisco" }, "2days");
977
+ await store.set("key1", { name: "Francisco" }, { expires: "2days" });
920
978
 
921
979
  // COMPLEX - With native file managing
922
980
  const file = './data/users.json';
@@ -937,7 +995,7 @@ import kv from "polystore";
937
995
 
938
996
  const store = kv("file:///Users/me/project/data/");
939
997
 
940
- await store.set("key1", "Hello world", "1h");
998
+ await store.set("key1", "Hello world", { expires: "1h" });
941
999
  // Writes "./data/key1.json"
942
1000
  console.log(await store.get("key1"));
943
1001
  // "Hello world"
@@ -975,7 +1033,7 @@ const store1 = kv(new URL(`file://${process.cwd()}/cache/`));
975
1033
 
976
1034
  ```js
977
1035
  // GOOD - with polystore
978
- await store.set("key1", { name: "Francisco" }, "2days");
1036
+ await store.set("key1", { name: "Francisco" }, { expires: "2days" });
979
1037
 
980
1038
  // COMPLEX - With native folder
981
1039
  const file = './data/user/key1.json';
@@ -995,7 +1053,7 @@ export default {
995
1053
  async fetch(request, env, ctx) {
996
1054
  const store = kv(env.YOUR_KV_NAMESPACE);
997
1055
 
998
- await store.set("key1", "Hello world", "1h");
1056
+ await store.set("key1", "Hello world", { expires: "1h" });
999
1057
  console.log(await store.get("key1"));
1000
1058
  // "Hello world"
1001
1059
 
@@ -1018,7 +1076,7 @@ It expects that you pass the namespace from Cloudflare straight as a `kv()` argu
1018
1076
 
1019
1077
  ```js
1020
1078
  // GOOD - with polystore
1021
- await store.set("user", { name: "Francisco" }, "2days");
1079
+ await store.set("user", { name: "Francisco" }, { expires: "2days" });
1022
1080
 
1023
1081
  // COMPLEX - With native Cloudflare KV
1024
1082
  const serialValue = JSON.stringify({ name: "Francisco" });
@@ -1038,7 +1096,7 @@ import { Level } from "level";
1038
1096
 
1039
1097
  const store = kv(new Level("example", { valueEncoding: "json" }));
1040
1098
 
1041
- await store.set("key1", "Hello world", "1h");
1099
+ await store.set("key1", "Hello world", { expires: "1h" });
1042
1100
  console.log(await store.get("key1"));
1043
1101
  // "Hello world"
1044
1102
  ```
@@ -1055,7 +1113,7 @@ You will need to set the `valueEncoding` to `"json"` for the store to work as ex
1055
1113
 
1056
1114
  ```js
1057
1115
  // GOOD - with polystore
1058
- await store.set("user", { hello: 'world' }, "2days");
1116
+ await store.set("user", { hello: 'world' }, { expires: "2days" });
1059
1117
 
1060
1118
  // With Level:
1061
1119
  ?? // Just not possible
@@ -1071,7 +1129,7 @@ import { Etcd3 } from "etcd3";
1071
1129
 
1072
1130
  const store = kv(new Etcd3());
1073
1131
 
1074
- await store.set("key1", "Hello world", "1h");
1132
+ await store.set("key1", "Hello world", { expires: "1h" });
1075
1133
  console.log(await store.get("key1"));
1076
1134
  // "Hello world"
1077
1135
  ```
@@ -1101,7 +1159,7 @@ await client.connect();
1101
1159
 
1102
1160
  const store = kv(client);
1103
1161
 
1104
- await store.set("key1", "Hello world", "1h");
1162
+ await store.set("key1", "Hello world", { expires: "1h" });
1105
1163
  console.log(await store.get("key1"));
1106
1164
  // "Hello world"
1107
1165
  ```
@@ -1150,9 +1208,9 @@ Please see the [creating a store](#creating-a-store) section for all the details
1150
1208
 
1151
1209
  While all of our stores support `expires`, `.prefix()` and group operations, the nature of those makes them to have different performance characteristics.
1152
1210
 
1153
- **Expires** we polyfill expiration when the underlying client 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).
1211
+ **Expires** we polyfill expiration when the underlying client 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. Expired keys 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.prune();` 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).
1154
1212
 
1155
- **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 in the general case. 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.
1213
+ **Group operations** are mostly intended for small datasets, for one-off scripts or for dev purposes, since by their own nature they can _never_ be high performance in the general case. 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.
1156
1214
 
1157
1215
  **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.
1158
1216
 
@@ -1164,8 +1222,8 @@ We unify all of the clients diverse expiration methods into a single, easy one w
1164
1222
 
1165
1223
  ```js
1166
1224
  // in-memory store
1167
- const store = polystore(new Map());
1168
- await store.set("a", "b", "1s");
1225
+ const store = kv(new Map());
1226
+ await store.set("a", "b", { expires: "1s" });
1169
1227
 
1170
1228
  // These checks of course work:
1171
1229
  console.log(await store.keys()); // ['a']
@@ -1214,7 +1272,7 @@ These details are explained in the respective client information.
1214
1272
 
1215
1273
  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.
1216
1274
 
1217
- 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:
1275
+ When dealing with large or complex amounts of data in a KV store, sometimes it's useful to divide them by categories. Some examples might be:
1218
1276
 
1219
1277
  - You use KV as a cache, and have different categories of data.
1220
1278
  - You use KV as a session store, and want to differentiate different kinds of sessions.
@@ -1230,12 +1288,12 @@ To create a store, you define a class with these properties and methods:
1230
1288
  class MyClient {
1231
1289
  // If this is set to `true`, the CLIENT (you) handle the expiration, so
1232
1290
  // the `.set()` and `.add()` receive a `expires` that is a `null` or `number`:
1233
- EXPIRES = false;
1291
+ HAS_EXPIRATION = false;
1234
1292
 
1235
1293
  // Mandatory methods
1236
1294
  get (key): Promise<any>;
1237
1295
  set (key, value, null|number): Promise<null>;
1238
- iterate(prefix): AyncIterator<[string, any]>
1296
+ iterate(prefix): AsyncIterator<[string, any]>
1239
1297
 
1240
1298
  // Optional item methods (for optimization or customization)
1241
1299
  add (prefix, data, null|number): Promise<string>;
@@ -1255,7 +1313,7 @@ class MyClient {
1255
1313
 
1256
1314
  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. for the client, the `expires` will always be a `null` or `number`, never `undefined` or a `string`), but also it differs from polystore's public API, like `.add()` has a different signature, and the group methods all take a explicit prefix.
1257
1315
 
1258
- **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 **seconds** for the key/value to will expire.
1316
+ **Expires**: if you set the `HAS_EXPIRATION = 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 **seconds** for the key/value to will expire.
1259
1317
 
1260
1318
  **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:
1261
1319
 
@@ -1311,7 +1369,7 @@ class MyClient {
1311
1369
  }
1312
1370
  ```
1313
1371
 
1314
- 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.
1372
+ We don't set `HAS_EXPIRATION` to true since plain objects do NOT support expiration natively. So by not adding the `HAS_EXPIRATION` 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 `HAS_EXPIRATION = false`, but it's not needed in this case.
1315
1373
 
1316
1374
  ### Example: custom ID generation
1317
1375
 
@@ -1321,10 +1379,10 @@ You might want to provide your custom key generation algorithm, which I'm going
1321
1379
  class MyClient {
1322
1380
 
1323
1381
  // Add the opt method .add() to have more control over the ID generation
1324
- async add (prefix, data, { expires }) {
1382
+ async add (prefix, data, expires) {
1325
1383
  const id = customId();
1326
1384
  const key = prefix + id;
1327
- return this.set(key, data, { expires });
1385
+ return this.set(key, data, expires);
1328
1386
  }
1329
1387
 
1330
1388
  //
@@ -1375,7 +1433,7 @@ class MyClient {
1375
1433
 
1376
1434
  In this example on one of my projects, I needed to use Cloudflare's REST API since I didn't have access to any KV store I was happy with on Netlify's Edge Functions. So I created it like this:
1377
1435
 
1378
- > Warning: this code snippet is an experimental example and hasn't gone through rigurous testing as the rest of the library, so please treat with caution.
1436
+ > Warning: this code snippet is an experimental example and hasn't gone through rigorous testing as the rest of the library, so please treat with caution.
1379
1437
 
1380
1438
  ```js
1381
1439
  const {
@@ -1392,7 +1450,7 @@ const headers = {
1392
1450
  };
1393
1451
 
1394
1452
  class CloudflareCustom {
1395
- EXPIRES = true;
1453
+ HAS_EXPIRATION = true;
1396
1454
 
1397
1455
  async get(key) {
1398
1456
  const res = await fetch(`${baseUrl}/values/${key}`, { headers });
@@ -1439,9 +1497,9 @@ class CloudflareCustom {
1439
1497
  }
1440
1498
 
1441
1499
  const store = kv(CloudflareCustom);
1442
- ````
1500
+ ```
1443
1501
 
1444
- It's lacking few things, so make sure to adapt to your needs, but it worked for my very simple cache needs.
1502
+ It's lacking a few things, so make sure to adapt to your needs, but it worked for my very simple cache needs.
1445
1503
 
1446
1504
 
1447
1505
  ## Examples
@@ -1461,7 +1519,7 @@ async function getProductInfo(id: string) {
1461
1519
  // Some processing here
1462
1520
  const clean = raw??;
1463
1521
 
1464
- await store.set(id, clean, '10days');
1522
+ await store.set(id, clean, { expires: "10days" });
1465
1523
  return clean;
1466
1524
  }
1467
1525
  ```
@@ -1485,75 +1543,12 @@ if (process.env.REDIS_URL) {
1485
1543
  store = kv(createClient(process.env.REDIS_URL).connect());
1486
1544
  } else {
1487
1545
  console.log('kv:folder using a folder for cache data');
1488
- store = kv(`${process.cwd()}/data/`);
1546
+ store = kv(`file://${process.cwd()}/data/`);
1489
1547
  }
1490
1548
 
1491
1549
  export default store;
1492
1550
  ```
1493
1551
 
1494
-
1495
- ### Better Auth
1496
-
1497
- Polystore is directly compatible with Better Auth, so no need for any wrapper. For example, for Redis:
1498
-
1499
- ```ts
1500
- import kv from "polystore";
1501
- import { createClient } from "redis";
1502
- import { betterAuth } from "better-auth";
1503
-
1504
- const secondaryStorage = kv(createClient().connect());
1505
-
1506
- export const auth = betterAuth({
1507
- // ... other options
1508
- secondaryStorage
1509
- });
1510
- ````
1511
-
1512
- Compare that with their official documentation for using Redis:
1513
-
1514
- ```ts
1515
- import { createClient } from "redis";
1516
- import { betterAuth } from "better-auth";
1517
-
1518
- const redis = createClient();
1519
- await redis.connect();
1520
-
1521
- export const auth = betterAuth({
1522
- // ... other options
1523
- secondaryStorage: {
1524
- get: async (key) => {
1525
- return await redis.get(key);
1526
- },
1527
- set: async (key, value, ttl) => {
1528
- if (ttl) await redis.set(key, value, { EX: ttl });
1529
- // or for ioredis:
1530
- // if (ttl) await redis.set(key, value, 'EX', ttl)
1531
- else await redis.set(key, value);
1532
- },
1533
- delete: async (key) => {
1534
- await redis.del(key);
1535
- }
1536
- }
1537
- });
1538
- ```
1539
-
1540
- ### Express.js
1541
-
1542
- ```ts
1543
- import kv from 'polystore/express';
1544
-
1545
- // This is a special, Express-only store
1546
- const store = kv();
1547
-
1548
- app.use(session({ store, ... }));
1549
- ````
1550
-
1551
- ### Hono
1552
-
1553
- ### Elysia?
1554
-
1555
- ### Axios Cache Interceptor
1556
-
1557
1552
  ### @server/next
1558
1553
 
1559
1554
  > [!info]
package/src/express.js ADDED
@@ -0,0 +1,47 @@
1
+ import session from "express-session";
2
+ import kv from "../index.js";
3
+
4
+ const ttlFromSession = (data) => {
5
+ const maxAge = data?.cookie?.originalMaxAge;
6
+ return typeof maxAge === "number" ? Math.ceil(maxAge / 1000) : null;
7
+ };
8
+
9
+ export class PolystoreSessionStore extends session.Store {
10
+ constructor(store) {
11
+ super();
12
+ this.store = store;
13
+ }
14
+
15
+ prefix(prefix = "") {
16
+ return new PolystoreSessionStore(this.store.prefix(prefix));
17
+ }
18
+
19
+ get(sid, cb) {
20
+ this.store.get(sid).then((data) => cb(null, data)).catch(cb);
21
+ }
22
+
23
+ set(sid, data, cb) {
24
+ this.store
25
+ .set(sid, data, ttlFromSession(data))
26
+ .then(() => cb && cb())
27
+ .catch((error) => cb && cb(error));
28
+ }
29
+
30
+ destroy(sid, cb) {
31
+ this.store
32
+ .del(sid)
33
+ .then(() => cb && cb())
34
+ .catch((error) => cb && cb(error));
35
+ }
36
+
37
+ touch(sid, data, cb) {
38
+ this.store
39
+ .set(sid, data, ttlFromSession(data))
40
+ .then(() => cb && cb())
41
+ .catch((error) => cb && cb(error));
42
+ }
43
+ }
44
+
45
+ export default function expressStore(client = new Map()) {
46
+ return new PolystoreSessionStore(kv(client));
47
+ }