@geekmidas/cache 10.0.0-alpha.9 → 10.0.0-alpha.90

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.
Files changed (84) hide show
  1. package/dist/expo.cjs +10 -11
  2. package/dist/expo.cjs.map +1 -1
  3. package/dist/expo.d.cts +2 -6
  4. package/dist/expo.d.cts.map +1 -1
  5. package/dist/expo.d.mts +2 -6
  6. package/dist/expo.d.mts.map +1 -1
  7. package/dist/expo.mjs +4 -7
  8. package/dist/expo.mjs.map +1 -1
  9. package/dist/file.cjs +16 -20
  10. package/dist/file.cjs.map +1 -1
  11. package/dist/file.d.cts +3 -6
  12. package/dist/file.d.cts.map +1 -1
  13. package/dist/file.d.mts +3 -6
  14. package/dist/file.d.mts.map +1 -1
  15. package/dist/file.mjs +7 -13
  16. package/dist/file.mjs.map +1 -1
  17. package/dist/{index-1sNRIo6N.d.mts → index-BbdGhmWP.d.cts} +2 -4
  18. package/dist/index-BbdGhmWP.d.cts.map +1 -0
  19. package/dist/{index-CNQ50bVk.d.cts → index-BbdGhmWP.d.mts} +2 -4
  20. package/dist/index-BbdGhmWP.d.mts.map +1 -0
  21. package/dist/index.cjs +15 -5
  22. package/dist/index.cjs.map +1 -0
  23. package/dist/index.d.cts +2 -2
  24. package/dist/index.d.mts +2 -2
  25. package/dist/index.mjs +12 -3
  26. package/dist/index.mjs.map +1 -0
  27. package/dist/memory.cjs +8 -12
  28. package/dist/memory.cjs.map +1 -1
  29. package/dist/memory.d.cts +2 -6
  30. package/dist/memory.d.cts.map +1 -1
  31. package/dist/memory.d.mts +2 -6
  32. package/dist/memory.d.mts.map +1 -1
  33. package/dist/memory.mjs +5 -9
  34. package/dist/memory.mjs.map +1 -1
  35. package/dist/postgres.cjs +46 -16
  36. package/dist/postgres.cjs.map +1 -1
  37. package/dist/postgres.d.cts +5 -9
  38. package/dist/postgres.d.cts.map +1 -1
  39. package/dist/postgres.d.mts +5 -9
  40. package/dist/postgres.d.mts.map +1 -1
  41. package/dist/postgres.mjs +42 -14
  42. package/dist/postgres.mjs.map +1 -1
  43. package/dist/redis.cjs +40 -22
  44. package/dist/redis.cjs.map +1 -1
  45. package/dist/redis.d.cts +4 -8
  46. package/dist/redis.d.cts.map +1 -1
  47. package/dist/redis.d.mts +4 -8
  48. package/dist/redis.d.mts.map +1 -1
  49. package/dist/redis.mjs +38 -20
  50. package/dist/redis.mjs.map +1 -1
  51. package/dist/registry.cjs +47 -5
  52. package/dist/registry.cjs.map +1 -0
  53. package/dist/registry.d.cts +1 -1
  54. package/dist/registry.d.mts +1 -1
  55. package/dist/registry.mjs +43 -2
  56. package/dist/registry.mjs.map +1 -0
  57. package/dist/{chunk-CUT6urMc.cjs → rolldown-runtime-VH7oDXx4.cjs} +8 -10
  58. package/dist/telemetry.cjs +38 -0
  59. package/dist/telemetry.cjs.map +1 -0
  60. package/dist/telemetry.d.cts +6 -0
  61. package/dist/telemetry.d.cts.map +1 -0
  62. package/dist/telemetry.d.mts +6 -0
  63. package/dist/telemetry.d.mts.map +1 -0
  64. package/dist/telemetry.mjs +37 -0
  65. package/dist/telemetry.mjs.map +1 -0
  66. package/dist/upstash.cjs +5 -6
  67. package/dist/upstash.cjs.map +1 -1
  68. package/dist/upstash.d.cts +6 -9
  69. package/dist/upstash.d.cts.map +1 -1
  70. package/dist/upstash.d.mts +6 -9
  71. package/dist/upstash.d.mts.map +1 -1
  72. package/dist/upstash.mjs +2 -3
  73. package/dist/upstash.mjs.map +1 -1
  74. package/package.json +6 -1
  75. package/dist/index-1sNRIo6N.d.mts.map +0 -1
  76. package/dist/index-CNQ50bVk.d.cts.map +0 -1
  77. package/dist/registry-CITg73zW.cjs +0 -65
  78. package/dist/registry-CITg73zW.cjs.map +0 -1
  79. package/dist/registry-CT0b-rjB.mjs +0 -41
  80. package/dist/registry-CT0b-rjB.mjs.map +0 -1
  81. package/dist/src-DVpoP03o.mjs +0 -14
  82. package/dist/src-DVpoP03o.mjs.map +0 -1
  83. package/dist/src-ysNIqjnr.cjs +0 -20
  84. package/dist/src-ysNIqjnr.cjs.map +0 -1
package/dist/memory.d.cts CHANGED
@@ -1,7 +1,6 @@
1
- import { Cache } from "./index-CNQ50bVk.cjs";
2
-
1
+ import { t as Cache } from "./index-BbdGhmWP.cjs";
3
2
  //#region src/memory.d.ts
