polystore 0.19.0 → 0.21.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
@@ -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 row = this.client.prepare(`SELECT value, expires_at FROM kv WHERE id = ?`).get(id);
474
- if (!row) return null;
475
- if (row.expires_at && row.expires_at < Date.now()) {
476
- this.del(id);
477
- return null;
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 = async (id) => {
489
- await this.client.prepare(`DELETE FROM kv WHERE id = ?`).run(id);
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
- #clearExpired = () => {
519
- this.client.prepare(`DELETE FROM kv WHERE expires_at < ?`).run(Date.now());
609
+ prune = () => {
610
+ this.client.prepare(`DELETE FROM kv WHERE expires_at <= ?`).run(Date.now());
520
611
  };
521
- clearAll = () => {
522
- this.client.exec(`DELETE FROM kv`);
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
- // postgres,
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
- 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
- }
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.19.0",
3
+ "version": "0.21.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,23 +16,36 @@
16
16
  "types": "./index.d.ts",
17
17
  "import": "./index.js"
18
18
  },
19
- "./express": "./src/express.js"
19
+ "./express": {
20
+ "types": "./src/integrations/express.d.ts",
21
+ "import": "./src/integrations/express.js"
22
+ },
23
+ "./hono-sessions": {
24
+ "types": "./src/integrations/hono-sessions.d.ts",
25
+ "import": "./src/integrations/hono-sessions.js"
26
+ }
20
27
  },
21
28
  "files": [
22
29
  "index.js",
23
30
  "index.d.ts",
24
- "src/express.js"
31
+ "src/integrations/express.js",
32
+ "src/integrations/express.d.ts",
33
+ "src/integrations/hono-sessions.js",
34
+ "src/integrations/hono-sessions.d.ts"
25
35
  ],
26
36
  "scripts": {
27
37
  "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",
28
- "build": "bunx tsup src/index.ts --format esm --dts --out-dir . --target node24",
38
+ "build": "bunx tsup src/index.ts --format esm --dts --out-dir . --target node24 && bunx tsup src/integrations/express.ts src/integrations/hono-sessions.ts --format esm --dts --out-dir src/integrations --target node24 --external polystore --external express-session --external hono-sessions",
29
39
  "lint": "npx tsc --noEmit",
30
40
  "start": "bun test --watch",
41
+ "service:db": "etcd",
42
+ "service:redis": "brew services start redis",
43
+ "service:postgres": "brew services start postgresql",
44
+ "service:server": "bun ./src/server.ts",
45
+ "services": "concurrently \"npm run service:db\" \"npm run service:redis\" \"npm run service:postgres\" \"npm run service:server\"",
31
46
  "test": "npm run test:bun && npm run test:jest",
32
- "test:bun": "bun test ./test/index.test.ts",
33
- "test:jest": "jest ./test/index.test.ts --detectOpenHandles --forceExit",
34
- "run:db": "etcd",
35
- "run:server": "bun ./src/server.ts"
47
+ "test:bun": "bun test ./test/index.test.ts ./src/integrations/",
48
+ "test:jest": "jest ./test/index.test.ts --detectOpenHandles --forceExit"
36
49
  },
37
50
  "keywords": [
38
51
  "kv",
@@ -47,21 +60,32 @@
47
60
  "@deno/kv": "^0.8.1",
48
61
  "@types/better-sqlite3": "^7.6.13",
49
62
  "@types/bun": "^1.3.3",
63
+ "@types/express": "^5.0.6",
64
+ "@types/express-session": "^1.18.2",
50
65
  "@types/jest": "^30.0.0",
51
66
  "@types/jsdom": "^27.0.0",
67
+ "@types/pg": "^8.11.10",
68
+ "@types/supertest": "^7.2.0",
52
69
  "better-sqlite3": "^12.6.0",
53
70
  "check-dts": "^0.8.0",
71
+ "concurrently": "^9.2.1",
54
72
  "cross-fetch": "^4.1.0",
55
73
  "dotenv": "^16.3.1",
56
74
  "edge-mock": "^0.0.15",
57
75
  "esbuild": "^0.27.0",
58
76
  "etcd3": "^1.1.2",
77
+ "express": "^5.2.1",
78
+ "express-session": "^1.19.0",
59
79
  "gzip-size-cli": "^5.1.0",
80
+ "hono": "^4.12.10",
81
+ "hono-sessions": "^0.8.1",
60
82
  "jest": "^30.2.0",
61
83
  "jsdom": "^27.2.0",
62
84
  "level": "^8.0.1",
63
85
  "localforage": "^1.10.0",
86
+ "pg": "^8.13.1",
64
87
  "redis": "^4.6.10",
88
+ "supertest": "^7.2.2",
65
89
  "ts-jest": "^29.4.6",
66
90
  "ts-node": "^10.9.2",
67
91
  "tsup": "^8.5.1",