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