polystore 0.17.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,5 +1,8 @@
1
+ type Prefix = string;
2
+ type Expires = number | null | string;
1
3
  type Options = {
2
- expires?: number | null | string;
4
+ prefix?: Prefix;
5
+ expires?: Expires;
3
6
  };
4
7
  type StoreData<T extends Serializable = Serializable> = {
5
8
  value: T;
@@ -9,13 +12,14 @@ type Serializable = string | number | boolean | null | (Serializable | null)[] |
9
12
  [key: string]: Serializable | null;
10
13
  };
11
14
  interface ClientExpires {
12
- EXPIRES: true;
15
+ TYPE: string;
16
+ HAS_EXPIRATION: true;
13
17
  promise?: Promise<any>;
14
18
  test?: (client: any) => boolean;
15
19
  get<T extends Serializable>(key: string): Promise<T | null> | T | null;
16
- set<T extends Serializable>(key: string, value: T, options?: Options): Promise<any> | any;
20
+ set<T extends Serializable>(key: string, value: T, expires?: Expires): Promise<any> | any;
17
21
  iterate<T extends Serializable>(prefix: string): AsyncGenerator<[string, T], void, unknown> | Generator<[string, T], void, unknown>;
18
- add?<T extends Serializable>(prefix: string, value: T, options?: Options): Promise<string>;
22
+ add?<T extends Serializable>(prefix: string, value: T, expires?: Expires): Promise<string>;
19
23
  has?(key: string): Promise<boolean> | boolean;
20
24
  del?(key: string): Promise<any> | any;
21
25
  keys?(prefix: string): Promise<string[]> | string[];
@@ -27,13 +31,14 @@ interface ClientExpires {
27
31
  close?(): Promise<any> | any;
28
32
  }
29
33
  interface ClientNonExpires {
30
- EXPIRES: false;
34
+ TYPE: string;
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;
34
- set<T extends Serializable>(key: string, value: StoreData<T> | null, options?: Options): Promise<any> | any;
39
+ set<T extends Serializable>(key: string, value: StoreData<T> | null, ttl?: Expires): Promise<any> | any;
35
40
  iterate<T extends Serializable>(prefix: string): AsyncGenerator<[string, StoreData<T>], void, unknown> | Generator<[string, StoreData<T>], void, unknown>;
36
- add?<T extends Serializable>(prefix: string, value: StoreData<T>, options?: Options): Promise<string>;
41
+ add?<T extends Serializable>(prefix: string, value: StoreData<T>, ttl?: Expires): Promise<string>;
37
42
  has?(key: string): Promise<boolean> | boolean;
38
43
  del?(key: string): Promise<any> | any;
39
44
  keys?(prefix: string): Promise<string[]> | string[];
@@ -46,12 +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
- constructor(clientPromise?: any);
60
+ type: string;
61
+ constructor(clientPromise?: any, options?: Options);
55
62
  /**
56
63
  * Save the data on an autogenerated key, can add expiration as well:
57
64
  *
@@ -63,8 +70,8 @@ declare class Store<TDefault extends Serializable = Serializable> {
63
70
  *
64
71
  * **[→ Full .add() Docs](https://polystore.dev/documentation#add)**
65
72
  */
66
- add(value: TDefault, options?: Options): Promise<string>;
67
- add<T extends TDefault>(value: T, options?: Options): Promise<string>;
73
+ add(value: TD, options?: Options): Promise<string>;
74
+ add<T extends TD>(value: T, options?: Options): Promise<string>;
68
75
  /**
69
76
  * Save the data on the given key, can add expiration as well:
70
77
  *
@@ -76,8 +83,8 @@ declare class Store<TDefault extends Serializable = Serializable> {
76
83
  *
77
84
  * **[→ Full .set() Docs](https://polystore.dev/documentation#set)**
78
85
  */
79
- set(key: string, value: TDefault, options?: Options): Promise<string>;
80
- set<T extends TDefault>(key: string, value: T, options?: Options): 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>;
81
88
  /**
82
89
  * Read a single value from the KV store:
83
90
  *
@@ -92,8 +99,8 @@ declare class Store<TDefault extends Serializable = Serializable> {
92
99
  *
93
100
  * **[→ Full .get() Docs](https://polystore.dev/documentation#get)**
94
101
  */
95
- get(key: string): Promise<TDefault | null>;
96
- 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>;
97
104
  /**
98
105
  * Check whether a key exists or not:
99
106
  *
@@ -121,6 +128,17 @@ declare class Store<TDefault extends Serializable = Serializable> {
121
128
  * **[→ Full .del() Docs](https://polystore.dev/documentation#del)**
122
129
  */
123
130
  del(key: string): Promise<string>;
131
+ /**
132
+ * @alias of .del(key: string)
133
+ * Remove a single key and its value from the store:
134
+ *
135
+ * ```js
136
+ * const key = await store.delete("key1");
137
+ * ```
138
+ *
139
+ * **[→ Full .del() Docs](https://polystore.dev/documentation#del)**
140
+ */
141
+ delete(key: string): Promise<string>;
124
142
  /**
125
143
  * An iterator that goes through all of the key:value pairs in the client
126
144
  *
@@ -132,8 +150,8 @@ declare class Store<TDefault extends Serializable = Serializable> {
132
150
  *
133
151
  * **[→ Full Iterator Docs](https://polystore.dev/documentation#iterator)**
134
152
  */
135
- [Symbol.asyncIterator](): AsyncGenerator<[string, TDefault], void, unknown>;
136
- [Symbol.asyncIterator]<T extends TDefault>(): AsyncGenerator<[
153
+ [Symbol.asyncIterator](): AsyncGenerator<[string, TD], void, unknown>;
154
+ [Symbol.asyncIterator]<T extends TD>(): AsyncGenerator<[
137
155
  string,
138
156
  T
139
157
  ], void, unknown>;
@@ -150,8 +168,8 @@ declare class Store<TDefault extends Serializable = Serializable> {
150
168
  *
151
169
  * **[→ Full .entries() Docs](https://polystore.dev/documentation#entries)**
152
170
  */
153
- entries(): Promise<[string, TDefault][]>;
154
- entries<T extends TDefault>(): Promise<[string, T][]>;
171
+ entries(): Promise<[string, TD][]>;
172
+ entries<T extends TD>(): Promise<[string, T][]>;
155
173
  /**
156
174
  * Return an array of the keys in the store:
157
175
  *
@@ -179,8 +197,8 @@ declare class Store<TDefault extends Serializable = Serializable> {
179
197
  *
180
198
  * **[→ Full .values() Docs](https://polystore.dev/documentation#values)**
181
199
  */
182
- values(): Promise<TDefault[]>;
183
- values<T extends TDefault>(): Promise<T[]>;
200
+ values(): Promise<TD[]>;
201
+ values<T extends TD>(): Promise<T[]>;
184
202
  /**
185
203
  * Return an object with the keys:values in the store:
186
204
  *
@@ -194,20 +212,24 @@ declare class Store<TDefault extends Serializable = Serializable> {
194
212
  *
195
213
  * **[→ Full .all() Docs](https://polystore.dev/documentation#all)**
196
214
  */
197
- all(): Promise<Record<string, TDefault>>;
198
- all<T extends TDefault>(): Promise<Record<string, T>>;
215
+ all(): Promise<Record<string, TD>>;
216
+ all<T extends TD>(): Promise<Record<string, T>>;
199
217
  /**
200
- * Delete all of the records of the store:
218
+ * Create a substore where all the keys are stored with
219
+ * the given prefix:
201
220
  *
202
221
  * ```js
203
- * 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"]]
204
228
  * ```
205
229
  *
206
- * It's useful for cache invalidation, clearing the data, and testing.
207
- *
208
- * **[→ Full .clear() Docs](https://polystore.dev/documentation#clear)**
230
+ * **[→ Full .prefix() Docs](https://polystore.dev/documentation#prefix)**
209
231
  */
210
- clear(): Promise<void>;
232
+ prefix(prefix?: Prefix): Store<TD>;
211
233
  /**
212
234
  * Create a substore where all the keys are stored with
213
235
  * the given prefix:
@@ -223,7 +245,29 @@ declare class Store<TDefault extends Serializable = Serializable> {
223
245
  *
224
246
  * **[→ Full .prefix() Docs](https://polystore.dev/documentation#prefix)**
225
247
  */
226
- 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>;
227
271
  /**
228
272
  * Stop the connection to the store, if any:
229
273
  *
@@ -238,6 +282,6 @@ declare class Store<TDefault extends Serializable = Serializable> {
238
282
  close(): Promise<void>;
239
283
  }
240
284
  declare function createStore(): Store<Serializable>;
241
- 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>;
242
286
 
243
287
  export { type Client, type Serializable, Store, createStore as default };
package/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // src/clients/Client.ts
2
2
  var Client = class {
3
- EXPIRES = false;
3
+ TYPE;
4
+ HAS_EXPIRATION = false;
4
5
  client;
5
6
  encode = (val) => JSON.stringify(val, null, 2);
6
7
  decode = (val) => val ? JSON.parse(val) : null;
@@ -11,8 +12,9 @@ var Client = class {
11
12
 
12
13
  // src/clients/api.ts
13
14
  var Api = class extends Client {
15
+ TYPE = "API";
14
16
  // Indicate that the file handler DOES handle expirations
15
- EXPIRES = true;
17
+ HAS_EXPIRATION = true;
16
18
  static test = (client) => typeof client === "string" && /^https?:\/\//.test(client);
17
19
  #api = async (key, opts = "", method = "GET", body) => {
18
20
  const url = `${this.client.replace(/\/$/, "")}/${encodeURIComponent(key)}${opts}`;
@@ -25,7 +27,7 @@ var Api = class extends Client {
25
27
  return this.decode(await res.text());
26
28
  };
27
29
  get = (key) => this.#api(key);
28
- set = async (key, value, { expires } = {}) => {
30
+ set = async (key, value, expires) => {
29
31
  const exp = typeof expires === "number" ? `?expires=${expires}` : "";
30
32
  await this.#api(key, exp, "PUT", this.encode(value));
31
33
  };
@@ -45,15 +47,16 @@ var Api = class extends Client {
45
47
 
46
48
  // src/clients/cloudflare.ts
47
49
  var Cloudflare = class extends Client {
50
+ TYPE = "CLOUDFLARE";
48
51
  // It handles expirations natively
49
- EXPIRES = true;
50
- // Check whether the given store is a FILE-type
51
- static test = (client) => client?.constructor?.name === "KvNamespace" || client?.constructor?.name === "EdgeKVNamespace";
52
+ HAS_EXPIRATION = true;
53
+ static testKeys = ["getWithMetadata", "get", "list", "delete"];
52
54
  get = async (key) => {
53
- return this.decode(await this.client.get(key));
55
+ const value = await this.client.get(key);
56
+ return this.decode(value);
54
57
  };
55
- set = async (key, data, opts) => {
56
- const expirationTtl = opts.expires ? Math.round(opts.expires) : void 0;
58
+ set = async (key, data, expires) => {
59
+ const expirationTtl = expires ? Math.round(expires) : void 0;
57
60
  if (expirationTtl && expirationTtl < 60) {
58
61
  throw new Error("Cloudflare's min expiration is '60s'");
59
62
  }
@@ -93,8 +96,9 @@ var Cloudflare = class extends Client {
93
96
 
94
97
  // src/clients/cookie.ts
95
98
  var Cookie = class extends Client {
99
+ TYPE = "COOKIE";
96
100
  // It handles expirations natively
97
- EXPIRES = true;
101
+ HAS_EXPIRATION = true;
98
102
  // Check if this is the right class for the given client
99
103
  static test = (client) => {
100
104
  return client === "cookie" || client === "cookies";
@@ -118,7 +122,7 @@ var Cookie = class extends Client {
118
122
  const all = this.#read();
119
123
  return key in all ? all[key] : null;
120
124
  };
121
- set = (key, data, { expires }) => {
125
+ set = (key, data, expires) => {
122
126
  const k = encodeURIComponent(key);
123
127
  const value = encodeURIComponent(this.encode(data ?? ""));
124
128
  let exp = "";
@@ -128,7 +132,7 @@ var Cookie = class extends Client {
128
132
  }
129
133
  document.cookie = `${k}=${value}${exp}`;
130
134
  };
131
- del = (key) => this.set(key, "", { expires: -100 });
135
+ del = (key) => this.set(key, "", -100);
132
136
  async *iterate(prefix = "") {
133
137
  for (let [key, value] of Object.entries(this.#read())) {
134
138
  if (!key.startsWith(prefix)) continue;
@@ -139,10 +143,11 @@ var Cookie = class extends Client {
139
143
 
140
144
  // src/clients/etcd.ts
141
145
  var Etcd = class extends Client {
146
+ TYPE = "ETCD3";
142
147
  // It desn't handle expirations natively
143
- EXPIRES = false;
148
+ HAS_EXPIRATION = false;
144
149
  // Check if this is the right class for the given client
145
- static test = (client) => client?.constructor?.name === "Etcd3";
150
+ static testKeys = ["leaseClient", "watchClient", "watchManager"];
146
151
  get = async (key) => {
147
152
  const data = await this.client.get(key).json();
148
153
  return data;
@@ -157,14 +162,6 @@ var Etcd = class extends Client {
157
162
  yield [key, await this.get(key)];
158
163
  }
159
164
  }
160
- keys = (prefix = "") => {
161
- return this.client.getAll().prefix(prefix).keys();
162
- };
163
- entries = async (prefix = "") => {
164
- const keys = await this.keys(prefix);
165
- const values = await Promise.all(keys.map((k) => this.get(k)));
166
- return keys.map((k, i) => [k, values[i]]);
167
- };
168
165
  clear = async (prefix = "") => {
169
166
  if (!prefix) return this.client.delete().all();
170
167
  return this.client.delete().prefix(prefix);
@@ -173,8 +170,9 @@ var Etcd = class extends Client {
173
170
 
174
171
  // src/clients/file.ts
175
172
  var File = class extends Client {
173
+ TYPE = "FILE";
176
174
  // It desn't handle expirations natively
177
- EXPIRES = false;
175
+ HAS_EXPIRATION = false;
178
176
  fsp;
179
177
  file = "";
180
178
  #lock = Promise.resolve();
@@ -266,8 +264,9 @@ var noFileOk = (error) => {
266
264
  throw error;
267
265
  };
268
266
  var Folder = class extends Client {
267
+ TYPE = "FOLDER";
269
268
  // It desn't handle expirations natively
270
- EXPIRES = false;
269
+ HAS_EXPIRATION = false;
271
270
  fsp;
272
271
  folder;
273
272
  // Check if this is the right class for the given client
@@ -311,8 +310,9 @@ var Folder = class extends Client {
311
310
 
312
311
  // src/clients/forage.ts
313
312
  var Forage = class extends Client {
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);
@@ -344,10 +344,11 @@ var notFound = (error) => {
344
344
  throw error;
345
345
  };
346
346
  var Level = class extends Client {
347
+ TYPE = "LEVEL";
347
348
  // It desn't handle expirations natively
348
- EXPIRES = false;
349
+ HAS_EXPIRATION = false;
349
350
  // Check if this is the right class for the given client
350
- static test = (client) => client?.constructor?.name === "ClassicLevel";
351
+ static testKeys = ["attachResource", "detachResource", "prependOnceListener"];
351
352
  get = (key) => this.client.get(key, { valueEncoding }).catch(notFound);
352
353
  set = (key, value) => this.client.put(key, value, { valueEncoding });
353
354
  del = (key) => this.client.del(key);
@@ -378,8 +379,9 @@ var Level = class extends Client {
378
379
 
379
380
  // src/clients/memory.ts
380
381
  var Memory = class extends Client {
382
+ TYPE = "MEMORY";
381
383
  // It desn't handle expirations natively
382
- EXPIRES = false;
384
+ HAS_EXPIRATION = false;
383
385
  // Check if this is the right class for the given client
384
386
  static test = (client) => client instanceof Map;
385
387
  get = (key) => this.client.get(key) ?? null;
@@ -395,12 +397,13 @@ var Memory = class extends Client {
395
397
 
396
398
  // src/clients/redis.ts
397
399
  var Redis = class extends Client {
400
+ TYPE = "REDIS";
398
401
  // Indicate if this client handles expirations (true = it does)
399
- EXPIRES = true;
402
+ HAS_EXPIRATION = true;
400
403
  // Check if this is the right class for the given client
401
404
  static test = (client) => client && client.pSubscribe && client.sSubscribe;
402
405
  get = async (key) => this.decode(await this.client.get(key));
403
- set = async (key, value, { expires } = {}) => {
406
+ set = async (key, value, expires) => {
404
407
  const EX = expires ? Math.round(expires) : void 0;
405
408
  return this.client.set(key, this.encode(value), { EX });
406
409
  };
@@ -438,11 +441,12 @@ var Redis = class extends Client {
438
441
 
439
442
  // src/clients/sqlite.ts
440
443
  var SQLite = class extends Client {
444
+ TYPE = "SQLITE";
441
445
  // This one is doing manual time management internally even though
442
446
  // sqlite does not natively support expirations. This is because it does
443
447
  // support creating a `expires_at:Date` column that makes managing
444
448
  // expirations much easier, so it's really "somewhere in between"
445
- EXPIRES = true;
449
+ HAS_EXPIRATION = true;
446
450
  // The table name to use
447
451
  table = "kv";
448
452
  // Make sure the folder already exists, so attempt to create it
@@ -474,7 +478,7 @@ var SQLite = class extends Client {
474
478
  }
475
479
  return this.decode(row.value);
476
480
  };
477
- set = (id, data, { expires } = {}) => {
481
+ set = (id, data, expires) => {
478
482
  const value = this.encode(data);
479
483
  const expires_at = expires ? Date.now() + expires * 1e3 : null;
480
484
  this.client.prepare(
@@ -524,8 +528,9 @@ ${prefix ? "AND id LIKE ?" : ""}
524
528
 
525
529
  // src/clients/storage.ts
526
530
  var WebStorage = class extends Client {
531
+ TYPE = "STORAGE";
527
532
  // It desn't handle expirations natively
528
- EXPIRES = false;
533
+ HAS_EXPIRATION = false;
529
534
  // Check if this is the right class for the given client
530
535
  static test(client) {
531
536
  if (typeof Storage === "undefined") return false;
@@ -605,23 +610,36 @@ function unix(expires) {
605
610
  // src/index.ts
606
611
  var Store = class _Store {
607
612
  PREFIX = "";
613
+ EXPIRES = null;
608
614
  promise;
609
615
  client;
610
- constructor(clientPromise = /* @__PURE__ */ new Map()) {
616
+ type = "UNKNOWN";
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);
611
623
  this.promise = Promise.resolve(clientPromise).then(async (client) => {
612
624
  this.client = this.#find(client);
613
625
  this.#validate(this.client);
614
626
  this.promise = null;
615
627
  await this.client.promise;
628
+ this.type = this.client?.TYPE || this.type;
616
629
  return client;
617
630
  });
618
631
  }
619
632
  #find(store) {
620
633
  if (store instanceof _Store) return store.client;
621
634
  for (let client of Object.values(clients_default)) {
622
- if (client.test && client.test(store)) {
635
+ if ("test" in client && client.test(store)) {
623
636
  return new client(store);
624
637
  }
638
+ if ("testKeys" in client && typeof store === "object") {
639
+ if (client.testKeys.every((key) => store[key])) {
640
+ return new client(store);
641
+ }
642
+ }
625
643
  }
626
644
  if (typeof store === "function" && /^class\s/.test(Function.prototype.toString.call(store))) {
627
645
  return new store();
@@ -633,7 +651,7 @@ var Store = class _Store {
633
651
  if (!client.set || !client.get || !client.iterate) {
634
652
  throw new Error("Client should have .get(), .set() and .iterate()");
635
653
  }
636
- if (client.EXPIRES) return;
654
+ if (client.HAS_EXPIRATION) return;
637
655
  for (let method of ["has", "keys", "values"]) {
638
656
  if (client[method]) {
639
657
  const msg = `You can only define client.${method}() when the client manages the expiration.`;
@@ -641,50 +659,48 @@ var Store = class _Store {
641
659
  }
642
660
  }
643
661
  }
644
- // Check if the given data is fresh or not; if
662
+ // Check if the given data is fresh or not
645
663
  #isFresh(data, key) {
646
664
  if (!data || typeof data !== "object" || !("value" in data)) {
647
- if (key) this.del(key);
648
665
  return false;
649
666
  }
650
- if (data.expires === null) return true;
651
- if (data.expires > Date.now()) return true;
652
- if (key) this.del(key);
653
- return false;
667
+ return data.expires === null || data.expires > Date.now();
654
668
  }
655
- async add(value, options = {}) {
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) {
656
674
  await this.promise;
657
- let expires = parse(options.expires);
675
+ const expires = this.#expiration(options?.expires);
676
+ const prefix = options?.prefix || this.PREFIX;
658
677
  if (this.client.add) {
659
- if (this.client.EXPIRES) {
660
- return await this.client.add(this.PREFIX, value, { expires });
678
+ if (this.client.HAS_EXPIRATION) {
679
+ return this.client.add(prefix, value, expires);
661
680
  }
662
- expires = unix(expires);
663
- const key2 = await this.client.add(this.PREFIX, { expires, value });
664
- return key2;
681
+ return this.client.add(prefix, { expires: unix(expires), value });
665
682
  }
666
- const key = createId();
667
- return this.set(key, value, { expires });
683
+ return this.set(createId(), value, { prefix, expires });
668
684
  }
669
- async set(key, value, options = {}) {
685
+ async set(key, value, options) {
670
686
  await this.promise;
671
- const id = this.PREFIX + key;
672
- let expires = parse(options.expires);
687
+ const expires = this.#expiration(options?.expires);
688
+ const prefix = options?.prefix || this.PREFIX;
689
+ const id = prefix + key;
673
690
  if (value === null || typeof expires === "number" && expires <= 0) {
674
691
  return this.del(key);
675
692
  }
676
- if (this.client.EXPIRES) {
677
- await this.client.set(id, value, { expires });
693
+ if (this.client.HAS_EXPIRATION) {
694
+ await this.client.set(id, value, expires);
678
695
  return key;
679
696
  }
680
- expires = unix(expires);
681
- await this.client.set(id, { expires, value });
697
+ await this.client.set(id, { expires: unix(expires), value });
682
698
  return key;
683
699
  }
684
700
  async get(key) {
685
701
  await this.promise;
686
702
  const id = this.PREFIX + key;
687
- if (this.client.EXPIRES) {
703
+ if (this.client.HAS_EXPIRATION) {
688
704
  const data = await this.client.get(id) ?? null;
689
705
  if (data === null) return null;
690
706
  return data;
@@ -735,16 +751,29 @@ var Store = class _Store {
735
751
  await this.client.del(id);
736
752
  return key;
737
753
  }
738
- if (this.client.EXPIRES) {
739
- await this.client.set(id, null, { expires: 0 });
754
+ if (this.client.HAS_EXPIRATION) {
755
+ await this.client.set(id, null, 0);
740
756
  } else {
741
757
  await this.client.set(id, null);
742
758
  }
743
759
  return key;
744
760
  }
761
+ /**
762
+ * @alias of .del(key: string)
763
+ * Remove a single key and its value from the store:
764
+ *
765
+ * ```js
766
+ * const key = await store.delete("key1");
767
+ * ```
768
+ *
769
+ * **[→ Full .del() Docs](https://polystore.dev/documentation#del)**
770
+ */
771
+ async delete(key) {
772
+ return this.del(key);
773
+ }
745
774
  async *[Symbol.asyncIterator]() {
746
775
  await this.promise;
747
- if (this.client.EXPIRES) {
776
+ if (this.client.HAS_EXPIRATION) {
748
777
  for await (const [name, data] of this.client.iterate(this.PREFIX)) {
749
778
  const key = name.slice(this.PREFIX.length);
750
779
  yield [key, data];
@@ -762,7 +791,7 @@ var Store = class _Store {
762
791
  await this.promise;
763
792
  const trim = (key) => key.slice(this.PREFIX.length);
764
793
  if (this.client.entries) {
765
- if (this.client.EXPIRES) {
794
+ if (this.client.HAS_EXPIRATION) {
766
795
  const entries = await this.client.entries(this.PREFIX);
767
796
  return entries.map(([k, v]) => [trim(k), v]);
768
797
  } else {
@@ -770,7 +799,7 @@ var Store = class _Store {
770
799
  return entries.map(([k, v]) => [trim(k), v]).filter(([key, data]) => this.#isFresh(data, key)).map(([key, data]) => [key, data.value]);
771
800
  }
772
801
  }
773
- if (this.client.EXPIRES) {
802
+ if (this.client.HAS_EXPIRATION) {
774
803
  const list = [];
775
804
  for await (const [k, v] of this.client.iterate(this.PREFIX)) {
776
805
  list.push([trim(k), v]);
@@ -812,7 +841,7 @@ var Store = class _Store {
812
841
  async values() {
813
842
  await this.promise;
814
843
  if (this.client.values) {
815
- if (this.client.EXPIRES) return this.client.values(this.PREFIX);
844
+ if (this.client.HAS_EXPIRATION) return this.client.values(this.PREFIX);
816
845
  const list = await this.client.values(this.PREFIX);
817
846
  return list.filter((data) => this.#isFresh(data)).map((data) => data.value);
818
847
  }
@@ -823,6 +852,52 @@ var Store = class _Store {
823
852
  const entries = await this.entries();
824
853
  return Object.fromEntries(entries);
825
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
+ }
826
901
  /**
827
902
  * Delete all of the records of the store:
828
903
  *
@@ -846,26 +921,25 @@ var Store = class _Store {
846
921
  await Promise.all(keys.map((key) => this.del(key)));
847
922
  }
848
923
  /**
849
- * Create a substore where all the keys are stored with
850
- * the given prefix:
924
+ * Remove all expired records from the store.
851
925
  *
852
926
  * ```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"]]
927
+ * await store.prune();
859
928
  * ```
860
929
  *
861
- * **[→ Full .prefix() Docs](https://polystore.dev/documentation#prefix)**
930
+ * Only affects stores where expiration is managed by this wrapper.
862
931
  */
863
- prefix(prefix = "") {
864
- const store = new _Store(
865
- Promise.resolve(this.promise).then(() => this.client)
866
- );
867
- store.PREFIX = this.PREFIX + prefix;
868
- 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
+ }
869
943
  }
870
944
  /**
871
945
  * Stop the connection to the store, if any:
@@ -885,8 +959,8 @@ var Store = class _Store {
885
959
  }
886
960
  }
887
961
  };
888
- function createStore(client) {
889
- return new Store(client);
962
+ function createStore(client, options) {
963
+ return new Store(client, options);
890
964
  }
891
965
  export {
892
966
  createStore as default
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "polystore",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "A small compatibility layer for many popular KV stores like localStorage, Redis, FileSystem, etc.",
5
- "homepage": "https://polystore.dev/",
5
+ "homepage": "https://polystore.dev",
6
6
  "repository": "https://github.com/franciscop/polystore.git",
7
7
  "bugs": "https://github.com/franciscop/polystore/issues",
8
8
  "funding": "https://www.paypal.me/franciscopresencia/19",
@@ -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
@@ -1,6 +1,6 @@
1
1
  # Polystore [![npm install polystore](https://img.shields.io/badge/npm%20install-polystore-blue.svg)](https://www.npmjs.com/package/polystore) [![test badge](https://github.com/franciscop/polystore/workflows/tests/badge.svg "test badge")](https://github.com/franciscop/polystore/blob/master/.github/workflows/tests.yml) [![gzip size](https://badgen.net/bundlephobia/minzip/polystore?label=gzip&color=green)](https://bundlephobia.com/package/polystore)
2
2
 
3
- A key-value library to unify the API of [many clients](#clients), like localStorage, Redis, FileSystem, etc:
3
+ A key-value library to unify the API of [many clients](#clients): localStorage, Redis, FileSystem, SQLite, etc.
4
4
 
5
5
  ```js
6
6
  import kv from "polystore";
@@ -18,14 +18,16 @@ These are all the methods of the [API](#api) (they are all `async`):
18
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", { expires: '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,8 +220,12 @@ 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, options?: { 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
231
  await store.set("key2", ["my", "grocery", "list"], { expires: "1h" });
@@ -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, options?: { 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
305
  const key2 = await store.add(["my", "grocery", "list"], { expires: "1h" });
299
- const key3 = await store.add({ name: "Francisco" }, { expires: 60 * 60 });
306
+ const key3 = await store.add({ name: "Francisco" }, { expires: 60 * 60 });
300
307
  ```
301
308
 
302
- The options and details 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:
@@ -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"`.
@@ -617,7 +675,7 @@ console.log(await store.get("key1"));
617
675
  // GOOD - with polystore
618
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
@@ -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
 
@@ -867,14 +925,14 @@ 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";
@@ -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,7 +1222,7 @@ 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());
1225
+ const store = kv(new Map());
1168
1226
  await store.set("a", "b", { expires: "1s" });
1169
1227
 
1170
1228
  // These checks of course work:
@@ -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,15 +1288,15 @@ 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
- set (key, value, { expires: null|number }): Promise<null>;
1238
- iterate(prefix): AyncIterator<[string, any]>
1295
+ set (key, value, null|number): Promise<null>;
1296
+ iterate(prefix): AsyncIterator<[string, any]>
1239
1297
 
1240
1298
  // Optional item methods (for optimization or customization)
1241
- add (prefix, data, { expires: null|number }): Promise<string>;
1299
+ add (prefix, data, null|number): Promise<string>;
1242
1300
  has (key): Promise<boolean>;
1243
1301
  del (key): Promise<null>;
1244
1302
 
@@ -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
 
@@ -1283,7 +1341,7 @@ client.keys = (prefix) => {
1283
1341
 
1284
1342
  While the signatures are different, you can check each entries on the output of Polystore API to see what is expected for the methods of the client to do, e.g. `.clear()` will remove all of the items that match the prefix (or everything if there's no prefix).
1285
1343
 
1286
- ### Example: Plain Object client
1344
+ ### Plain Object client
1287
1345
 
1288
1346
  This is a good example of how simple a store can be, however do not use it literally since it behaves the same as the already-supported `new Map()`, only use it as the base for your own clients:
1289
1347
 
@@ -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,6 +1497,74 @@ class CloudflareCustom {
1439
1497
  }
1440
1498
 
1441
1499
  const store = kv(CloudflareCustom);
1442
- ````
1500
+ ```
1501
+
1502
+ It's lacking a few things, so make sure to adapt to your needs, but it worked for my very simple cache needs.
1503
+
1504
+
1505
+ ## Examples
1506
+
1507
+ ### Simple cache
1508
+
1509
+ I've used Polystore in many projects as a simple cache. With `fetch()`, it's fairly easy:
1510
+
1511
+ ```ts
1512
+ async function getProductInfo(id: string) {
1513
+ const data = await store.get(id);
1514
+ if (data) return data;
1515
+
1516
+ const res = await fetch(`https://some-url.com/products/${id}`);
1517
+ const raw = await res.json();
1518
+
1519
+ // Some processing here
1520
+ const clean = raw??;
1521
+
1522
+ await store.set(id, clean, { expires: "10days" });
1523
+ return clean;
1524
+ }
1525
+ ```
1526
+
1527
+ You can store either the raw data, or the processed data. Depending on whether the processing is sync or async, and the data size of the raw vs processing data, we pick one or the other.
1443
1528
 
1444
- It's lacking few things, so make sure to adapt to your needs, but it worked for my very simple cache needs.
1529
+
1530
+
1531
+ ### Dev vs Prod
1532
+
1533
+ With Polystore it's easy to configure your KV solution to use a different client in dev vs production. We've found particularly useful to use an easy-to-debug client in dev like [Folder](#folder) and a high-performance client in production like [Redis](#redis):
1534
+
1535
+ ```ts
1536
+ // store.ts
1537
+ import kv from "polystore";
1538
+ import { createClient } from "redis";
1539
+
1540
+ let store;
1541
+ if (process.env.REDIS_URL) {
1542
+ console.log('kv:redis using Redis for cache data');
1543
+ store = kv(createClient(process.env.REDIS_URL).connect());
1544
+ } else {
1545
+ console.log('kv:folder using a folder for cache data');
1546
+ store = kv(`file://${process.cwd()}/data/`);
1547
+ }
1548
+
1549
+ export default store;
1550
+ ```
1551
+
1552
+ ### @server/next
1553
+
1554
+ > [!info]
1555
+ > @server/next is still experimental, but it's the main reason I created Polystore and so I wanted to document it as well
1556
+
1557
+ Server.js supports Polystore directly:
1558
+
1559
+ ```ts
1560
+ import kv from "polystore";
1561
+ import server from "../../";
1562
+
1563
+ const session = kv(new Map());
1564
+
1565
+ export default server({ session }).get("/", (ctx) => {
1566
+ if (!ctx.session.counter) ctx.session.counter = 0;
1567
+ ctx.session.counter++;
1568
+ return `User visited ${ctx.session.counter} times`;
1569
+ });
1570
+ ```
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
+ }