polystore 0.19.0 → 0.20.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 +1 -0
- package/index.js +115 -26
- package/package.json +5 -1
- package/readme.md +85 -23
package/index.d.ts
CHANGED
|
@@ -45,6 +45,7 @@ interface ClientNonExpires {
|
|
|
45
45
|
values?<T extends Serializable>(prefix: string): Promise<StoreData<T>[]> | StoreData<T>[];
|
|
46
46
|
entries?<T extends Serializable>(prefix: string): Promise<[string, StoreData<T>][]> | [string, StoreData<T>][];
|
|
47
47
|
all?<T extends Serializable>(prefix: string): Promise<Record<string, StoreData<T>>> | Record<string, StoreData<T>>;
|
|
48
|
+
prune?(): Promise<any> | any;
|
|
48
49
|
clear?(prefix: string): Promise<any> | any;
|
|
49
50
|
clearAll?(): Promise<any> | any;
|
|
50
51
|
close?(): Promise<any> | any;
|
package/index.js
CHANGED
|
@@ -244,8 +244,10 @@ var File = class extends Client {
|
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
246
|
// Bulk updates are worth creating a custom method here
|
|
247
|
-
clearAll = () => this.#withLock(() => this.#write({}));
|
|
248
247
|
clear = async (prefix = "") => {
|
|
248
|
+
if (!prefix) {
|
|
249
|
+
await this.#withLock(() => this.#write({}));
|
|
250
|
+
}
|
|
249
251
|
return this.#withLock(async () => {
|
|
250
252
|
const data = await this.#read();
|
|
251
253
|
for (let key in data) {
|
|
@@ -366,8 +368,10 @@ var Level = class extends Client {
|
|
|
366
368
|
list.map(async (k) => [k, await this.get(k)])
|
|
367
369
|
);
|
|
368
370
|
};
|
|
369
|
-
clearAll = () => this.client.clear();
|
|
370
371
|
clear = async (prefix = "") => {
|
|
372
|
+
if (!prefix) {
|
|
373
|
+
return await this.client.clear();
|
|
374
|
+
}
|
|
371
375
|
const keys = await this.client.keys().all();
|
|
372
376
|
const list = keys.filter((k) => k.startsWith(prefix));
|
|
373
377
|
return this.client.batch(
|
|
@@ -395,6 +399,97 @@ var Memory = class extends Client {
|
|
|
395
399
|
clearAll = () => this.client.clear();
|
|
396
400
|
};
|
|
397
401
|
|
|
402
|
+
// src/clients/postgres.ts
|
|
403
|
+
var Postgres = class extends Client {
|
|
404
|
+
TYPE = "POSTGRES";
|
|
405
|
+
// This one is doing manual time management internally even though
|
|
406
|
+
// sqlite does not natively support expirations. This is because it does
|
|
407
|
+
// support creating a `expires_at:Date` column that makes managing
|
|
408
|
+
// expirations much easier, so it's really "somewhere in between"
|
|
409
|
+
HAS_EXPIRATION = true;
|
|
410
|
+
// The table name to use
|
|
411
|
+
table = "kv";
|
|
412
|
+
// Ensure schema exists before any operation
|
|
413
|
+
promise = (async () => {
|
|
414
|
+
if (!/^[a-zA-Z_]+$/.test(this.table)) {
|
|
415
|
+
throw new Error(`Invalid table name ${this.table}`);
|
|
416
|
+
}
|
|
417
|
+
await this.client.query(`
|
|
418
|
+
CREATE TABLE IF NOT EXISTS ${this.table} (
|
|
419
|
+
id TEXT PRIMARY KEY,
|
|
420
|
+
value TEXT NOT NULL,
|
|
421
|
+
expires_at TIMESTAMPTZ
|
|
422
|
+
)
|
|
423
|
+
`);
|
|
424
|
+
await this.client.query(
|
|
425
|
+
`CREATE INDEX IF NOT EXISTS idx_${this.table}_expires_at ON ${this.table} (expires_at)`
|
|
426
|
+
);
|
|
427
|
+
})();
|
|
428
|
+
static test = (client) => {
|
|
429
|
+
return client && client.query && !client.filename;
|
|
430
|
+
};
|
|
431
|
+
get = async (id) => {
|
|
432
|
+
const result = await this.client.query(
|
|
433
|
+
`SELECT value
|
|
434
|
+
FROM ${this.table}
|
|
435
|
+
WHERE id = $1 AND (expires_at IS NULL OR expires_at > NOW())`,
|
|
436
|
+
[id]
|
|
437
|
+
);
|
|
438
|
+
if (!result.rows.length) return null;
|
|
439
|
+
return this.decode(result.rows[0].value);
|
|
440
|
+
};
|
|
441
|
+
set = async (id, data, expires) => {
|
|
442
|
+
const value = this.encode(data);
|
|
443
|
+
const expires_at = expires ? new Date(Date.now() + expires * 1e3) : null;
|
|
444
|
+
await this.client.query(
|
|
445
|
+
`INSERT INTO ${this.table} (id, value, expires_at)
|
|
446
|
+
VALUES ($1, $2, $3)
|
|
447
|
+
ON CONFLICT (id) DO UPDATE
|
|
448
|
+
SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at`,
|
|
449
|
+
[id, value, expires_at]
|
|
450
|
+
);
|
|
451
|
+
};
|
|
452
|
+
del = async (id) => {
|
|
453
|
+
await this.client.query(`DELETE FROM ${this.table} WHERE id = $1`, [id]);
|
|
454
|
+
};
|
|
455
|
+
async *iterate(prefix = "") {
|
|
456
|
+
const result = await this.client.query(
|
|
457
|
+
`SELECT id, value FROM ${this.table}
|
|
458
|
+
WHERE (expires_at IS NULL OR expires_at > NOW()) ${prefix ? `AND id LIKE $1` : ""}`,
|
|
459
|
+
prefix ? [`${prefix}%`] : []
|
|
460
|
+
);
|
|
461
|
+
for (const row of result.rows) {
|
|
462
|
+
yield [row.id, this.decode(row.value)];
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
async keys(prefix = "") {
|
|
466
|
+
const result = await this.client.query(
|
|
467
|
+
`SELECT id FROM ${this.table}
|
|
468
|
+
WHERE (expires_at IS NULL OR expires_at > NOW())
|
|
469
|
+
${prefix ? `AND id LIKE $1` : ""}`,
|
|
470
|
+
prefix ? [`${prefix}%`] : []
|
|
471
|
+
);
|
|
472
|
+
return result.rows.map((r) => r.id);
|
|
473
|
+
}
|
|
474
|
+
prune = async () => {
|
|
475
|
+
await this.client.query(
|
|
476
|
+
`DELETE FROM ${this.table}
|
|
477
|
+
WHERE expires_at IS NOT NULL AND expires_at <= NOW()`
|
|
478
|
+
);
|
|
479
|
+
};
|
|
480
|
+
clear = async (prefix = "") => {
|
|
481
|
+
await this.client.query(
|
|
482
|
+
`DELETE FROM ${this.table} ${prefix ? `WHERE id LIKE $1` : ""}`,
|
|
483
|
+
prefix ? [`${prefix}%`] : []
|
|
484
|
+
);
|
|
485
|
+
};
|
|
486
|
+
close = async () => {
|
|
487
|
+
if (this.client.end) {
|
|
488
|
+
await this.client.end();
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
};
|
|
492
|
+
|
|
398
493
|
// src/clients/redis.ts
|
|
399
494
|
var Redis = class extends Client {
|
|
400
495
|
TYPE = "REDIS";
|
|
@@ -470,13 +565,11 @@ var SQLite = class extends Client {
|
|
|
470
565
|
return typeof client?.prepare === "function" && typeof client?.exec === "function";
|
|
471
566
|
};
|
|
472
567
|
get = (id) => {
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
}
|
|
479
|
-
return this.decode(row.value);
|
|
568
|
+
const value = this.client.prepare(
|
|
569
|
+
`SELECT value, expires_at FROM kv WHERE id = ? AND (expires_at IS NULL OR expires_at > ?)`
|
|
570
|
+
).get(id, Date.now())?.value;
|
|
571
|
+
if (!value) return null;
|
|
572
|
+
return this.decode(value);
|
|
480
573
|
};
|
|
481
574
|
set = (id, data, expires) => {
|
|
482
575
|
const value = this.encode(data);
|
|
@@ -485,8 +578,8 @@ var SQLite = class extends Client {
|
|
|
485
578
|
`INSERT INTO kv (id, value, expires_at) VALUES (?, ?, ?) ON CONFLICT(id) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at`
|
|
486
579
|
).run(id, value, expires_at);
|
|
487
580
|
};
|
|
488
|
-
del =
|
|
489
|
-
|
|
581
|
+
del = (id) => {
|
|
582
|
+
this.client.prepare(`DELETE FROM kv WHERE id = ?`).run(id);
|
|
490
583
|
};
|
|
491
584
|
has = (id) => {
|
|
492
585
|
const row = this.client.prepare(`SELECT expires_at FROM kv WHERE id = ?`).get(id);
|
|
@@ -498,7 +591,6 @@ var SQLite = class extends Client {
|
|
|
498
591
|
return true;
|
|
499
592
|
};
|
|
500
593
|
*iterate(prefix = "") {
|
|
501
|
-
this.#clearExpired();
|
|
502
594
|
const sql = `SELECT id, value FROM kv WHERE (expires_at IS NULL OR expires_at > ?) ${prefix ? "AND id LIKE ?" : ""}
|
|
503
595
|
`;
|
|
504
596
|
const params = prefix ? [Date.now(), `${prefix}%`] : [Date.now()];
|
|
@@ -507,7 +599,6 @@ var SQLite = class extends Client {
|
|
|
507
599
|
}
|
|
508
600
|
}
|
|
509
601
|
keys = (prefix = "") => {
|
|
510
|
-
this.#clearExpired();
|
|
511
602
|
const sql = `SELECT id FROM kv WHERE (expires_at IS NULL OR expires_at > ?)
|
|
512
603
|
${prefix ? "AND id LIKE ?" : ""}
|
|
513
604
|
`;
|
|
@@ -515,11 +606,15 @@ ${prefix ? "AND id LIKE ?" : ""}
|
|
|
515
606
|
const rows = this.client.prepare(sql).all(...params);
|
|
516
607
|
return rows.map((r) => r.id);
|
|
517
608
|
};
|
|
518
|
-
|
|
519
|
-
this.client.prepare(`DELETE FROM kv WHERE expires_at
|
|
609
|
+
prune = () => {
|
|
610
|
+
this.client.prepare(`DELETE FROM kv WHERE expires_at <= ?`).run(Date.now());
|
|
520
611
|
};
|
|
521
|
-
|
|
522
|
-
|
|
612
|
+
clear = (prefix = "") => {
|
|
613
|
+
if (!prefix) {
|
|
614
|
+
this.client.prepare(`DELETE FROM ${this.table}`).run();
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
this.client.prepare(`DELETE FROM ${this.table} WHERE id LIKE ?`).run(`${prefix}%`);
|
|
523
618
|
};
|
|
524
619
|
close = () => {
|
|
525
620
|
this.client.close?.();
|
|
@@ -564,8 +659,7 @@ var clients_default = {
|
|
|
564
659
|
forage: Forage,
|
|
565
660
|
level: Level,
|
|
566
661
|
memory: Memory,
|
|
567
|
-
|
|
568
|
-
// prisma,
|
|
662
|
+
postgres: Postgres,
|
|
569
663
|
redis: Redis,
|
|
570
664
|
storage: WebStorage,
|
|
571
665
|
sqlite: SQLite
|
|
@@ -932,13 +1026,8 @@ var Store = class _Store {
|
|
|
932
1026
|
async prune() {
|
|
933
1027
|
await this.promise;
|
|
934
1028
|
if (this.client.HAS_EXPIRATION) return;
|
|
935
|
-
|
|
936
|
-
this.
|
|
937
|
-
)) {
|
|
938
|
-
const key = name.slice(this.PREFIX.length);
|
|
939
|
-
if (!this.#isFresh(data, key)) {
|
|
940
|
-
await this.del(key);
|
|
941
|
-
}
|
|
1029
|
+
if (this.client.prune) {
|
|
1030
|
+
await this.client.prune();
|
|
942
1031
|
}
|
|
943
1032
|
}
|
|
944
1033
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polystore",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.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",
|
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
"test:bun": "bun test ./test/index.test.ts",
|
|
33
33
|
"test:jest": "jest ./test/index.test.ts --detectOpenHandles --forceExit",
|
|
34
34
|
"run:db": "etcd",
|
|
35
|
+
"run:redis": "brew services start redis",
|
|
36
|
+
"run:postgres": "brew services start postgresql",
|
|
35
37
|
"run:server": "bun ./src/server.ts"
|
|
36
38
|
},
|
|
37
39
|
"keywords": [
|
|
@@ -49,6 +51,7 @@
|
|
|
49
51
|
"@types/bun": "^1.3.3",
|
|
50
52
|
"@types/jest": "^30.0.0",
|
|
51
53
|
"@types/jsdom": "^27.0.0",
|
|
54
|
+
"@types/pg": "^8.11.10",
|
|
52
55
|
"better-sqlite3": "^12.6.0",
|
|
53
56
|
"check-dts": "^0.8.0",
|
|
54
57
|
"cross-fetch": "^4.1.0",
|
|
@@ -61,6 +64,7 @@
|
|
|
61
64
|
"jsdom": "^27.2.0",
|
|
62
65
|
"level": "^8.0.1",
|
|
63
66
|
"localforage": "^1.10.0",
|
|
67
|
+
"pg": "^8.13.1",
|
|
64
68
|
"redis": "^4.6.10",
|
|
65
69
|
"ts-jest": "^29.4.6",
|
|
66
70
|
"ts-node": "^10.9.2",
|
package/readme.md
CHANGED
|
@@ -631,6 +631,25 @@ A client is the library that manages the low-level store operations. For example
|
|
|
631
631
|
|
|
632
632
|
Polystore provides a unified API you can use `Promises`, `expires` and `.prefix()` even with those stores that do not support these operations natively.
|
|
633
633
|
|
|
634
|
+
Quick overview:
|
|
635
|
+
|
|
636
|
+
| Client | Runtime | Persistence | Native expiration | Notes |
|
|
637
|
+
|---|---|---|---|---|
|
|
638
|
+
| [Memory](#memory) | Node.js + Browser | ❌ | ❌ | Great for tests and ephemeral caches |
|
|
639
|
+
| [Local Storage](#local-storage) | Browser | ✅ | ❌ | Persistent browser storage |
|
|
640
|
+
| [Session Storage](#session-storage) | Browser | ❌ | ❌ | Cleared when tab/session ends |
|
|
641
|
+
| [Cookies](#cookies) | Browser | ✅ | ✅ | Browser-side cookies |
|
|
642
|
+
| [Local Forage](#local-forage) | Browser | ✅ | ❓ | Better capacity than localStorage |
|
|
643
|
+
| [Redis](#redis) | Node.js | ✅ | ✅ | Good distributed cache backend |
|
|
644
|
+
| [SQLite](#sqlite) | Node.js | ✅ | ❌ | Simple local persistence |
|
|
645
|
+
| [Fetch API](#fetch-api) | Any with `fetch` | ❓ | ❓ | Bring your own KV HTTP API |
|
|
646
|
+
| [File](#file) | Node.js | ✅ | ❌ | Single JSON file store |
|
|
647
|
+
| [Folder](#folder) | Node.js | ✅ | ❌ | One-file-per-key store |
|
|
648
|
+
| [Cloudflare KV](#cloudflare-kv) | Cloudflare | ✅ | ✅ | Edge-native KV |
|
|
649
|
+
| [Level](#level) | Node.js | ✅ | ❌ | Uses Level ecosystem |
|
|
650
|
+
| [Etcd](#etcd) | Node.js | ✅ | ✅ | Distributed KV |
|
|
651
|
+
| [Postgres](#postgres) | Node.js | ✅ | ❌ | Table-backed KV |
|
|
652
|
+
|
|
634
653
|
While you can keep a reference to the client and access it directly, we strongly recommend to only access it through `polystore`, since we might add custom serialization and extra properties for e.g. expiration time:
|
|
635
654
|
|
|
636
655
|
```js
|
|
@@ -1164,28 +1183,32 @@ console.log(await store.get("key1"));
|
|
|
1164
1183
|
// "Hello world"
|
|
1165
1184
|
```
|
|
1166
1185
|
|
|
1167
|
-
|
|
1186
|
+
Polystore will initialize the schema automatically: it creates the `kv` table and expiration index if they do not exist yet, and does not fail if they already exist.
|
|
1168
1187
|
|
|
1169
|
-
|
|
1188
|
+
Required schema (auto-created by Polystore):
|
|
1170
1189
|
|
|
1171
1190
|
```sql
|
|
1172
|
-
CREATE TABLE kv (
|
|
1191
|
+
CREATE TABLE IF NOT EXISTS kv (
|
|
1173
1192
|
id TEXT PRIMARY KEY,
|
|
1174
1193
|
value TEXT NOT NULL,
|
|
1175
1194
|
"expiresAt" TIMESTAMP
|
|
1176
1195
|
);
|
|
1196
|
+
|
|
1197
|
+
CREATE INDEX IF NOT EXISTS idx_kv_expiresAt
|
|
1198
|
+
ON kv ("expiresAt");
|
|
1177
1199
|
```
|
|
1178
1200
|
|
|
1179
|
-
The default table name is `kv
|
|
1201
|
+
The default table name is `kv`. Key prefixes still work as normal key namespaces:
|
|
1180
1202
|
|
|
1181
1203
|
```js
|
|
1182
|
-
const sessions = store.prefix("session:");
|
|
1183
|
-
const cache = store.prefix("cache:");
|
|
1204
|
+
const sessions = store.prefix("session:");
|
|
1205
|
+
const cache = store.prefix("cache:");
|
|
1184
1206
|
|
|
1185
1207
|
await sessions.set("user123", { name: "Alice" });
|
|
1208
|
+
// Stored key in Postgres: "session:user123"
|
|
1186
1209
|
```
|
|
1187
1210
|
|
|
1188
|
-
This
|
|
1211
|
+
This keeps a single table while preserving namespace-style grouping through prefixed keys.
|
|
1189
1212
|
|
|
1190
1213
|
<details>
|
|
1191
1214
|
<summary>Why use polystore with Postgres?</summary>
|
|
@@ -1202,7 +1225,9 @@ This maps prefixes to table names for better performance on group operations.
|
|
|
1202
1225
|
|
|
1203
1226
|
Please see the [creating a store](#creating-a-store) section for all the details!
|
|
1204
1227
|
|
|
1205
|
-
##
|
|
1228
|
+
## Guides
|
|
1229
|
+
|
|
1230
|
+
### Performance
|
|
1206
1231
|
|
|
1207
1232
|
> TL;DR: if you only use the item operations (add, set, get, has, del) and your client supports expiration natively, you have nothing to worry about! Otherwise, please read on.
|
|
1208
1233
|
|
|
@@ -1214,7 +1239,7 @@ While all of our stores support `expires`, `.prefix()` and group operations, the
|
|
|
1214
1239
|
|
|
1215
1240
|
**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.
|
|
1216
1241
|
|
|
1217
|
-
|
|
1242
|
+
### Expirations
|
|
1218
1243
|
|
|
1219
1244
|
> Warning: if a client doesn't support expiration natively, we will hide expired keys on the API calls for a nice DX, but _old data might not be evicted automatically_. See [the notes in Performance](#performance) for details on how to work around this.
|
|
1220
1245
|
|
|
@@ -1258,7 +1283,7 @@ These are all the units available:
|
|
|
1258
1283
|
|
|
1259
1284
|
This is great because with polystore we do ensure that if a key has expired, it doesn't show up in `.keys()`, `.entries()`, `.values()`, `.has()` or `.get()`.
|
|
1260
1285
|
|
|
1261
|
-
|
|
1286
|
+
#### Eviction
|
|
1262
1287
|
|
|
1263
1288
|
However, in some stores this does come with some potential performance disadvantages. For example, both the in-memory example above and localStorage _don't_ have a native expiration/eviction process, so we have to store that information as metadata, meaning that even to check if a key exists we need to read and decode its value. For one or few keys it's not a problem, but for large sets this can become an issue.
|
|
1264
1289
|
|
|
@@ -1266,7 +1291,7 @@ For other stores like Redis this is not a problem, because the low-level operati
|
|
|
1266
1291
|
|
|
1267
1292
|
These details are explained in the respective client information.
|
|
1268
1293
|
|
|
1269
|
-
|
|
1294
|
+
### Substores
|
|
1270
1295
|
|
|
1271
1296
|
> There's some [basic `.prefix()` API info](#prefix) for everyday usage, this section is the in-depth explanation.
|
|
1272
1297
|
|
|
@@ -1280,7 +1305,46 @@ When dealing with large or complex amounts of data in a KV store, sometimes it's
|
|
|
1280
1305
|
|
|
1281
1306
|
For these and more situations, you can use `.prefix()` to simplify your life further.
|
|
1282
1307
|
|
|
1283
|
-
|
|
1308
|
+
### Error Handling
|
|
1309
|
+
|
|
1310
|
+
Polystore methods return promises and surface errors from the underlying client. A good rule of thumb is to treat errors in three categories:
|
|
1311
|
+
|
|
1312
|
+
1. **Connectivity/runtime errors**
|
|
1313
|
+
Network/database/filesystem/client runtime failures (for example Redis unavailable, failed fetch, permission denied on files).
|
|
1314
|
+
|
|
1315
|
+
2. **Data/serialization errors**
|
|
1316
|
+
Invalid JSON payloads, invalid value encoding, or data that was written outside Polystore and cannot be decoded with its metadata expectations.
|
|
1317
|
+
|
|
1318
|
+
3. **Usage/configuration errors**
|
|
1319
|
+
Invalid client setup, invalid URLs/paths, or unsupported operations in a specific runtime.
|
|
1320
|
+
|
|
1321
|
+
Recommended patterns:
|
|
1322
|
+
|
|
1323
|
+
- Use `try/catch` around all write/read operations in production paths.
|
|
1324
|
+
- Prefer returning safe fallbacks for cache-like usage (`null`, stale response, or refetch).
|
|
1325
|
+
- Log enough context (`client type`, `key`, operation name) without logging sensitive values.
|
|
1326
|
+
- For remote clients, consider retry/backoff only for transient failures.
|
|
1327
|
+
- Call `.close()` during shutdown when the client supports it.
|
|
1328
|
+
|
|
1329
|
+
Example:
|
|
1330
|
+
|
|
1331
|
+
```js
|
|
1332
|
+
const key = `user:${userId}`;
|
|
1333
|
+
|
|
1334
|
+
try {
|
|
1335
|
+
const cached = await store.get(key);
|
|
1336
|
+
if (cached) return cached;
|
|
1337
|
+
|
|
1338
|
+
const fresh = await fetchUserFromAPI(userId);
|
|
1339
|
+
await store.set(key, fresh, { expires: "10min" });
|
|
1340
|
+
return fresh;
|
|
1341
|
+
} catch (err) {
|
|
1342
|
+
console.error("polystore error", { key, err });
|
|
1343
|
+
return fetchUserFromAPI(userId);
|
|
1344
|
+
}
|
|
1345
|
+
```
|
|
1346
|
+
|
|
1347
|
+
### Creating a store
|
|
1284
1348
|
|
|
1285
1349
|
To create a store, you define a class with these properties and methods:
|
|
1286
1350
|
|
|
@@ -1341,6 +1405,10 @@ client.keys = (prefix) => {
|
|
|
1341
1405
|
|
|
1342
1406
|
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).
|
|
1343
1407
|
|
|
1408
|
+
|
|
1409
|
+
|
|
1410
|
+
## Examples
|
|
1411
|
+
|
|
1344
1412
|
### Plain Object client
|
|
1345
1413
|
|
|
1346
1414
|
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:
|
|
@@ -1371,7 +1439,7 @@ class MyClient {
|
|
|
1371
1439
|
|
|
1372
1440
|
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.
|
|
1373
1441
|
|
|
1374
|
-
###
|
|
1442
|
+
### Custom ID generation
|
|
1375
1443
|
|
|
1376
1444
|
You might want to provide your custom key generation algorithm, which I'm going to call `customId()` for example purposes. The only place where `polystore` generates IDs is in `add`, so you can provide your client with a custom generator:
|
|
1377
1445
|
|
|
@@ -1404,7 +1472,7 @@ const id2 = await store.prefix("hello:").add({ hello: "world" });
|
|
|
1404
1472
|
// this is `hello:{your own custom id}`
|
|
1405
1473
|
```
|
|
1406
1474
|
|
|
1407
|
-
###
|
|
1475
|
+
### Serializing the data
|
|
1408
1476
|
|
|
1409
1477
|
If you need to serialize the data before storing it, you can do it within your custom client. Here's an example of how you can handle data serialization when setting values:
|
|
1410
1478
|
|
|
@@ -1429,7 +1497,7 @@ class MyClient {
|
|
|
1429
1497
|
}
|
|
1430
1498
|
```
|
|
1431
1499
|
|
|
1432
|
-
###
|
|
1500
|
+
### Cloudflare API calls
|
|
1433
1501
|
|
|
1434
1502
|
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:
|
|
1435
1503
|
|
|
@@ -1501,9 +1569,6 @@ const store = kv(CloudflareCustom);
|
|
|
1501
1569
|
|
|
1502
1570
|
It's lacking a few things, so make sure to adapt to your needs, but it worked for my very simple cache needs.
|
|
1503
1571
|
|
|
1504
|
-
|
|
1505
|
-
## Examples
|
|
1506
|
-
|
|
1507
1572
|
### Simple cache
|
|
1508
1573
|
|
|
1509
1574
|
I've used Polystore in many projects as a simple cache. With `fetch()`, it's fairly easy:
|
|
@@ -1514,12 +1579,9 @@ async function getProductInfo(id: string) {
|
|
|
1514
1579
|
if (data) return data;
|
|
1515
1580
|
|
|
1516
1581
|
const res = await fetch(`https://some-url.com/products/${id}`);
|
|
1517
|
-
const
|
|
1518
|
-
|
|
1519
|
-
// Some processing here
|
|
1520
|
-
const clean = raw??;
|
|
1582
|
+
const data = await res.json();
|
|
1521
1583
|
|
|
1522
|
-
await store.set(id,
|
|
1584
|
+
await store.set(id, data, { expires: "10days" });
|
|
1523
1585
|
return clean;
|
|
1524
1586
|
}
|
|
1525
1587
|
```
|