4
- declare class InMemoryCache implements Cache {
3
+ export declare class InMemoryCache implements Cache {
5
4
  private store;
6
5
  private expirations;
7
6
  get<T>(key: string): Promise<T | undefined>;
@@ -9,8 +8,5 @@ declare class InMemoryCache implements Cache {
9
8
  set<T>(key: string, value: T, ttl?: number): Promise<void>;
10
9
  delete(key: string): Promise<void>;
11
10
  }
12
- //# sourceMappingURL=memory.d.ts.map
13
-
14
11
  //#endregion
15
- export { InMemoryCache };
16
12
  //# sourceMappingURL=memory.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"memory.d.cts","names":[],"sources":["../src/memory.ts"],"sourcesContent":[],"mappings":";;;cAGa,aAAA,YAAyB;;EAAzB,QAAA,WAAc;EAAA,GAAA,CAAA,CAAA,CAAA,CAAA,GAAA,EAAA,MAAA,CAAA,EAIC,OAJD,CAIS,CAJT,GAAA,SAAA,CAAA;EAAA,GAIS,CAAA,GAAA,EAAA,MAAA,CAAA,EAUX,OAVW,CAAA,MAAA,CAAA;EAAC,GAAT,CAAA,CAAA,CAAA,CAAA,GAAA,EAAA,MAAA,EAAA,KAAA,EAmBM,CAnBN,EAAA,GAAA,CAAA,EAAA,MAAA,CAAA,EAmB6B,OAnB7B,CAAA,IAAA,CAAA;EAAO,MAUV,CAAA,GAAA,EAAA,MAAA,CAAA,EAgBG,OAhBH,CAAA,IAAA,CAAA"}
1
+ {"version":3,"file":"memory.d.cts","names":[],"sources":["../src/memory.ts"],"mappings":";;qBAGa,yBAAyB;UAC7B;UACA;EAEF,IAAI,GAAG,cAAc,QAAQ;EAU7B,IAAI,cAAc;EASlB,IAAI,GAAG,aAAa,OAAO,GAAG,eAAoB;EAOlD,OAAO,cAAc"}
package/dist/memory.d.mts CHANGED
@@ -1,7 +1,6 @@
1
- import { Cache } from "./index-1sNRIo6N.mjs";
2
-
1
+ import { t as Cache } from "./index-BbdGhmWP.mjs";
3
2
  //#region src/memory.d.ts
4
- declare class InMemoryCache implements Cache {
3
+ export declare class InMemoryCache implements Cache {
5
4
  private store;
6
5
  private expirations;
7
6
  get<T>(key: string): Promise<T | undefined>;
@@ -9,8 +8,5 @@ declare class InMemoryCache implements Cache {
9
8
  set<T>(key: string, value: T, ttl?: number): Promise<void>;
10
9
  delete(key: string): Promise<void>;
11
10
  }
12
- //# sourceMappingURL=memory.d.ts.map
13
-
14
11
  //#endregion
15
- export { InMemoryCache };
16
12
  //# sourceMappingURL=memory.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"memory.d.mts","names":[],"sources":["../src/memory.ts"],"sourcesContent":[],"mappings":";;;cAGa,aAAA,YAAyB;;EAAzB,QAAA,WAAc;EAAA,GAAA,CAAA,CAAA,CAAA,CAAA,GAAA,EAAA,MAAA,CAAA,EAIC,OAJD,CAIS,CAJT,GAAA,SAAA,CAAA;EAAA,GAIS,CAAA,GAAA,EAAA,MAAA,CAAA,EAUX,OAVW,CAAA,MAAA,CAAA;EAAC,GAAT,CAAA,CAAA,CAAA,CAAA,GAAA,EAAA,MAAA,EAAA,KAAA,EAmBM,CAnBN,EAAA,GAAA,CAAA,EAAA,MAAA,CAAA,EAmB6B,OAnB7B,CAAA,IAAA,CAAA;EAAO,MAUV,CAAA,GAAA,EAAA,MAAA,CAAA,EAgBG,OAhBH,CAAA,IAAA,CAAA"}
1
+ {"version":3,"file":"memory.d.mts","names":[],"sources":["../src/memory.ts"],"mappings":";;qBAGa,yBAAyB;UAC7B;UACA;EAEF,IAAI,GAAG,cAAc,QAAQ;EAU7B,IAAI,cAAc;EASlB,IAAI,GAAG,aAAa,OAAO,GAAG,eAAoB;EAOlD,OAAO,cAAc"}
package/dist/memory.mjs CHANGED
@@ -1,16 +1,13 @@
1
- import "./registry-CT0b-rjB.mjs";
2
- import { getExpirationInSeconds } from "./src-DVpoP03o.mjs";
1
+ import { getExpirationInSeconds } from "./index.mjs";
3
2
  import { addSeconds } from "date-fns";
4
-
5
3
  //#region src/memory.ts
6
4
  var InMemoryCache = class {
7
5
  store = /* @__PURE__ */ new Map();
8
6
  expirations = /* @__PURE__ */ new Map();
9
7
  async get(key) {
10
- const expiration = await this.ttl(key);
11
- if (!expiration) {
8
+ if (!await this.ttl(key)) {
12
9
  await this.delete(key);
13
- return void 0;
10
+ return;
14
11
  }
15
12
  return this.store.get(key);
16
13
  }
@@ -21,8 +18,7 @@ var InMemoryCache = class {
21
18
  }
22
19
  async set(key, value, ttl = 600) {
23
20
  this.store.set(key, value);
24
- const now = /* @__PURE__ */ new Date();
25
- const expiresAt = addSeconds(now, ttl);
21
+ const expiresAt = addSeconds(/* @__PURE__ */ new Date(), ttl);
26
22
  this.expirations.set(key, expiresAt);
27
23
  }
28
24
  async delete(key) {
@@ -30,7 +26,7 @@ var InMemoryCache = class {
30
26
  this.expirations.delete(key);
31
27
  }
32
28
  };
33
-
34
29
  //#endregion
35
30
  export { InMemoryCache };
31
+
36
32
  //# sourceMappingURL=memory.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"memory.mjs","names":["key: string","value: T","ttl: number"],"sources":["../src/memory.ts"],"sourcesContent":["import { addSeconds } from 'date-fns';\nimport { type Cache, getExpirationInSeconds } from './';\n\nexport class InMemoryCache implements Cache {\n\tprivate store: Map<string, unknown> = new Map();\n\tprivate expirations: Map<string, Date> = new Map();\n\n\tasync get<T>(key: string): Promise<T | undefined> {\n\t\tconst expiration = await this.ttl(key);\n\t\tif (!expiration) {\n\t\t\tawait this.delete(key);\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn this.store.get(key) as T | undefined;\n\t}\n\n\tasync ttl(key: string): Promise<number> {\n\t\tconst expiration = this.expirations.get(key);\n\t\tif (!expiration) {\n\t\t\treturn 0;\n\t\t}\n\n\t\treturn getExpirationInSeconds(expiration);\n\t}\n\n\tasync set<T>(key: string, value: T, ttl: number = 600): Promise<void> {\n\t\tthis.store.set(key, value);\n\t\tconst now = new Date();\n\t\tconst expiresAt = addSeconds(now, ttl);\n\t\tthis.expirations.set(key, expiresAt);\n\t}\n\n\tasync delete(key: string): Promise<void> {\n\t\tthis.store.delete(key);\n\t\tthis.expirations.delete(key);\n\t}\n}\n"],"mappings":";;;;;AAGA,IAAa,gBAAb,MAA4C;CAC3C,AAAQ,wBAA8B,IAAI;CAC1C,AAAQ,8BAAiC,IAAI;CAE7C,MAAM,IAAOA,KAAqC;EACjD,MAAM,aAAa,MAAM,KAAK,IAAI,IAAI;AACtC,OAAK,YAAY;AAChB,SAAM,KAAK,OAAO,IAAI;AACtB;EACA;AAED,SAAO,KAAK,MAAM,IAAI,IAAI;CAC1B;CAED,MAAM,IAAIA,KAA8B;EACvC,MAAM,aAAa,KAAK,YAAY,IAAI,IAAI;AAC5C,OAAK,WACJ,QAAO;AAGR,SAAO,uBAAuB,WAAW;CACzC;CAED,MAAM,IAAOA,KAAaC,OAAUC,MAAc,KAAoB;AACrE,OAAK,MAAM,IAAI,KAAK,MAAM;EAC1B,MAAM,sBAAM,IAAI;EAChB,MAAM,YAAY,WAAW,KAAK,IAAI;AACtC,OAAK,YAAY,IAAI,KAAK,UAAU;CACpC;CAED,MAAM,OAAOF,KAA4B;AACxC,OAAK,MAAM,OAAO,IAAI;AACtB,OAAK,YAAY,OAAO,IAAI;CAC5B;AACD"}
1
+ {"version":3,"file":"memory.mjs","names":[],"sources":["../src/memory.ts"],"sourcesContent":["import { addSeconds } from 'date-fns';\nimport { type Cache, getExpirationInSeconds } from './';\n\nexport class InMemoryCache implements Cache {\n\tprivate store: Map<string, unknown> = new Map();\n\tprivate expirations: Map<string, Date> = new Map();\n\n\tasync get<T>(key: string): Promise<T | undefined> {\n\t\tconst expiration = await this.ttl(key);\n\t\tif (!expiration) {\n\t\t\tawait this.delete(key);\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn this.store.get(key) as T | undefined;\n\t}\n\n\tasync ttl(key: string): Promise<number> {\n\t\tconst expiration = this.expirations.get(key);\n\t\tif (!expiration) {\n\t\t\treturn 0;\n\t\t}\n\n\t\treturn getExpirationInSeconds(expiration);\n\t}\n\n\tasync set<T>(key: string, value: T, ttl: number = 600): Promise<void> {\n\t\tthis.store.set(key, value);\n\t\tconst now = new Date();\n\t\tconst expiresAt = addSeconds(now, ttl);\n\t\tthis.expirations.set(key, expiresAt);\n\t}\n\n\tasync delete(key: string): Promise<void> {\n\t\tthis.store.delete(key);\n\t\tthis.expirations.delete(key);\n\t}\n}\n"],"mappings":";;;AAGA,IAAa,gBAAb,MAA4C;CAC3C,wBAAsC,IAAI,IAAI;CAC9C,8BAAyC,IAAI,IAAI;CAEjD,MAAM,IAAO,KAAqC;EAEjD,IAAI,CAAC,MADoB,KAAK,IAAI,GAAG,GACpB;GAChB,MAAM,KAAK,OAAO,GAAG;GACrB;EACD;EAEA,OAAO,KAAK,MAAM,IAAI,GAAG;CAC1B;CAEA,MAAM,IAAI,KAA8B;EACvC,MAAM,aAAa,KAAK,YAAY,IAAI,GAAG;EAC3C,IAAI,CAAC,YACJ,OAAO;EAGR,OAAO,uBAAuB,UAAU;CACzC;CAEA,MAAM,IAAO,KAAa,OAAU,MAAc,KAAoB;EACrE,KAAK,MAAM,IAAI,KAAK,KAAK;EAEzB,MAAM,YAAY,2BAAW,IADb,KACe,GAAG,GAAG;EACrC,KAAK,YAAY,IAAI,KAAK,SAAS;CACpC;CAEA,MAAM,OAAO,KAA4B;EACxC,KAAK,MAAM,OAAO,GAAG;EACrB,KAAK,YAAY,OAAO,GAAG;CAC5B;AACD"}
package/dist/postgres.cjs CHANGED
@@ -1,31 +1,61 @@
1
- const require_chunk = require('./chunk-CUT6urMc.cjs');
2
- const pg = require_chunk.__toESM(require("pg"));
3
-
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_rolldown_runtime = require("./rolldown-runtime-VH7oDXx4.cjs");
3
+ const require_telemetry = require("./telemetry.cjs");
4
+ let pg = require("pg");
5
+ pg = require_rolldown_runtime.__toESM(pg, 1);
4
6
  //#region src/postgres.ts
7
+ /**
8
+ * A cache that lives in a database you already have.
9
+ *
10
+ * The backend with no infrastructure. A cache is a table; the database is the
11
+ * one the application already declared, so an idle stage costs nothing extra
12
+ * and there is one fewer thing to provision, secure and pay for. It is slower
13
+ * than Redis and that is the trade: for session lookups, rate-limit counters and
14
+ * memoised API responses at ordinary volumes, a primary-key hit on a small table
15
+ * is not the thing that will be slow.
16
+ *
17
+ * **The table is provisioned, not created on first use.** That is the one place
18
+ * this differs from pg-boss, and the reason is the role split: a handler
19
+ * connects as a role that may read and write rows and may not create anything,
20
+ * so a driver issuing `CREATE TABLE` would fail on exactly the deployment where
21
+ * the roles are doing their job. The DDL is exported instead — see
22
+ * {@link cacheTableStatements} — and applied by whatever applies the rest.
23
+ *
24
+ * Reads filter on expiry and delete lazily, so an expired row is never returned
25
+ * even if nothing has swept it; a sweep is an optimisation, not a correctness
26
+ * requirement.
27
+ */
5
28
  const DEFAULT_TABLE = "cache";
6
29
  var PostgresCache = class {
30
+ pool;
7
31
  table;
8
32
  constructor(pool, options = {}) {
9
33
  this.pool = pool;
10
34
  this.table = options.table ?? DEFAULT_TABLE;
11
35
  }
12
36
  async get(key) {
13
- const { rows } = await this.query(`SELECT value FROM ${this.identifier()}
14
- WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())`, [key]);
15
- return rows[0]?.value;
37
+ return require_telemetry.traceCache("postgresql", "get", async () => {
38
+ const { rows } = await this.query(`SELECT value FROM ${this.identifier()}
39
+ WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())`, [key]);
40
+ return rows[0]?.value;
41
+ });
16
42
  }
17
43
  async set(key, value, ttl) {
18
- await this.query(`INSERT INTO ${this.identifier()} (key, value, expires_at)
19
- VALUES ($1, $2::jsonb, ${ttl === void 0 ? "NULL" : "now() + ($3 || ' seconds')::interval"})
20
- ON CONFLICT (key) DO UPDATE
21
- SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at`, ttl === void 0 ? [key, JSON.stringify(value)] : [
22
- key,
23
- JSON.stringify(value),
24
- String(ttl)
25
- ]);
44
+ return require_telemetry.traceCache("postgresql", "set", async () => {
45
+ await this.query(`INSERT INTO ${this.identifier()} (key, value, expires_at)
46
+ VALUES ($1, $2::jsonb, ${ttl === void 0 ? "NULL" : "now() + ($3 || ' seconds')::interval"})
47
+ ON CONFLICT (key) DO UPDATE
48
+ SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at`, ttl === void 0 ? [key, JSON.stringify(value)] : [
49
+ key,
50
+ JSON.stringify(value),
51
+ String(ttl)
52
+ ]);
53
+ });
26
54
  }
27
55
  async delete(key) {
28
- await this.query(`DELETE FROM ${this.identifier()} WHERE key = $1`, [key]);
56
+ return require_telemetry.traceCache("postgresql", "delete", async () => {
57
+ await this.query(`DELETE FROM ${this.identifier()} WHERE key = $1`, [key]);
58
+ });
29
59
  }
30
60
  async ttl(key) {
31
61
  const { rows } = await this.query(`SELECT EXTRACT(EPOCH FROM (expires_at - now()))::int AS seconds
@@ -147,9 +177,9 @@ function quote(table) {
147
177
  function indexName(table) {
148
178
  return `"${table.replace(/[^a-zA-Z0-9]/g, "_")}_expires_at_idx"`;
149
179
  }
150
-
151
180
  //#endregion
152
181
  exports.PostgresCache = PostgresCache;
153
182
  exports.cacheTableStatements = cacheTableStatements;
154
183
  exports.postgresCacheDriver = postgresCacheDriver;
184
+
155
185
  //# sourceMappingURL=postgres.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"postgres.cjs","names":["pool: Pool","options: PostgresCacheOptions","key: string","value: T","ttl?: number","sql: string","values: unknown[]","postgresCacheDriver: CacheDriver","url: string","table: string"],"sources":["../src/postgres.ts"],"sourcesContent":["/**\n * A cache that lives in a database you already have.\n *\n * The backend with no infrastructure. A cache is a table; the database is the\n * one the application already declared, so an idle stage costs nothing extra\n * and there is one fewer thing to provision, secure and pay for. It is slower\n * than Redis and that is the trade: for session lookups, rate-limit counters and\n * memoised API responses at ordinary volumes, a primary-key hit on a small table\n * is not the thing that will be slow.\n *\n * **The table is provisioned, not created on first use.** That is the one place\n * this differs from pg-boss, and the reason is the role split: a handler\n * connects as a role that may read and write rows and may not create anything,\n * so a driver issuing `CREATE TABLE` would fail on exactly the deployment where\n * the roles are doing their job. The DDL is exported instead — see\n * {@link cacheTableStatements} — and applied by whatever applies the rest.\n *\n * Reads filter on expiry and delete lazily, so an expired row is never returned\n * even if nothing has swept it; a sweep is an optimisation, not a correctness\n * requirement.\n */\n\nimport pg, { type Pool } from 'pg';\nimport type { Cache } from './index';\nimport type { CacheDriver } from './registry';\n\nexport interface PostgresCacheOptions {\n\t/**\n\t * The table to keep entries in, schema-qualified if it needs to be.\n\t *\n\t * Defaults to `cache`. It resolves against the connection's `search_path`,\n\t * which the role carries — so a tenant's cache lands in the tenant's schema\n\t * without anything here knowing the schema's name.\n\t */\n\ttable?: string;\n}\n\nconst DEFAULT_TABLE = 'cache';\n\nexport class PostgresCache implements Cache {\n\tprivate readonly table: string;\n\n\tconstructor(\n\t\tprivate readonly pool: Pool,\n\t\toptions: PostgresCacheOptions = {},\n\t) {\n\t\tthis.table = options.table ?? DEFAULT_TABLE;\n\t}\n\n\tasync get<T>(key: string): Promise<T | undefined> {\n\t\tconst { rows } = await this.query<{ value: T }>(\n\t\t\t`SELECT value FROM ${this.identifier()}\n\t\t\t WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())`,\n\t\t\t[key],\n\t\t);\n\n\t\treturn rows[0]?.value;\n\t}\n\n\tasync set<T>(key: string, value: T, ttl?: number): Promise<void> {\n\t\t// `now() + interval` computed by the server, not by this process: a\n\t\t// client whose clock is minutes off would otherwise write entries that\n\t\t// expire early or late, and clock skew is not a thing to debug through a\n\t\t// cache.\n\t\tawait this.query(\n\t\t\t`INSERT INTO ${this.identifier()} (key, value, expires_at)\n\t\t\t VALUES ($1, $2::jsonb, ${ttl === undefined ? 'NULL' : \"now() + ($3 || ' seconds')::interval\"})\n\t\t\t ON CONFLICT (key) DO UPDATE\n\t\t\t SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at`,\n\t\t\tttl === undefined\n\t\t\t\t? [key, JSON.stringify(value)]\n\t\t\t\t: [key, JSON.stringify(value), String(ttl)],\n\t\t);\n\t}\n\n\tasync delete(key: string): Promise<void> {\n\t\tawait this.query(`DELETE FROM ${this.identifier()} WHERE key = $1`, [key]);\n\t}\n\n\tasync ttl(key: string): Promise<number> {\n\t\tconst { rows } = await this.query<{ seconds: number | null }>(\n\t\t\t`SELECT EXTRACT(EPOCH FROM (expires_at - now()))::int AS seconds\n\t\t\t FROM ${this.identifier()}\n\t\t\t WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())`,\n\t\t\t[key],\n\t\t);\n\n\t\tconst row = rows[0];\n\t\tif (!row) return 0;\n\n\t\t// A row with no expiry has no time *left* to report. Zero is what the\n\t\t// interface says a missing key returns, and \"never expires\" is closer to\n\t\t// that than to any number this could invent.\n\t\treturn row.seconds ?? 0;\n\t}\n\n\t/**\n\t * Delete every expired entry.\n\t *\n\t * Optional: reads already ignore them. Worth running occasionally so the\n\t * table does not grow without bound in a workload that writes many keys once.\n\t */\n\tasync sweep(): Promise<number> {\n\t\tconst { rowCount } = await this.query(\n\t\t\t`DELETE FROM ${this.identifier()} WHERE expires_at <= now()`,\n\t\t);\n\n\t\treturn rowCount ?? 0;\n\t}\n\n\tprivate async query<T>(\n\t\tsql: string,\n\t\tvalues: unknown[] = [],\n\t): Promise<{ rows: T[]; rowCount: number | null }> {\n\t\tconst result = await this.pool.query(sql, values);\n\n\t\treturn { rows: result.rows as T[], rowCount: result.rowCount };\n\t}\n\n\t/**\n\t * The table name as an identifier.\n\t *\n\t * Quoted per segment so a schema-qualified name stays two identifiers rather\n\t * than becoming one with a dot in it. The name comes from configuration\n\t * rather than from a request, so this is defence rather than sanitisation.\n\t */\n\tprivate identifier(): string {\n\t\treturn quote(this.table);\n\t}\n}\n\n/**\n * The `postgres://` driver.\n *\n * One pool per URL, cached for the life of the process: a cache that opened a\n * connection per call would be slower than the thing it exists to speed up, and\n * a Lambda reusing a warm container should reuse the pool with it.\n *\n * `pg` is imported at the top of this module rather than lazily, because the\n * *module* is the lazy boundary — an app caching in Redis never imports\n * `@geekmidas/cache/postgres` and so never resolves the driver behind it. Same\n * arrangement `@geekmidas/storage/aws` uses for the AWS SDK.\n */\nexport const postgresCacheDriver: CacheDriver = {\n\tscheme: 'postgres:',\n\tcreate(url) {\n\t\t// The table travels in the URL, the way pg-boss's schema does.\n\t\t//\n\t\t// It has to, because a database-backed cache has no address of its own:\n\t\t// two caches in one database resolve the *same* connection string, so a\n\t\t// client built from the URL alone cannot tell them apart and both would\n\t\t// read and write one table — sharing a keyspace and evicting each other's\n\t\t// keys. The parameter is what makes the address identify the cache.\n\t\tconst { connectionString, table } = split(url);\n\n\t\tconst existing = pools.get(connectionString);\n\t\tconst pool = existing ?? new pg.Pool({ connectionString });\n\t\tif (!existing) pools.set(connectionString, pool);\n\n\t\t// One pool per database, shared by every cache in it — the table is the\n\t\t// client's, not the pool's.\n\t\treturn new PostgresCache(pool, table ? { table } : {});\n\t},\n};\n\n/**\n * A cache URL as its two facts: where to connect, and what to read.\n *\n * `table` is removed from the string handed to `pg` rather than left on it,\n * because a Postgres connection string carries libpq parameters and an unknown\n * one is not guaranteed to be ignored — and because the pool should be shared\n * by every cache in the database, which only works if the key does not vary\n * per cache.\n */\nfunction split(url: string): { connectionString: string; table?: string } {\n\ttry {\n\t\tconst parsed = new URL(url);\n\t\tconst table = parsed.searchParams.get('table') ?? undefined;\n\n\t\tif (!table) return { connectionString: url };\n\n\t\tparsed.searchParams.delete('table');\n\n\t\treturn { connectionString: parsed.toString(), table };\n\t} catch {\n\t\t// Not a URL `URL` can parse. The driver's job is not to validate one, so\n\t\t// this hands it to `pg` unchanged and lets that failure be the one\n\t\t// reported.\n\t\treturn { connectionString: url };\n\t}\n}\n\nconst pools = new Map<string, Pool>();\n\n/**\n * The DDL a Postgres-backed cache needs, for whoever applies DDL.\n *\n * Generation only, in the shape `@geekmidas/db/pg/roles` uses — the same\n * statements run in-process from `gkm dev` and from a provisioner inside a VPC,\n * and one implementation is what stops \"works locally, fails deployed\".\n *\n * Run as the schema's **owner**. The role a handler connects as cannot create a\n * table, which is the point of the split and the reason this is not done lazily.\n */\nexport function cacheTableStatements(options: PostgresCacheOptions = {}): {\n\tdescribe: string;\n\texists?: { sql: string; values: unknown[] };\n\tsql: string;\n}[] {\n\tconst table = options.table ?? DEFAULT_TABLE;\n\tconst [schema, name] = table.includes('.')\n\t\t? (table.split('.') as [string, string])\n\t\t: [undefined, table];\n\tconst qualified = quote(table);\n\n\treturn [\n\t\t{\n\t\t\tdescribe: `cache table ${table}`,\n\t\t\texists: {\n\t\t\t\tsql: schema\n\t\t\t\t\t? 'SELECT 1 FROM information_schema.tables WHERE table_schema = $1 AND table_name = $2'\n\t\t\t\t\t: 'SELECT 1 FROM information_schema.tables WHERE table_name = $1',\n\t\t\t\tvalues: schema ? [schema, name] : [name],\n\t\t\t},\n\t\t\tsql: `CREATE TABLE ${qualified} (\n\t\t\t\tkey text PRIMARY KEY,\n\t\t\t\tvalue jsonb NOT NULL,\n\t\t\t\texpires_at timestamptz\n\t\t\t)`,\n\t\t},\n\t\t{\n\t\t\t// Only the expiring rows: a partial index stays small in a cache whose\n\t\t\t// entries mostly never expire, and the sweep only ever looks at those.\n\t\t\tdescribe: `cache expiry index on ${table}`,\n\t\t\tsql: `CREATE INDEX IF NOT EXISTS ${indexName(table)}\n\t\t\t ON ${qualified} (expires_at) WHERE expires_at IS NOT NULL`,\n\t\t},\n\t];\n}\n\n/** A possibly schema-qualified name, quoted per segment. */\nfunction quote(table: string): string {\n\treturn table\n\t\t.split('.')\n\t\t.map((part) => `\"${part.replace(/\"/g, '\"\"')}\"`)\n\t\t.join('.');\n}\n\nfunction indexName(table: string): string {\n\treturn `\"${table.replace(/[^a-zA-Z0-9]/g, '_')}_expires_at_idx\"`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"postgres.cjs","names":["traceCache"],"sources":["../src/postgres.ts"],"sourcesContent":["/**\n * A cache that lives in a database you already have.\n *\n * The backend with no infrastructure. A cache is a table; the database is the\n * one the application already declared, so an idle stage costs nothing extra\n * and there is one fewer thing to provision, secure and pay for. It is slower\n * than Redis and that is the trade: for session lookups, rate-limit counters and\n * memoised API responses at ordinary volumes, a primary-key hit on a small table\n * is not the thing that will be slow.\n *\n * **The table is provisioned, not created on first use.** That is the one place\n * this differs from pg-boss, and the reason is the role split: a handler\n * connects as a role that may read and write rows and may not create anything,\n * so a driver issuing `CREATE TABLE` would fail on exactly the deployment where\n * the roles are doing their job. The DDL is exported instead — see\n * {@link cacheTableStatements} — and applied by whatever applies the rest.\n *\n * Reads filter on expiry and delete lazily, so an expired row is never returned\n * even if nothing has swept it; a sweep is an optimisation, not a correctness\n * requirement.\n */\n\nimport pg, { type Pool } from 'pg';\nimport type { Cache } from './index';\nimport type { CacheDriver } from './registry';\nimport { traceCache } from './telemetry';\n\nexport interface PostgresCacheOptions {\n\t/**\n\t * The table to keep entries in, schema-qualified if it needs to be.\n\t *\n\t * Defaults to `cache`. It resolves against the connection's `search_path`,\n\t * which the role carries — so a tenant's cache lands in the tenant's schema\n\t * without anything here knowing the schema's name.\n\t */\n\ttable?: string;\n}\n\nconst DEFAULT_TABLE = 'cache';\n\nexport class PostgresCache implements Cache {\n\tprivate readonly table: string;\n\n\tconstructor(\n\t\tprivate readonly pool: Pool,\n\t\toptions: PostgresCacheOptions = {},\n\t) {\n\t\tthis.table = options.table ?? DEFAULT_TABLE;\n\t}\n\n\tasync get<T>(key: string): Promise<T | undefined> {\n\t\treturn traceCache('postgresql', 'get', async () => {\n\t\t\tconst { rows } = await this.query<{ value: T }>(\n\t\t\t\t`SELECT value FROM ${this.identifier()}\n\t\t\t\t WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())`,\n\t\t\t\t[key],\n\t\t\t);\n\n\t\t\treturn rows[0]?.value;\n\t\t});\n\t}\n\n\tasync set<T>(key: string, value: T, ttl?: number): Promise<void> {\n\t\treturn traceCache('postgresql', 'set', async () => {\n\t\t\t// `now() + interval` computed by the server, not by this process: a\n\t\t\t// client whose clock is minutes off would otherwise write entries that\n\t\t\t// expire early or late, and clock skew is not a thing to debug through a\n\t\t\t// cache.\n\t\t\tawait this.query(\n\t\t\t\t`INSERT INTO ${this.identifier()} (key, value, expires_at)\n\t\t\t\t VALUES ($1, $2::jsonb, ${ttl === undefined ? 'NULL' : \"now() + ($3 || ' seconds')::interval\"})\n\t\t\t\t ON CONFLICT (key) DO UPDATE\n\t\t\t\t SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at`,\n\t\t\t\tttl === undefined\n\t\t\t\t\t? [key, JSON.stringify(value)]\n\t\t\t\t\t: [key, JSON.stringify(value), String(ttl)],\n\t\t\t);\n\t\t});\n\t}\n\n\tasync delete(key: string): Promise<void> {\n\t\treturn traceCache('postgresql', 'delete', async () => {\n\t\t\tawait this.query(`DELETE FROM ${this.identifier()} WHERE key = $1`, [\n\t\t\t\tkey,\n\t\t\t]);\n\t\t});\n\t}\n\n\tasync ttl(key: string): Promise<number> {\n\t\tconst { rows } = await this.query<{ seconds: number | null }>(\n\t\t\t`SELECT EXTRACT(EPOCH FROM (expires_at - now()))::int AS seconds\n\t\t\t FROM ${this.identifier()}\n\t\t\t WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())`,\n\t\t\t[key],\n\t\t);\n\n\t\tconst row = rows[0];\n\t\tif (!row) return 0;\n\n\t\t// A row with no expiry has no time *left* to report. Zero is what the\n\t\t// interface says a missing key returns, and \"never expires\" is closer to\n\t\t// that than to any number this could invent.\n\t\treturn row.seconds ?? 0;\n\t}\n\n\t/**\n\t * Delete every expired entry.\n\t *\n\t * Optional: reads already ignore them. Worth running occasionally so the\n\t * table does not grow without bound in a workload that writes many keys once.\n\t */\n\tasync sweep(): Promise<number> {\n\t\tconst { rowCount } = await this.query(\n\t\t\t`DELETE FROM ${this.identifier()} WHERE expires_at <= now()`,\n\t\t);\n\n\t\treturn rowCount ?? 0;\n\t}\n\n\tprivate async query<T>(\n\t\tsql: string,\n\t\tvalues: unknown[] = [],\n\t): Promise<{ rows: T[]; rowCount: number | null }> {\n\t\tconst result = await this.pool.query(sql, values);\n\n\t\treturn { rows: result.rows as T[], rowCount: result.rowCount };\n\t}\n\n\t/**\n\t * The table name as an identifier.\n\t *\n\t * Quoted per segment so a schema-qualified name stays two identifiers rather\n\t * than becoming one with a dot in it. The name comes from configuration\n\t * rather than from a request, so this is defence rather than sanitisation.\n\t */\n\tprivate identifier(): string {\n\t\treturn quote(this.table);\n\t}\n}\n\n/**\n * The `postgres://` driver.\n *\n * One pool per URL, cached for the life of the process: a cache that opened a\n * connection per call would be slower than the thing it exists to speed up, and\n * a Lambda reusing a warm container should reuse the pool with it.\n *\n * `pg` is imported at the top of this module rather than lazily, because the\n * *module* is the lazy boundary — an app caching in Redis never imports\n * `@geekmidas/cache/postgres` and so never resolves the driver behind it. Same\n * arrangement `@geekmidas/storage/aws` uses for the AWS SDK.\n */\nexport const postgresCacheDriver: CacheDriver = {\n\tscheme: 'postgres:',\n\tcreate(url) {\n\t\t// The table travels in the URL, the way pg-boss's schema does.\n\t\t//\n\t\t// It has to, because a database-backed cache has no address of its own:\n\t\t// two caches in one database resolve the *same* connection string, so a\n\t\t// client built from the URL alone cannot tell them apart and both would\n\t\t// read and write one table — sharing a keyspace and evicting each other's\n\t\t// keys. The parameter is what makes the address identify the cache.\n\t\tconst { connectionString, table } = split(url);\n\n\t\tconst existing = pools.get(connectionString);\n\t\tconst pool = existing ?? new pg.Pool({ connectionString });\n\t\tif (!existing) pools.set(connectionString, pool);\n\n\t\t// One pool per database, shared by every cache in it — the table is the\n\t\t// client's, not the pool's.\n\t\treturn new PostgresCache(pool, table ? { table } : {});\n\t},\n};\n\n/**\n * A cache URL as its two facts: where to connect, and what to read.\n *\n * `table` is removed from the string handed to `pg` rather than left on it,\n * because a Postgres connection string carries libpq parameters and an unknown\n * one is not guaranteed to be ignored — and because the pool should be shared\n * by every cache in the database, which only works if the key does not vary\n * per cache.\n */\nfunction split(url: string): { connectionString: string; table?: string } {\n\ttry {\n\t\tconst parsed = new URL(url);\n\t\tconst table = parsed.searchParams.get('table') ?? undefined;\n\n\t\tif (!table) return { connectionString: url };\n\n\t\tparsed.searchParams.delete('table');\n\n\t\treturn { connectionString: parsed.toString(), table };\n\t} catch {\n\t\t// Not a URL `URL` can parse. The driver's job is not to validate one, so\n\t\t// this hands it to `pg` unchanged and lets that failure be the one\n\t\t// reported.\n\t\treturn { connectionString: url };\n\t}\n}\n\nconst pools = new Map<string, Pool>();\n\n/**\n * The DDL a Postgres-backed cache needs, for whoever applies DDL.\n *\n * Generation only, in the shape `@geekmidas/db/pg/roles` uses — the same\n * statements run in-process from `gkm dev` and from a provisioner inside a VPC,\n * and one implementation is what stops \"works locally, fails deployed\".\n *\n * Run as the schema's **owner**. The role a handler connects as cannot create a\n * table, which is the point of the split and the reason this is not done lazily.\n */\nexport function cacheTableStatements(options: PostgresCacheOptions = {}): {\n\tdescribe: string;\n\texists?: { sql: string; values: unknown[] };\n\tsql: string;\n}[] {\n\tconst table = options.table ?? DEFAULT_TABLE;\n\tconst [schema, name] = table.includes('.')\n\t\t? (table.split('.') as [string, string])\n\t\t: [undefined, table];\n\tconst qualified = quote(table);\n\n\treturn [\n\t\t{\n\t\t\tdescribe: `cache table ${table}`,\n\t\t\texists: {\n\t\t\t\tsql: schema\n\t\t\t\t\t? 'SELECT 1 FROM information_schema.tables WHERE table_schema = $1 AND table_name = $2'\n\t\t\t\t\t: 'SELECT 1 FROM information_schema.tables WHERE table_name = $1',\n\t\t\t\tvalues: schema ? [schema, name] : [name],\n\t\t\t},\n\t\t\tsql: `CREATE TABLE ${qualified} (\n\t\t\t\tkey text PRIMARY KEY,\n\t\t\t\tvalue jsonb NOT NULL,\n\t\t\t\texpires_at timestamptz\n\t\t\t)`,\n\t\t},\n\t\t{\n\t\t\t// Only the expiring rows: a partial index stays small in a cache whose\n\t\t\t// entries mostly never expire, and the sweep only ever looks at those.\n\t\t\tdescribe: `cache expiry index on ${table}`,\n\t\t\tsql: `CREATE INDEX IF NOT EXISTS ${indexName(table)}\n\t\t\t ON ${qualified} (expires_at) WHERE expires_at IS NOT NULL`,\n\t\t},\n\t];\n}\n\n/** A possibly schema-qualified name, quoted per segment. */\nfunction quote(table: string): string {\n\treturn table\n\t\t.split('.')\n\t\t.map((part) => `\"${part.replace(/\"/g, '\"\"')}\"`)\n\t\t.join('.');\n}\n\nfunction indexName(table: string): string {\n\treturn `\"${table.replace(/[^a-zA-Z0-9]/g, '_')}_expires_at_idx\"`;\n}\n"],"mappings":"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"}
@@ -1,9 +1,7 @@
1
- import { Cache, CacheDriver } from "./index-CNQ50bVk.cjs";
1
+ import { r as CacheDriver, t as Cache } from "./index-BbdGhmWP.cjs";
2
2
  import { Pool } from "pg";
3
-
4
3
  //#region src/postgres.d.ts
5
-
6
- interface PostgresCacheOptions {
4
+ export interface PostgresCacheOptions {
7
5
  /**
8
6
  * The table to keep entries in, schema-qualified if it needs to be.
9
7
  *
@@ -13,7 +11,7 @@ interface PostgresCacheOptions {
13
11
  */
14
12
  table?: string;
15
13
  }
16
- declare class PostgresCache implements Cache {
14
+ export declare class PostgresCache implements Cache {
17
15
  private readonly pool;
18
16
  private readonly table;
19
17
  constructor(pool: Pool, options?: PostgresCacheOptions);
@@ -50,7 +48,7 @@ declare class PostgresCache implements Cache {
50
48
  * `@geekmidas/cache/postgres` and so never resolves the driver behind it. Same
51
49
  * arrangement `@geekmidas/storage/aws` uses for the AWS SDK.
52
50
  */
53
- declare const postgresCacheDriver: CacheDriver;
51
+ export declare const postgresCacheDriver: CacheDriver;
54
52
  /**
55
53
  * The DDL a Postgres-backed cache needs, for whoever applies DDL.
56
54
  *
@@ -61,7 +59,7 @@ declare const postgresCacheDriver: CacheDriver;
61
59
  * Run as the schema's **owner**. The role a handler connects as cannot create a
62
60
  * table, which is the point of the split and the reason this is not done lazily.
63
61
  */
64
- declare function cacheTableStatements(options?: PostgresCacheOptions): {
62
+ export declare function cacheTableStatements(options?: PostgresCacheOptions): {
65
63
  describe: string;
66
64
  exists?: {
67
65
  sql: string;
@@ -69,7 +67,5 @@ declare function cacheTableStatements(options?: PostgresCacheOptions): {
69
67
  };
70
68
  sql: string;
71
69
  }[];
72
- //# sourceMappingURL=postgres.d.ts.map
73
70
  //#endregion
74
- export { PostgresCache, PostgresCacheOptions, cacheTableStatements, postgresCacheDriver };
75
71
  //# sourceMappingURL=postgres.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"postgres.d.cts","names":[],"sources":["../src/postgres.ts"],"sourcesContent":[],"mappings":";;;;;UA0BiB,oBAAA;;;;;;;;;;cAaJ,aAAA,YAAyB;;;oBAIb,gBACd;uBAKiB,QAAQ;6BAUF,kBAAkB;uBAgBxB;oBAIH;;;;;;;WAuBT;;;;;;;;;;;;;;;;;;;;;;;cAyCH,qBAAqB;;;;;;;;;;;iBA6DlB,oBAAA,WAA8B"}
1
+ {"version":3,"file":"postgres.d.cts","names":[],"sources":["../src/postgres.ts"],"mappings":";;;iBA2BiB;;;;;;;;EAQhB;;qBAKY,yBAAyB;mBAInB;mBAHD;EAEjB,YACkB,MAAM,MACvB,UAAS;EAKJ,IAAI,GAAG,cAAc,QAAQ;EAY7B,IAAI,GAAG,aAAa,OAAO,GAAG,eAAe;EAkB7C,OAAO,cAAc;EAQrB,IAAI,cAAc;;;;;;;EAuBlB,SAAS;UAQD;;;;;;;;UAgBN;;;;;;;;;;;;;;qBAiBI,qBAAqB;;;;;;;;;;;wBA6DlB,qBAAqB,UAAS;EAC7C;EACA;IAAW;IAAa;;EACxB"}
@@ -1,9 +1,7 @@
1
- import { Cache, CacheDriver } from "./index-1sNRIo6N.mjs";
1
+ import { r as CacheDriver, t as Cache } from "./index-BbdGhmWP.mjs";
2
2
  import { Pool } from "pg";
3
-
4
3
  //#region src/postgres.d.ts
5
-
6
- interface PostgresCacheOptions {
4
+ export interface PostgresCacheOptions {
7
5
  /**
8
6
  * The table to keep entries in, schema-qualified if it needs to be.
9
7
  *
@@ -13,7 +11,7 @@ interface PostgresCacheOptions {
13
11
  */
14
12
  table?: string;
15
13
  }
16
- declare class PostgresCache implements Cache {
14
+ export declare class PostgresCache implements Cache {
17
15
  private readonly pool;
18
16
  private readonly table;
19
17
  constructor(pool: Pool, options?: PostgresCacheOptions);
@@ -50,7 +48,7 @@ declare class PostgresCache implements Cache {
50
48
  * `@geekmidas/cache/postgres` and so never resolves the driver behind it. Same
51
49
  * arrangement `@geekmidas/storage/aws` uses for the AWS SDK.
52
50
  */
53
- declare const postgresCacheDriver: CacheDriver;
51
+ export declare const postgresCacheDriver: CacheDriver;
54
52
  /**
55
53
  * The DDL a Postgres-backed cache needs, for whoever applies DDL.
56
54
  *
@@ -61,7 +59,7 @@ declare const postgresCacheDriver: CacheDriver;
61
59
  * Run as the schema's **owner**. The role a handler connects as cannot create a
62
60
  * table, which is the point of the split and the reason this is not done lazily.
63
61
  */
64
- declare function cacheTableStatements(options?: PostgresCacheOptions): {
62
+ export declare function cacheTableStatements(options?: PostgresCacheOptions): {
65
63
  describe: string;
66
64
  exists?: {
67
65
  sql: string;
@@ -69,7 +67,5 @@ declare function cacheTableStatements(options?: PostgresCacheOptions): {
69
67
  };
70
68
  sql: string;
71
69
  }[];
72
- //# sourceMappingURL=postgres.d.ts.map
73
70
  //#endregion
74
- export { PostgresCache, PostgresCacheOptions, cacheTableStatements, postgresCacheDriver };
75
71
  //# sourceMappingURL=postgres.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"postgres.d.mts","names":[],"sources":["../src/postgres.ts"],"sourcesContent":[],"mappings":";;;;;UA0BiB,oBAAA;;;;;;;;;;cAaJ,aAAA,YAAyB;;;oBAIb,gBACd;uBAKiB,QAAQ;6BAUF,kBAAkB;uBAgBxB;oBAIH;;;;;;;WAuBT;;;;;;;;;;;;;;;;;;;;;;;cAyCH,qBAAqB;;;;;;;;;;;iBA6DlB,oBAAA,WAA8B"}
1
+ {"version":3,"file":"postgres.d.mts","names":[],"sources":["../src/postgres.ts"],"mappings":";;;iBA2BiB;;;;;;;;EAQhB;;qBAKY,yBAAyB;mBAInB;mBAHD;EAEjB,YACkB,MAAM,MACvB,UAAS;EAKJ,IAAI,GAAG,cAAc,QAAQ;EAY7B,IAAI,GAAG,aAAa,OAAO,GAAG,eAAe;EAkB7C,OAAO,cAAc;EAQrB,IAAI,cAAc;;;;;;;EAuBlB,SAAS;UAQD;;;;;;;;UAgBN;;;;;;;;;;;;;;qBAiBI,qBAAqB;;;;;;;;;;;wBA6DlB,qBAAqB,UAAS;EAC7C;EACA;IAAW;IAAa;;EACxB"}
package/dist/postgres.mjs CHANGED
@@ -1,30 +1,58 @@
1
+ import { traceCache } from "./telemetry.mjs";
1
2
  import pg from "pg";
2
-
3
3
  //#region src/postgres.ts
4
+ /**
5
+ * A cache that lives in a database you already have.
6
+ *
7
+ * The backend with no infrastructure. A cache is a table; the database is the
8
+ * one the application already declared, so an idle stage costs nothing extra
9
+ * and there is one fewer thing to provision, secure and pay for. It is slower
10
+ * than Redis and that is the trade: for session lookups, rate-limit counters and
11
+ * memoised API responses at ordinary volumes, a primary-key hit on a small table
12
+ * is not the thing that will be slow.
13
+ *
14
+ * **The table is provisioned, not created on first use.** That is the one place
15
+ * this differs from pg-boss, and the reason is the role split: a handler
16
+ * connects as a role that may read and write rows and may not create anything,
17
+ * so a driver issuing `CREATE TABLE` would fail on exactly the deployment where
18
+ * the roles are doing their job. The DDL is exported instead — see
19
+ * {@link cacheTableStatements} — and applied by whatever applies the rest.
20
+ *
21
+ * Reads filter on expiry and delete lazily, so an expired row is never returned
22
+ * even if nothing has swept it; a sweep is an optimisation, not a correctness
23
+ * requirement.
24
+ */
4
25
  const DEFAULT_TABLE = "cache";
5
26
  var PostgresCache = class {
27
+ pool;
6
28
  table;
7
29
  constructor(pool, options = {}) {
8
30
  this.pool = pool;
9
31
  this.table = options.table ?? DEFAULT_TABLE;
10
32
  }
11
33
  async get(key) {
12
- const { rows } = await this.query(`SELECT value FROM ${this.identifier()}
13
- WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())`, [key]);
14
- return rows[0]?.value;
34
+ return traceCache("postgresql", "get", async () => {
35
+ const { rows } = await this.query(`SELECT value FROM ${this.identifier()}
36
+ WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())`, [key]);
37
+ return rows[0]?.value;
38
+ });
15
39
  }
16
40
  async set(key, value, ttl) {
17
- await this.query(`INSERT INTO ${this.identifier()} (key, value, expires_at)
18
- VALUES ($1, $2::jsonb, ${ttl === void 0 ? "NULL" : "now() + ($3 || ' seconds')::interval"})
19
- ON CONFLICT (key) DO UPDATE
20
- SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at`, ttl === void 0 ? [key, JSON.stringify(value)] : [
21
- key,
22
- JSON.stringify(value),
23
- String(ttl)
24
- ]);
41
+ return traceCache("postgresql", "set", async () => {
42
+ await this.query(`INSERT INTO ${this.identifier()} (key, value, expires_at)
43
+ VALUES ($1, $2::jsonb, ${ttl === void 0 ? "NULL" : "now() + ($3 || ' seconds')::interval"})
44
+ ON CONFLICT (key) DO UPDATE
45
+ SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at`, ttl === void 0 ? [key, JSON.stringify(value)] : [
46
+ key,
47
+ JSON.stringify(value),
48
+ String(ttl)
49
+ ]);
50
+ });
25
51
  }
26
52
  async delete(key) {
27
- await this.query(`DELETE FROM ${this.identifier()} WHERE key = $1`, [key]);
53
+ return traceCache("postgresql", "delete", async () => {
54
+ await this.query(`DELETE FROM ${this.identifier()} WHERE key = $1`, [key]);
55
+ });
28
56
  }
29
57
  async ttl(key) {
30
58
  const { rows } = await this.query(`SELECT EXTRACT(EPOCH FROM (expires_at - now()))::int AS seconds
@@ -146,7 +174,7 @@ function quote(table) {
146
174
  function indexName(table) {
147
175
  return `"${table.replace(/[^a-zA-Z0-9]/g, "_")}_expires_at_idx"`;
148
176
  }
149
-
150
177
  //#endregion
151
178
  export { PostgresCache, cacheTableStatements, postgresCacheDriver };
179
+
152
180
  //# sourceMappingURL=postgres.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"postgres.mjs","names":["pool: Pool","options: PostgresCacheOptions","key: string","value: T","ttl?: number","sql: string","values: unknown[]","postgresCacheDriver: CacheDriver","url: string","table: string"],"sources":["../src/postgres.ts"],"sourcesContent":["/**\n * A cache that lives in a database you already have.\n *\n * The backend with no infrastructure. A cache is a table; the database is the\n * one the application already declared, so an idle stage costs nothing extra\n * and there is one fewer thing to provision, secure and pay for. It is slower\n * than Redis and that is the trade: for session lookups, rate-limit counters and\n * memoised API responses at ordinary volumes, a primary-key hit on a small table\n * is not the thing that will be slow.\n *\n * **The table is provisioned, not created on first use.** That is the one place\n * this differs from pg-boss, and the reason is the role split: a handler\n * connects as a role that may read and write rows and may not create anything,\n * so a driver issuing `CREATE TABLE` would fail on exactly the deployment where\n * the roles are doing their job. The DDL is exported instead — see\n * {@link cacheTableStatements} — and applied by whatever applies the rest.\n *\n * Reads filter on expiry and delete lazily, so an expired row is never returned\n * even if nothing has swept it; a sweep is an optimisation, not a correctness\n * requirement.\n */\n\nimport pg, { type Pool } from 'pg';\nimport type { Cache } from './index';\nimport type { CacheDriver } from './registry';\n\nexport interface PostgresCacheOptions {\n\t/**\n\t * The table to keep entries in, schema-qualified if it needs to be.\n\t *\n\t * Defaults to `cache`. It resolves against the connection's `search_path`,\n\t * which the role carries — so a tenant's cache lands in the tenant's schema\n\t * without anything here knowing the schema's name.\n\t */\n\ttable?: string;\n}\n\nconst DEFAULT_TABLE = 'cache';\n\nexport class PostgresCache implements Cache {\n\tprivate readonly table: string;\n\n\tconstructor(\n\t\tprivate readonly pool: Pool,\n\t\toptions: PostgresCacheOptions = {},\n\t) {\n\t\tthis.table = options.table ?? DEFAULT_TABLE;\n\t}\n\n\tasync get<T>(key: string): Promise<T | undefined> {\n\t\tconst { rows } = await this.query<{ value: T }>(\n\t\t\t`SELECT value FROM ${this.identifier()}\n\t\t\t WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())`,\n\t\t\t[key],\n\t\t);\n\n\t\treturn rows[0]?.value;\n\t}\n\n\tasync set<T>(key: string, value: T, ttl?: number): Promise<void> {\n\t\t// `now() + interval` computed by the server, not by this process: a\n\t\t// client whose clock is minutes off would otherwise write entries that\n\t\t// expire early or late, and clock skew is not a thing to debug through a\n\t\t// cache.\n\t\tawait this.query(\n\t\t\t`INSERT INTO ${this.identifier()} (key, value, expires_at)\n\t\t\t VALUES ($1, $2::jsonb, ${ttl === undefined ? 'NULL' : \"now() + ($3 || ' seconds')::interval\"})\n\t\t\t ON CONFLICT (key) DO UPDATE\n\t\t\t SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at`,\n\t\t\tttl === undefined\n\t\t\t\t? [key, JSON.stringify(value)]\n\t\t\t\t: [key, JSON.stringify(value), String(ttl)],\n\t\t);\n\t}\n\n\tasync delete(key: string): Promise<void> {\n\t\tawait this.query(`DELETE FROM ${this.identifier()} WHERE key = $1`, [key]);\n\t}\n\n\tasync ttl(key: string): Promise<number> {\n\t\tconst { rows } = await this.query<{ seconds: number | null }>(\n\t\t\t`SELECT EXTRACT(EPOCH FROM (expires_at - now()))::int AS seconds\n\t\t\t FROM ${this.identifier()}\n\t\t\t WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())`,\n\t\t\t[key],\n\t\t);\n\n\t\tconst row = rows[0];\n\t\tif (!row) return 0;\n\n\t\t// A row with no expiry has no time *left* to report. Zero is what the\n\t\t// interface says a missing key returns, and \"never expires\" is closer to\n\t\t// that than to any number this could invent.\n\t\treturn row.seconds ?? 0;\n\t}\n\n\t/**\n\t * Delete every expired entry.\n\t *\n\t * Optional: reads already ignore them. Worth running occasionally so the\n\t * table does not grow without bound in a workload that writes many keys once.\n\t */\n\tasync sweep(): Promise<number> {\n\t\tconst { rowCount } = await this.query(\n\t\t\t`DELETE FROM ${this.identifier()} WHERE expires_at <= now()`,\n\t\t);\n\n\t\treturn rowCount ?? 0;\n\t}\n\n\tprivate async query<T>(\n\t\tsql: string,\n\t\tvalues: unknown[] = [],\n\t): Promise<{ rows: T[]; rowCount: number | null }> {\n\t\tconst result = await this.pool.query(sql, values);\n\n\t\treturn { rows: result.rows as T[], rowCount: result.rowCount };\n\t}\n\n\t/**\n\t * The table name as an identifier.\n\t *\n\t * Quoted per segment so a schema-qualified name stays two identifiers rather\n\t * than becoming one with a dot in it. The name comes from configuration\n\t * rather than from a request, so this is defence rather than sanitisation.\n\t */\n\tprivate identifier(): string {\n\t\treturn quote(this.table);\n\t}\n}\n\n/**\n * The `postgres://` driver.\n *\n * One pool per URL, cached for the life of the process: a cache that opened a\n * connection per call would be slower than the thing it exists to speed up, and\n * a Lambda reusing a warm container should reuse the pool with it.\n *\n * `pg` is imported at the top of this module rather than lazily, because the\n * *module* is the lazy boundary — an app caching in Redis never imports\n * `@geekmidas/cache/postgres` and so never resolves the driver behind it. Same\n * arrangement `@geekmidas/storage/aws` uses for the AWS SDK.\n */\nexport const postgresCacheDriver: CacheDriver = {\n\tscheme: 'postgres:',\n\tcreate(url) {\n\t\t// The table travels in the URL, the way pg-boss's schema does.\n\t\t//\n\t\t// It has to, because a database-backed cache has no address of its own:\n\t\t// two caches in one database resolve the *same* connection string, so a\n\t\t// client built from the URL alone cannot tell them apart and both would\n\t\t// read and write one table — sharing a keyspace and evicting each other's\n\t\t// keys. The parameter is what makes the address identify the cache.\n\t\tconst { connectionString, table } = split(url);\n\n\t\tconst existing = pools.get(connectionString);\n\t\tconst pool = existing ?? new pg.Pool({ connectionString });\n\t\tif (!existing) pools.set(connectionString, pool);\n\n\t\t// One pool per database, shared by every cache in it — the table is the\n\t\t// client's, not the pool's.\n\t\treturn new PostgresCache(pool, table ? { table } : {});\n\t},\n};\n\n/**\n * A cache URL as its two facts: where to connect, and what to read.\n *\n * `table` is removed from the string handed to `pg` rather than left on it,\n * because a Postgres connection string carries libpq parameters and an unknown\n * one is not guaranteed to be ignored — and because the pool should be shared\n * by every cache in the database, which only works if the key does not vary\n * per cache.\n */\nfunction split(url: string): { connectionString: string; table?: string } {\n\ttry {\n\t\tconst parsed = new URL(url);\n\t\tconst table = parsed.searchParams.get('table') ?? undefined;\n\n\t\tif (!table) return { connectionString: url };\n\n\t\tparsed.searchParams.delete('table');\n\n\t\treturn { connectionString: parsed.toString(), table };\n\t} catch {\n\t\t// Not a URL `URL` can parse. The driver's job is not to validate one, so\n\t\t// this hands it to `pg` unchanged and lets that failure be the one\n\t\t// reported.\n\t\treturn { connectionString: url };\n\t}\n}\n\nconst pools = new Map<string, Pool>();\n\n/**\n * The DDL a Postgres-backed cache needs, for whoever applies DDL.\n *\n * Generation only, in the shape `@geekmidas/db/pg/roles` uses — the same\n * statements run in-process from `gkm dev` and from a provisioner inside a VPC,\n * and one implementation is what stops \"works locally, fails deployed\".\n *\n * Run as the schema's **owner**. The role a handler connects as cannot create a\n * table, which is the point of the split and the reason this is not done lazily.\n */\nexport function cacheTableStatements(options: PostgresCacheOptions = {}): {\n\tdescribe: string;\n\texists?: { sql: string; values: unknown[] };\n\tsql: string;\n}[] {\n\tconst table = options.table ?? DEFAULT_TABLE;\n\tconst [schema, name] = table.includes('.')\n\t\t? (table.split('.') as [string, string])\n\t\t: [undefined, table];\n\tconst qualified = quote(table);\n\n\treturn [\n\t\t{\n\t\t\tdescribe: `cache table ${table}`,\n\t\t\texists: {\n\t\t\t\tsql: schema\n\t\t\t\t\t? 'SELECT 1 FROM information_schema.tables WHERE table_schema = $1 AND table_name = $2'\n\t\t\t\t\t: 'SELECT 1 FROM information_schema.tables WHERE table_name = $1',\n\t\t\t\tvalues: schema ? [schema, name] : [name],\n\t\t\t},\n\t\t\tsql: `CREATE TABLE ${qualified} (\n\t\t\t\tkey text PRIMARY KEY,\n\t\t\t\tvalue jsonb NOT NULL,\n\t\t\t\texpires_at timestamptz\n\t\t\t)`,\n\t\t},\n\t\t{\n\t\t\t// Only the expiring rows: a partial index stays small in a cache whose\n\t\t\t// entries mostly never expire, and the sweep only ever looks at those.\n\t\t\tdescribe: `cache expiry index on ${table}`,\n\t\t\tsql: `CREATE INDEX IF NOT EXISTS ${indexName(table)}\n\t\t\t ON ${qualified} (expires_at) WHERE expires_at IS NOT NULL`,\n\t\t},\n\t];\n}\n\n/** A possibly schema-qualified name, quoted per segment. */\nfunction quote(table: string): string {\n\treturn table\n\t\t.split('.')\n\t\t.map((part) => `\"${part.replace(/\"/g, '\"\"')}\"`)\n\t\t.join('.');\n}\n\nfunction indexName(table: string): string {\n\treturn `\"${table.replace(/[^a-zA-Z0-9]/g, '_')}_expires_at_idx\"`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"postgres.mjs","names":[],"sources":["../src/postgres.ts"],"sourcesContent":["/**\n * A cache that lives in a database you already have.\n *\n * The backend with no infrastructure. A cache is a table; the database is the\n * one the application already declared, so an idle stage costs nothing extra\n * and there is one fewer thing to provision, secure and pay for. It is slower\n * than Redis and that is the trade: for session lookups, rate-limit counters and\n * memoised API responses at ordinary volumes, a primary-key hit on a small table\n * is not the thing that will be slow.\n *\n * **The table is provisioned, not created on first use.** That is the one place\n * this differs from pg-boss, and the reason is the role split: a handler\n * connects as a role that may read and write rows and may not create anything,\n * so a driver issuing `CREATE TABLE` would fail on exactly the deployment where\n * the roles are doing their job. The DDL is exported instead — see\n * {@link cacheTableStatements} — and applied by whatever applies the rest.\n *\n * Reads filter on expiry and delete lazily, so an expired row is never returned\n * even if nothing has swept it; a sweep is an optimisation, not a correctness\n * requirement.\n */\n\nimport pg, { type Pool } from 'pg';\nimport type { Cache } from './index';\nimport type { CacheDriver } from './registry';\nimport { traceCache } from './telemetry';\n\nexport interface PostgresCacheOptions {\n\t/**\n\t * The table to keep entries in, schema-qualified if it needs to be.\n\t *\n\t * Defaults to `cache`. It resolves against the connection's `search_path`,\n\t * which the role carries — so a tenant's cache lands in the tenant's schema\n\t * without anything here knowing the schema's name.\n\t */\n\ttable?: string;\n}\n\nconst DEFAULT_TABLE = 'cache';\n\nexport class PostgresCache implements Cache {\n\tprivate readonly table: string;\n\n\tconstructor(\n\t\tprivate readonly pool: Pool,\n\t\toptions: PostgresCacheOptions = {},\n\t) {\n\t\tthis.table = options.table ?? DEFAULT_TABLE;\n\t}\n\n\tasync get<T>(key: string): Promise<T | undefined> {\n\t\treturn traceCache('postgresql', 'get', async () => {\n\t\t\tconst { rows } = await this.query<{ value: T }>(\n\t\t\t\t`SELECT value FROM ${this.identifier()}\n\t\t\t\t WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())`,\n\t\t\t\t[key],\n\t\t\t);\n\n\t\t\treturn rows[0]?.value;\n\t\t});\n\t}\n\n\tasync set<T>(key: string, value: T, ttl?: number): Promise<void> {\n\t\treturn traceCache('postgresql', 'set', async () => {\n\t\t\t// `now() + interval` computed by the server, not by this process: a\n\t\t\t// client whose clock is minutes off would otherwise write entries that\n\t\t\t// expire early or late, and clock skew is not a thing to debug through a\n\t\t\t// cache.\n\t\t\tawait this.query(\n\t\t\t\t`INSERT INTO ${this.identifier()} (key, value, expires_at)\n\t\t\t\t VALUES ($1, $2::jsonb, ${ttl === undefined ? 'NULL' : \"now() + ($3 || ' seconds')::interval\"})\n\t\t\t\t ON CONFLICT (key) DO UPDATE\n\t\t\t\t SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at`,\n\t\t\t\tttl === undefined\n\t\t\t\t\t? [key, JSON.stringify(value)]\n\t\t\t\t\t: [key, JSON.stringify(value), String(ttl)],\n\t\t\t);\n\t\t});\n\t}\n\n\tasync delete(key: string): Promise<void> {\n\t\treturn traceCache('postgresql', 'delete', async () => {\n\t\t\tawait this.query(`DELETE FROM ${this.identifier()} WHERE key = $1`, [\n\t\t\t\tkey,\n\t\t\t]);\n\t\t});\n\t}\n\n\tasync ttl(key: string): Promise<number> {\n\t\tconst { rows } = await this.query<{ seconds: number | null }>(\n\t\t\t`SELECT EXTRACT(EPOCH FROM (expires_at - now()))::int AS seconds\n\t\t\t FROM ${this.identifier()}\n\t\t\t WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())`,\n\t\t\t[key],\n\t\t);\n\n\t\tconst row = rows[0];\n\t\tif (!row) return 0;\n\n\t\t// A row with no expiry has no time *left* to report. Zero is what the\n\t\t// interface says a missing key returns, and \"never expires\" is closer to\n\t\t// that than to any number this could invent.\n\t\treturn row.seconds ?? 0;\n\t}\n\n\t/**\n\t * Delete every expired entry.\n\t *\n\t * Optional: reads already ignore them. Worth running occasionally so the\n\t * table does not grow without bound in a workload that writes many keys once.\n\t */\n\tasync sweep(): Promise<number> {\n\t\tconst { rowCount } = await this.query(\n\t\t\t`DELETE FROM ${this.identifier()} WHERE expires_at <= now()`,\n\t\t);\n\n\t\treturn rowCount ?? 0;\n\t}\n\n\tprivate async query<T>(\n\t\tsql: string,\n\t\tvalues: unknown[] = [],\n\t): Promise<{ rows: T[]; rowCount: number | null }> {\n\t\tconst result = await this.pool.query(sql, values);\n\n\t\treturn { rows: result.rows as T[], rowCount: result.rowCount };\n\t}\n\n\t/**\n\t * The table name as an identifier.\n\t *\n\t * Quoted per segment so a schema-qualified name stays two identifiers rather\n\t * than becoming one with a dot in it. The name comes from configuration\n\t * rather than from a request, so this is defence rather than sanitisation.\n\t */\n\tprivate identifier(): string {\n\t\treturn quote(this.table);\n\t}\n}\n\n/**\n * The `postgres://` driver.\n *\n * One pool per URL, cached for the life of the process: a cache that opened a\n * connection per call would be slower than the thing it exists to speed up, and\n * a Lambda reusing a warm container should reuse the pool with it.\n *\n * `pg` is imported at the top of this module rather than lazily, because the\n * *module* is the lazy boundary — an app caching in Redis never imports\n * `@geekmidas/cache/postgres` and so never resolves the driver behind it. Same\n * arrangement `@geekmidas/storage/aws` uses for the AWS SDK.\n */\nexport const postgresCacheDriver: CacheDriver = {\n\tscheme: 'postgres:',\n\tcreate(url) {\n\t\t// The table travels in the URL, the way pg-boss's schema does.\n\t\t//\n\t\t// It has to, because a database-backed cache has no address of its own:\n\t\t// two caches in one database resolve the *same* connection string, so a\n\t\t// client built from the URL alone cannot tell them apart and both would\n\t\t// read and write one table — sharing a keyspace and evicting each other's\n\t\t// keys. The parameter is what makes the address identify the cache.\n\t\tconst { connectionString, table } = split(url);\n\n\t\tconst existing = pools.get(connectionString);\n\t\tconst pool = existing ?? new pg.Pool({ connectionString });\n\t\tif (!existing) pools.set(connectionString, pool);\n\n\t\t// One pool per database, shared by every cache in it — the table is the\n\t\t// client's, not the pool's.\n\t\treturn new PostgresCache(pool, table ? { table } : {});\n\t},\n};\n\n/**\n * A cache URL as its two facts: where to connect, and what to read.\n *\n * `table` is removed from the string handed to `pg` rather than left on it,\n * because a Postgres connection string carries libpq parameters and an unknown\n * one is not guaranteed to be ignored — and because the pool should be shared\n * by every cache in the database, which only works if the key does not vary\n * per cache.\n */\nfunction split(url: string): { connectionString: string; table?: string } {\n\ttry {\n\t\tconst parsed = new URL(url);\n\t\tconst table = parsed.searchParams.get('table') ?? undefined;\n\n\t\tif (!table) return { connectionString: url };\n\n\t\tparsed.searchParams.delete('table');\n\n\t\treturn { connectionString: parsed.toString(), table };\n\t} catch {\n\t\t// Not a URL `URL` can parse. The driver's job is not to validate one, so\n\t\t// this hands it to `pg` unchanged and lets that failure be the one\n\t\t// reported.\n\t\treturn { connectionString: url };\n\t}\n}\n\nconst pools = new Map<string, Pool>();\n\n/**\n * The DDL a Postgres-backed cache needs, for whoever applies DDL.\n *\n * Generation only, in the shape `@geekmidas/db/pg/roles` uses — the same\n * statements run in-process from `gkm dev` and from a provisioner inside a VPC,\n * and one implementation is what stops \"works locally, fails deployed\".\n *\n * Run as the schema's **owner**. The role a handler connects as cannot create a\n * table, which is the point of the split and the reason this is not done lazily.\n */\nexport function cacheTableStatements(options: PostgresCacheOptions = {}): {\n\tdescribe: string;\n\texists?: { sql: string; values: unknown[] };\n\tsql: string;\n}[] {\n\tconst table = options.table ?? DEFAULT_TABLE;\n\tconst [schema, name] = table.includes('.')\n\t\t? (table.split('.') as [string, string])\n\t\t: [undefined, table];\n\tconst qualified = quote(table);\n\n\treturn [\n\t\t{\n\t\t\tdescribe: `cache table ${table}`,\n\t\t\texists: {\n\t\t\t\tsql: schema\n\t\t\t\t\t? 'SELECT 1 FROM information_schema.tables WHERE table_schema = $1 AND table_name = $2'\n\t\t\t\t\t: 'SELECT 1 FROM information_schema.tables WHERE table_name = $1',\n\t\t\t\tvalues: schema ? [schema, name] : [name],\n\t\t\t},\n\t\t\tsql: `CREATE TABLE ${qualified} (\n\t\t\t\tkey text PRIMARY KEY,\n\t\t\t\tvalue jsonb NOT NULL,\n\t\t\t\texpires_at timestamptz\n\t\t\t)`,\n\t\t},\n\t\t{\n\t\t\t// Only the expiring rows: a partial index stays small in a cache whose\n\t\t\t// entries mostly never expire, and the sweep only ever looks at those.\n\t\t\tdescribe: `cache expiry index on ${table}`,\n\t\t\tsql: `CREATE INDEX IF NOT EXISTS ${indexName(table)}\n\t\t\t ON ${qualified} (expires_at) WHERE expires_at IS NOT NULL`,\n\t\t},\n\t];\n}\n\n/** A possibly schema-qualified name, quoted per segment. */\nfunction quote(table: string): string {\n\treturn table\n\t\t.split('.')\n\t\t.map((part) => `\"${part.replace(/\"/g, '\"\"')}\"`)\n\t\t.join('.');\n}\n\nfunction indexName(table: string): string {\n\treturn `\"${table.replace(/[^a-zA-Z0-9]/g, '_')}_expires_at_idx\"`;\n}\n"],"mappings":"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"}
package/dist/redis.cjs CHANGED
@@ -1,30 +1,51 @@
1
- const require_chunk = require('./chunk-CUT6urMc.cjs');
2
- const ioredis = require_chunk.__toESM(require("ioredis"));
3
-
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_telemetry = require("./telemetry.cjs");
3
+ let ioredis = require("ioredis");
4
4
  //#region src/redis.ts
5
+ /**
6
+ * A cache over the Redis wire protocol — ElastiCache, Valkey, or a Redis you run.
7
+ *
8
+ * The sibling of the Upstash client rather than a replacement for it. Upstash
9
+ * speaks HTTP, which is what makes it reachable from a Lambda with no VPC and
10
+ * no connection pool; this speaks the wire protocol, which is what ElastiCache
11
+ * offers and what a container next to your app is cheapest on.
12
+ *
13
+ * Choosing one is a deployment decision, and the local target follows it: pick
14
+ * `elasticache` and dev runs plain Redis, pick `upstash` and dev runs the HTTP
15
+ * proxy in front of it. Either way dev and prod speak the same protocol, which
16
+ * is the property that matters — a cache that behaves differently in the two
17
+ * places is worse than a slower one.
18
+ */
5
19
  var RedisCache = class {
20
+ client;
6
21
  constructor(client) {
7
22
  this.client = client;
8
23
  }
9
24
  async get(key) {
10
- const value = await this.client.get(key);
11
- if (value === null) return void 0;
12
- try {
13
- return JSON.parse(value);
14
- } catch {
15
- return value;
16
- }
25
+ return require_telemetry.traceCache("redis", "get", async () => {
26
+ const value = await this.client.get(key);
27
+ if (value === null) return void 0;
28
+ try {
29
+ return JSON.parse(value);
30
+ } catch {
31
+ return value;
32
+ }
33
+ });
17
34
  }
18
35
  async set(key, value, ttl) {
19
- const encoded = JSON.stringify(value);
20
- if (ttl === void 0) {
21
- await this.client.set(key, encoded);
22
- return;
23
- }
24
- await this.client.set(key, encoded, "EX", ttl);
36
+ return require_telemetry.traceCache("redis", "set", async () => {
37
+ const encoded = JSON.stringify(value);
38
+ if (ttl === void 0) {
39
+ await this.client.set(key, encoded);
40
+ return;
41
+ }
42
+ await this.client.set(key, encoded, "EX", ttl);
43
+ });
25
44
  }
26
45
  async delete(key) {
27
- await this.client.del(key);
46
+ return require_telemetry.traceCache("redis", "delete", async () => {
47
+ await this.client.del(key);
48
+ });
28
49
  }
29
50
  async ttl(key) {
30
51
  const seconds = await this.client.ttl(key);
@@ -55,16 +76,13 @@ const clients = /* @__PURE__ */ new Map();
55
76
  function clientFor(url) {
56
77
  const existing = clients.get(url);
57
78
  if (existing) return existing;
58
- const client = new ioredis.Redis(url, {
59
- maxRetriesPerRequest: 2,
60
- enableOfflineQueue: false
61
- });
79
+ const client = new ioredis.Redis(url, { maxRetriesPerRequest: 2 });
62
80
  clients.set(url, client);
63
81
  return client;
64
82
  }
65
-
66
83
  //#endregion
67
84
  exports.RedisCache = RedisCache;
68
85
  exports.redisCacheDriver = redisCacheDriver;
69
86
  exports.redissCacheDriver = redissCacheDriver;
87
+
70
88
  //# sourceMappingURL=redis.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"redis.cjs","names":["client: Redis","key: string","value: T","ttl?: number","redisCacheDriver: CacheDriver","redissCacheDriver: CacheDriver","url: string","Redis"],"sources":["../src/redis.ts"],"sourcesContent":["/**\n * A cache over the Redis wire protocol — ElastiCache, Valkey, or a Redis you run.\n *\n * The sibling of the Upstash client rather than a replacement for it. Upstash\n * speaks HTTP, which is what makes it reachable from a Lambda with no VPC and\n * no connection pool; this speaks the wire protocol, which is what ElastiCache\n * offers and what a container next to your app is cheapest on.\n *\n * Choosing one is a deployment decision, and the local target follows it: pick\n * `elasticache` and dev runs plain Redis, pick `upstash` and dev runs the HTTP\n * proxy in front of it. Either way dev and prod speak the same protocol, which\n * is the property that matters — a cache that behaves differently in the two\n * places is worse than a slower one.\n */\n\nimport { Redis } from 'ioredis';\nimport type { Cache } from './index';\nimport type { CacheDriver } from './registry';\n\nexport class RedisCache implements Cache {\n\tconstructor(private readonly client: Redis) {}\n\n\tasync get<T>(key: string): Promise<T | undefined> {\n\t\tconst value = await this.client.get(key);\n\t\tif (value === null) return undefined;\n\n\t\ttry {\n\t\t\treturn JSON.parse(value) as T;\n\t\t} catch {\n\t\t\t// Written by something that did not go through `set`. Returning the\n\t\t\t// raw string is more useful than throwing on a value somebody put\n\t\t\t// there on purpose.\n\t\t\treturn value as unknown as T;\n\t\t}\n\t}\n\n\tasync set<T>(key: string, value: T, ttl?: number): Promise<void> {\n\t\tconst encoded = JSON.stringify(value);\n\n\t\tif (ttl === undefined) {\n\t\t\tawait this.client.set(key, encoded);\n\t\t\treturn;\n\t\t}\n\n\t\tawait this.client.set(key, encoded, 'EX', ttl);\n\t}\n\n\tasync delete(key: string): Promise<void> {\n\t\tawait this.client.del(key);\n\t}\n\n\tasync ttl(key: string): Promise<number> {\n\t\tconst seconds = await this.client.ttl(key);\n\n\t\t// Redis reports -2 for a key that does not exist and -1 for one with no\n\t\t// expiry. The interface says zero for a missing key, and \"never expires\"\n\t\t// has no time left to report either — so both become zero rather than\n\t\t// leaking sentinel values a caller would have to know about.\n\t\treturn seconds < 0 ? 0 : seconds;\n\t}\n}\n\n/**\n * The `redis://` and `rediss://` driver.\n *\n * One client per URL for the life of the process — a Redis connection is a\n * socket, and opening one per cache call is the thing that makes people\n * conclude a cache is not worth having.\n *\n * `ioredis` is imported at the top of this module because the *module* is the\n * lazy boundary: an app caching in Postgres never imports\n * `@geekmidas/cache/redis`, and so never resolves it.\n */\nexport const redisCacheDriver: CacheDriver = {\n\tscheme: 'redis:',\n\tcreate: (url) => new RedisCache(clientFor(url)),\n};\n\n/** The TLS form. ElastiCache Serverless requires it; the same client handles both. */\nexport const redissCacheDriver: CacheDriver = {\n\tscheme: 'rediss:',\n\tcreate: (url) => new RedisCache(clientFor(url)),\n};\n\nconst clients = new Map<string, Redis>();\n\nfunction clientFor(url: string): Redis {\n\tconst existing = clients.get(url);\n\tif (existing) return existing;\n\n\tconst client = new Redis(url, {\n\t\t// A cache miss is recoverable and a hung request is not. Failing fast and\n\t\t// letting the caller treat it as a miss is the behaviour a cache should\n\t\t// have; retrying forever turns a degraded cache into a degraded app.\n\t\tmaxRetriesPerRequest: 2,\n\t\tenableOfflineQueue: false,\n\t});\n\n\tclients.set(url, client);\n\n\treturn client;\n}\n"],"mappings":";;;;AAmBA,IAAa,aAAb,MAAyC;CACxC,YAA6BA,QAAe;EAAf;CAAiB;CAE9C,MAAM,IAAOC,KAAqC;EACjD,MAAM,QAAQ,MAAM,KAAK,OAAO,IAAI,IAAI;AACxC,MAAI,UAAU,KAAM;AAEpB,MAAI;AACH,UAAO,KAAK,MAAM,MAAM;EACxB,QAAO;AAIP,UAAO;EACP;CACD;CAED,MAAM,IAAOA,KAAaC,OAAUC,KAA6B;EAChE,MAAM,UAAU,KAAK,UAAU,MAAM;AAErC,MAAI,gBAAmB;AACtB,SAAM,KAAK,OAAO,IAAI,KAAK,QAAQ;AACnC;EACA;AAED,QAAM,KAAK,OAAO,IAAI,KAAK,SAAS,MAAM,IAAI;CAC9C;CAED,MAAM,OAAOF,KAA4B;AACxC,QAAM,KAAK,OAAO,IAAI,IAAI;CAC1B;CAED,MAAM,IAAIA,KAA8B;EACvC,MAAM,UAAU,MAAM,KAAK,OAAO,IAAI,IAAI;AAM1C,SAAO,UAAU,IAAI,IAAI;CACzB;AACD;;;;;;;;;;;;AAaD,MAAaG,mBAAgC;CAC5C,QAAQ;CACR,QAAQ,CAAC,QAAQ,IAAI,WAAW,UAAU,IAAI;AAC9C;;AAGD,MAAaC,oBAAiC;CAC7C,QAAQ;CACR,QAAQ,CAAC,QAAQ,IAAI,WAAW,UAAU,IAAI;AAC9C;AAED,MAAM,0BAAU,IAAI;AAEpB,SAAS,UAAUC,KAAoB;CACtC,MAAM,WAAW,QAAQ,IAAI,IAAI;AACjC,KAAI,SAAU,QAAO;CAErB,MAAM,SAAS,IAAIC,cAAM,KAAK;EAI7B,sBAAsB;EACtB,oBAAoB;CACpB;AAED,SAAQ,IAAI,KAAK,OAAO;AAExB,QAAO;AACP"}
1
+ {"version":3,"file":"redis.cjs","names":["traceCache","Redis"],"sources":["../src/redis.ts"],"sourcesContent":["/**\n * A cache over the Redis wire protocol — ElastiCache, Valkey, or a Redis you run.\n *\n * The sibling of the Upstash client rather than a replacement for it. Upstash\n * speaks HTTP, which is what makes it reachable from a Lambda with no VPC and\n * no connection pool; this speaks the wire protocol, which is what ElastiCache\n * offers and what a container next to your app is cheapest on.\n *\n * Choosing one is a deployment decision, and the local target follows it: pick\n * `elasticache` and dev runs plain Redis, pick `upstash` and dev runs the HTTP\n * proxy in front of it. Either way dev and prod speak the same protocol, which\n * is the property that matters — a cache that behaves differently in the two\n * places is worse than a slower one.\n */\n\nimport { Redis } from 'ioredis';\nimport type { Cache } from './index';\nimport type { CacheDriver } from './registry';\nimport { traceCache } from './telemetry';\n\nexport class RedisCache implements Cache {\n\tconstructor(private readonly client: Redis) {}\n\n\tasync get<T>(key: string): Promise<T | undefined> {\n\t\treturn traceCache('redis', 'get', async () => {\n\t\t\tconst value = await this.client.get(key);\n\t\t\tif (value === null) return undefined;\n\n\t\t\ttry {\n\t\t\t\treturn JSON.parse(value) as T;\n\t\t\t} catch {\n\t\t\t\t// Written by something that did not go through `set`. Returning the\n\t\t\t\t// raw string is more useful than throwing on a value somebody put\n\t\t\t\t// there on purpose.\n\t\t\t\treturn value as unknown as T;\n\t\t\t}\n\t\t});\n\t}\n\n\tasync set<T>(key: string, value: T, ttl?: number): Promise<void> {\n\t\treturn traceCache('redis', 'set', async () => {\n\t\t\tconst encoded = JSON.stringify(value);\n\n\t\t\tif (ttl === undefined) {\n\t\t\t\tawait this.client.set(key, encoded);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tawait this.client.set(key, encoded, 'EX', ttl);\n\t\t});\n\t}\n\n\tasync delete(key: string): Promise<void> {\n\t\treturn traceCache('redis', 'delete', async () => {\n\t\t\tawait this.client.del(key);\n\t\t});\n\t}\n\n\tasync ttl(key: string): Promise<number> {\n\t\tconst seconds = await this.client.ttl(key);\n\n\t\t// Redis reports -2 for a key that does not exist and -1 for one with no\n\t\t// expiry. The interface says zero for a missing key, and \"never expires\"\n\t\t// has no time left to report either — so both become zero rather than\n\t\t// leaking sentinel values a caller would have to know about.\n\t\treturn seconds < 0 ? 0 : seconds;\n\t}\n}\n\n/**\n * The `redis://` and `rediss://` driver.\n *\n * One client per URL for the life of the process — a Redis connection is a\n * socket, and opening one per cache call is the thing that makes people\n * conclude a cache is not worth having.\n *\n * `ioredis` is imported at the top of this module because the *module* is the\n * lazy boundary: an app caching in Postgres never imports\n * `@geekmidas/cache/redis`, and so never resolves it.\n */\nexport const redisCacheDriver: CacheDriver = {\n\tscheme: 'redis:',\n\tcreate: (url) => new RedisCache(clientFor(url)),\n};\n\n/** The TLS form. ElastiCache Serverless requires it; the same client handles both. */\nexport const redissCacheDriver: CacheDriver = {\n\tscheme: 'rediss:',\n\tcreate: (url) => new RedisCache(clientFor(url)),\n};\n\nconst clients = new Map<string, Redis>();\n\nfunction clientFor(url: string): Redis {\n\tconst existing = clients.get(url);\n\tif (existing) return existing;\n\n\tconst client = new Redis(url, {\n\t\t// A cache miss is recoverable and a hung request is not. Failing fast and\n\t\t// letting the caller treat it as a miss is the behaviour a cache should\n\t\t// have; retrying forever turns a degraded cache into a degraded app.\n\t\t//\n\t\t// The offline queue stays on: the socket connects after the constructor\n\t\t// returns, so without it the first command of every fresh client — every\n\t\t// cold start — is rejected before the connection has had a chance. The\n\t\t// retry bound above is what keeps a queued command from waiting forever.\n\t\tmaxRetriesPerRequest: 2,\n\t});\n\n\tclients.set(url, client);\n\n\treturn client;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAoBA,IAAa,aAAb,MAAyC;CACX;CAA7B,YAAY,QAAgC;EAAf,KAAA,SAAA;CAAgB;CAE7C,MAAM,IAAO,KAAqC;EACjD,OAAOA,kBAAAA,WAAW,SAAS,OAAO,YAAY;GAC7C,MAAM,QAAQ,MAAM,KAAK,OAAO,IAAI,GAAG;GACvC,IAAI,UAAU,MAAM,OAAO,KAAA;GAE3B,IAAI;IACH,OAAO,KAAK,MAAM,KAAK;GACxB,QAAQ;IAIP,OAAO;GACR;EACD,CAAC;CACF;CAEA,MAAM,IAAO,KAAa,OAAU,KAA6B;EAChE,OAAOA,kBAAAA,WAAW,SAAS,OAAO,YAAY;GAC7C,MAAM,UAAU,KAAK,UAAU,KAAK;GAEpC,IAAI,QAAQ,KAAA,GAAW;IACtB,MAAM,KAAK,OAAO,IAAI,KAAK,OAAO;IAClC;GACD;GAEA,MAAM,KAAK,OAAO,IAAI,KAAK,SAAS,MAAM,GAAG;EAC9C,CAAC;CACF;CAEA,MAAM,OAAO,KAA4B;EACxC,OAAOA,kBAAAA,WAAW,SAAS,UAAU,YAAY;GAChD,MAAM,KAAK,OAAO,IAAI,GAAG;EAC1B,CAAC;CACF;CAEA,MAAM,IAAI,KAA8B;EACvC,MAAM,UAAU,MAAM,KAAK,OAAO,IAAI,GAAG;EAMzC,OAAO,UAAU,IAAI,IAAI;CAC1B;AACD;;;;;;;;;;;;AAaA,MAAa,mBAAgC;CAC5C,QAAQ;CACR,SAAS,QAAQ,IAAI,WAAW,UAAU,GAAG,CAAC;AAC/C;;AAGA,MAAa,oBAAiC;CAC7C,QAAQ;CACR,SAAS,QAAQ,IAAI,WAAW,UAAU,GAAG,CAAC;AAC/C;AAEA,MAAM,0BAAU,IAAI,IAAmB;AAEvC,SAAS,UAAU,KAAoB;CACtC,MAAM,WAAW,QAAQ,IAAI,GAAG;CAChC,IAAI,UAAU,OAAO;CAErB,MAAM,SAAS,IAAIC,QAAAA,MAAM,KAAK,EAS7B,sBAAsB,EACvB,CAAC;CAED,QAAQ,IAAI,KAAK,MAAM;CAEvB,OAAO;AACR"}