@geekmidas/cache 9.0.2 → 10.0.0-alpha.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/dist/expo.cjs +2 -1
- package/dist/expo.cjs.map +1 -1
- package/dist/expo.d.cts +1 -1
- package/dist/expo.d.mts +1 -1
- package/dist/expo.mjs +2 -1
- package/dist/expo.mjs.map +1 -1
- package/dist/file.cjs +2 -1
- package/dist/file.cjs.map +1 -1
- package/dist/file.d.cts +1 -1
- package/dist/file.d.mts +1 -1
- package/dist/file.mjs +2 -1
- package/dist/file.mjs.map +1 -1
- package/dist/index-1sNRIo6N.d.mts +64 -0
- package/dist/index-1sNRIo6N.d.mts.map +1 -0
- package/dist/index-CNQ50bVk.d.cts +64 -0
- package/dist/index-CNQ50bVk.d.cts.map +1 -0
- package/dist/index.cjs +7 -2
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +3 -2
- package/dist/memory.cjs +2 -1
- package/dist/memory.cjs.map +1 -1
- package/dist/memory.d.cts +1 -1
- package/dist/memory.d.mts +1 -1
- package/dist/memory.mjs +2 -1
- package/dist/memory.mjs.map +1 -1
- package/dist/postgres.cjs +155 -0
- package/dist/postgres.cjs.map +1 -0
- package/dist/postgres.d.cts +75 -0
- package/dist/postgres.d.cts.map +1 -0
- package/dist/postgres.d.mts +75 -0
- package/dist/postgres.d.mts.map +1 -0
- package/dist/postgres.mjs +152 -0
- package/dist/postgres.mjs.map +1 -0
- package/dist/redis.cjs +70 -0
- package/dist/redis.cjs.map +1 -0
- package/dist/redis.d.cts +31 -0
- package/dist/redis.d.cts.map +1 -0
- package/dist/redis.d.mts +31 -0
- package/dist/redis.d.mts.map +1 -0
- package/dist/redis.mjs +67 -0
- package/dist/redis.mjs.map +1 -0
- package/dist/registry-CITg73zW.cjs +65 -0
- package/dist/registry-CITg73zW.cjs.map +1 -0
- package/dist/registry-CT0b-rjB.mjs +41 -0
- package/dist/registry-CT0b-rjB.mjs.map +1 -0
- package/dist/registry.cjs +6 -0
- package/dist/registry.d.cts +2 -0
- package/dist/registry.d.mts +2 -0
- package/dist/registry.mjs +3 -0
- package/dist/{src-CrE4Nl5T.mjs → src-DVpoP03o.mjs} +1 -1
- package/dist/{src-CrE4Nl5T.mjs.map → src-DVpoP03o.mjs.map} +1 -1
- package/dist/{src-zfYb7mdS.cjs → src-ysNIqjnr.cjs} +1 -1
- package/dist/{src-zfYb7mdS.cjs.map → src-ysNIqjnr.cjs.map} +1 -1
- package/dist/upstash.cjs +41 -0
- package/dist/upstash.cjs.map +1 -1
- package/dist/upstash.d.cts +27 -3
- package/dist/upstash.d.cts.map +1 -1
- package/dist/upstash.d.mts +27 -3
- package/dist/upstash.d.mts.map +1 -1
- package/dist/upstash.mjs +39 -1
- package/dist/upstash.mjs.map +1 -1
- package/package.json +45 -3
- package/CHANGELOG.md +0 -50
- package/dist/index-BSYYdwRm.d.cts +0 -36
- package/dist/index-BSYYdwRm.d.cts.map +0 -1
- package/dist/index-Dx96gr-9.d.mts +0 -36
- package/dist/index-Dx96gr-9.d.mts.map +0 -1
- package/src/__benchmarks__/memory.bench.ts +0 -51
- package/src/__tests__/cache.spec.ts +0 -144
- package/src/__tests__/file.spec.ts +0 -192
- package/src/__tests__/integration.spec.ts +0 -282
- package/src/__tests__/memory.spec.ts +0 -134
- package/src/__tests__/upstash.spec.ts +0 -276
- package/src/expo.ts +0 -50
- package/src/file.ts +0 -126
- package/src/index.ts +0 -48
- package/src/memory.ts +0 -38
- package/src/upstash.ts +0 -41
- package/tsconfig.json +0 -9
- package/tsdown.config.ts +0 -5
package/dist/expo.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const require_chunk = require('./chunk-CUT6urMc.cjs');
|
|
2
|
-
|
|
2
|
+
require('./registry-CITg73zW.cjs');
|
|
3
|
+
const require_src = require('./src-ysNIqjnr.cjs');
|
|
3
4
|
const date_fns = require_chunk.__toESM(require("date-fns"));
|
|
4
5
|
const expo_secure_store = require_chunk.__toESM(require("expo-secure-store"));
|
|
5
6
|
|
package/dist/expo.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"expo.cjs","names":["key: string","value: T","ttl: number"],"sources":["../src/expo.ts"],"sourcesContent":["import { addSeconds } from 'date-fns';\nimport * as SecureStore from 'expo-secure-store';\nimport { type Cache, getExpirationInSeconds } from './index';\n\nexport class ExpoSecureCache implements Cache {\n\tstatic getExpiryKey(key: string): string {\n\t\treturn `${key}:expiresAt`;\n\t}\n\n\tasync ttl(key: string): Promise<number> {\n\t\tconst result = await SecureStore.getItemAsync(\n\t\t\tExpoSecureCache.getExpiryKey(key),\n\t\t);\n\t\tif (!result) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tconst secondsLeft = getExpirationInSeconds(result);\n\n\t\treturn Math.max(secondsLeft, 0);\n\t}\n\n\tasync get<T>(key: string): Promise<T | undefined> {\n\t\tconst result = await SecureStore.getItemAsync(key);\n\t\tif (!result) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst expiresAt = await this.ttl(key);\n\t\tif (expiresAt === 0) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn JSON.parse(result) as T;\n\t}\n\n\tasync set<T>(key: string, value: T, ttl: number = 600): Promise<void> {\n\t\tconst expiresAt = addSeconds(new Date(), ttl).toISOString();\n\t\tawait SecureStore.setItemAsync(key, JSON.stringify(value));\n\t\tawait SecureStore.setItemAsync(\n\t\t\tExpoSecureCache.getExpiryKey(key),\n\t\t\texpiresAt,\n\t\t);\n\t}\n\n\tasync delete(key: string): Promise<void> {\n\t\tawait SecureStore.deleteItemAsync(key);\n\t\tawait SecureStore.deleteItemAsync(ExpoSecureCache.getExpiryKey(key));\n\t}\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"expo.cjs","names":["key: string","value: T","ttl: number"],"sources":["../src/expo.ts"],"sourcesContent":["import { addSeconds } from 'date-fns';\nimport * as SecureStore from 'expo-secure-store';\nimport { type Cache, getExpirationInSeconds } from './index';\n\nexport class ExpoSecureCache implements Cache {\n\tstatic getExpiryKey(key: string): string {\n\t\treturn `${key}:expiresAt`;\n\t}\n\n\tasync ttl(key: string): Promise<number> {\n\t\tconst result = await SecureStore.getItemAsync(\n\t\t\tExpoSecureCache.getExpiryKey(key),\n\t\t);\n\t\tif (!result) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tconst secondsLeft = getExpirationInSeconds(result);\n\n\t\treturn Math.max(secondsLeft, 0);\n\t}\n\n\tasync get<T>(key: string): Promise<T | undefined> {\n\t\tconst result = await SecureStore.getItemAsync(key);\n\t\tif (!result) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst expiresAt = await this.ttl(key);\n\t\tif (expiresAt === 0) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn JSON.parse(result) as T;\n\t}\n\n\tasync set<T>(key: string, value: T, ttl: number = 600): Promise<void> {\n\t\tconst expiresAt = addSeconds(new Date(), ttl).toISOString();\n\t\tawait SecureStore.setItemAsync(key, JSON.stringify(value));\n\t\tawait SecureStore.setItemAsync(\n\t\t\tExpoSecureCache.getExpiryKey(key),\n\t\t\texpiresAt,\n\t\t);\n\t}\n\n\tasync delete(key: string): Promise<void> {\n\t\tawait SecureStore.deleteItemAsync(key);\n\t\tawait SecureStore.deleteItemAsync(ExpoSecureCache.getExpiryKey(key));\n\t}\n}\n"],"mappings":";;;;;;;AAIA,IAAa,kBAAb,MAAa,gBAAiC;CAC7C,OAAO,aAAaA,KAAqB;AACxC,UAAQ,EAAE,IAAI;CACd;CAED,MAAM,IAAIA,KAA8B;EACvC,MAAM,SAAS,MAAM,kBAAY,aAChC,gBAAgB,aAAa,IAAI,CACjC;AACD,OAAK,OACJ,QAAO;EAGR,MAAM,cAAc,mCAAuB,OAAO;AAElD,SAAO,KAAK,IAAI,aAAa,EAAE;CAC/B;CAED,MAAM,IAAOA,KAAqC;EACjD,MAAM,SAAS,MAAM,kBAAY,aAAa,IAAI;AAClD,OAAK,OACJ;EAGD,MAAM,YAAY,MAAM,KAAK,IAAI,IAAI;AACrC,MAAI,cAAc,EACjB;AAGD,SAAO,KAAK,MAAM,OAAO;CACzB;CAED,MAAM,IAAOA,KAAaC,OAAUC,MAAc,KAAoB;EACrE,MAAM,YAAY,yCAAW,IAAI,QAAQ,IAAI,CAAC,aAAa;AAC3D,QAAM,kBAAY,aAAa,KAAK,KAAK,UAAU,MAAM,CAAC;AAC1D,QAAM,kBAAY,aACjB,gBAAgB,aAAa,IAAI,EACjC,UACA;CACD;CAED,MAAM,OAAOF,KAA4B;AACxC,QAAM,kBAAY,gBAAgB,IAAI;AACtC,QAAM,kBAAY,gBAAgB,gBAAgB,aAAa,IAAI,CAAC;CACpE;AACD"}
|
package/dist/expo.d.cts
CHANGED
package/dist/expo.d.mts
CHANGED
package/dist/expo.mjs
CHANGED
package/dist/expo.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"expo.mjs","names":["key: string","value: T","ttl: number"],"sources":["../src/expo.ts"],"sourcesContent":["import { addSeconds } from 'date-fns';\nimport * as SecureStore from 'expo-secure-store';\nimport { type Cache, getExpirationInSeconds } from './index';\n\nexport class ExpoSecureCache implements Cache {\n\tstatic getExpiryKey(key: string): string {\n\t\treturn `${key}:expiresAt`;\n\t}\n\n\tasync ttl(key: string): Promise<number> {\n\t\tconst result = await SecureStore.getItemAsync(\n\t\t\tExpoSecureCache.getExpiryKey(key),\n\t\t);\n\t\tif (!result) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tconst secondsLeft = getExpirationInSeconds(result);\n\n\t\treturn Math.max(secondsLeft, 0);\n\t}\n\n\tasync get<T>(key: string): Promise<T | undefined> {\n\t\tconst result = await SecureStore.getItemAsync(key);\n\t\tif (!result) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst expiresAt = await this.ttl(key);\n\t\tif (expiresAt === 0) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn JSON.parse(result) as T;\n\t}\n\n\tasync set<T>(key: string, value: T, ttl: number = 600): Promise<void> {\n\t\tconst expiresAt = addSeconds(new Date(), ttl).toISOString();\n\t\tawait SecureStore.setItemAsync(key, JSON.stringify(value));\n\t\tawait SecureStore.setItemAsync(\n\t\t\tExpoSecureCache.getExpiryKey(key),\n\t\t\texpiresAt,\n\t\t);\n\t}\n\n\tasync delete(key: string): Promise<void> {\n\t\tawait SecureStore.deleteItemAsync(key);\n\t\tawait SecureStore.deleteItemAsync(ExpoSecureCache.getExpiryKey(key));\n\t}\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"expo.mjs","names":["key: string","value: T","ttl: number"],"sources":["../src/expo.ts"],"sourcesContent":["import { addSeconds } from 'date-fns';\nimport * as SecureStore from 'expo-secure-store';\nimport { type Cache, getExpirationInSeconds } from './index';\n\nexport class ExpoSecureCache implements Cache {\n\tstatic getExpiryKey(key: string): string {\n\t\treturn `${key}:expiresAt`;\n\t}\n\n\tasync ttl(key: string): Promise<number> {\n\t\tconst result = await SecureStore.getItemAsync(\n\t\t\tExpoSecureCache.getExpiryKey(key),\n\t\t);\n\t\tif (!result) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tconst secondsLeft = getExpirationInSeconds(result);\n\n\t\treturn Math.max(secondsLeft, 0);\n\t}\n\n\tasync get<T>(key: string): Promise<T | undefined> {\n\t\tconst result = await SecureStore.getItemAsync(key);\n\t\tif (!result) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst expiresAt = await this.ttl(key);\n\t\tif (expiresAt === 0) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn JSON.parse(result) as T;\n\t}\n\n\tasync set<T>(key: string, value: T, ttl: number = 600): Promise<void> {\n\t\tconst expiresAt = addSeconds(new Date(), ttl).toISOString();\n\t\tawait SecureStore.setItemAsync(key, JSON.stringify(value));\n\t\tawait SecureStore.setItemAsync(\n\t\t\tExpoSecureCache.getExpiryKey(key),\n\t\t\texpiresAt,\n\t\t);\n\t}\n\n\tasync delete(key: string): Promise<void> {\n\t\tawait SecureStore.deleteItemAsync(key);\n\t\tawait SecureStore.deleteItemAsync(ExpoSecureCache.getExpiryKey(key));\n\t}\n}\n"],"mappings":";;;;;;AAIA,IAAa,kBAAb,MAAa,gBAAiC;CAC7C,OAAO,aAAaA,KAAqB;AACxC,UAAQ,EAAE,IAAI;CACd;CAED,MAAM,IAAIA,KAA8B;EACvC,MAAM,SAAS,MAAM,YAAY,aAChC,gBAAgB,aAAa,IAAI,CACjC;AACD,OAAK,OACJ,QAAO;EAGR,MAAM,cAAc,uBAAuB,OAAO;AAElD,SAAO,KAAK,IAAI,aAAa,EAAE;CAC/B;CAED,MAAM,IAAOA,KAAqC;EACjD,MAAM,SAAS,MAAM,YAAY,aAAa,IAAI;AAClD,OAAK,OACJ;EAGD,MAAM,YAAY,MAAM,KAAK,IAAI,IAAI;AACrC,MAAI,cAAc,EACjB;AAGD,SAAO,KAAK,MAAM,OAAO;CACzB;CAED,MAAM,IAAOA,KAAaC,OAAUC,MAAc,KAAoB;EACrE,MAAM,YAAY,2BAAW,IAAI,QAAQ,IAAI,CAAC,aAAa;AAC3D,QAAM,YAAY,aAAa,KAAK,KAAK,UAAU,MAAM,CAAC;AAC1D,QAAM,YAAY,aACjB,gBAAgB,aAAa,IAAI,EACjC,UACA;CACD;CAED,MAAM,OAAOF,KAA4B;AACxC,QAAM,YAAY,gBAAgB,IAAI;AACtC,QAAM,YAAY,gBAAgB,gBAAgB,aAAa,IAAI,CAAC;CACpE;AACD"}
|
package/dist/file.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const require_chunk = require('./chunk-CUT6urMc.cjs');
|
|
2
|
-
|
|
2
|
+
require('./registry-CITg73zW.cjs');
|
|
3
|
+
const require_src = require('./src-ysNIqjnr.cjs');
|
|
3
4
|
const date_fns = require_chunk.__toESM(require("date-fns"));
|
|
4
5
|
const node_fs = require_chunk.__toESM(require("node:fs"));
|
|
5
6
|
const node_fs_promises = require_chunk.__toESM(require("node:fs/promises"));
|
package/dist/file.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file.cjs","names":["options: FileCacheOptions","key: string","value: T","ttl?: number","fn: (data: FileCacheData) => Promise<FileCacheData>","resolve: () => void"],"sources":["../src/file.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { addSeconds } from 'date-fns';\nimport lockfile from 'proper-lockfile';\nimport { type Cache, getExpirationInSeconds } from './';\n\ninterface FileCacheEntry {\n\tvalue: unknown;\n\texpiresAt: string;\n}\n\ntype FileCacheData = Record<string, FileCacheEntry>;\n\nexport interface FileCacheOptions {\n\t/** Path to the cache JSON file. Default: `process.cwd()/.gkm/cache.json` */\n\tpath?: string;\n\t/** Default TTL in seconds. Default: 600 */\n\tttl?: number;\n}\n\nexport class FileCache implements Cache {\n\tprivate readonly path: string;\n\tprivate readonly defaultTtl: number;\n\t/** In-process mutex to serialize concurrent writes within the same process. */\n\tprivate mutex: Promise<void> = Promise.resolve();\n\n\tconstructor(options: FileCacheOptions = {}) {\n\t\tthis.path = options.path ?? join(process.cwd(), '.gkm', 'cache.json');\n\t\tthis.defaultTtl = options.ttl ?? 600;\n\t}\n\n\tasync get<T>(key: string): Promise<T | undefined> {\n\t\tconst data = await this.readData();\n\t\tconst entry = data[key];\n\t\tif (!entry) return undefined;\n\n\t\tconst remaining = getExpirationInSeconds(entry.expiresAt);\n\t\tif (remaining <= 0) {\n\t\t\tawait this.withLock(async (current) => {\n\t\t\t\tdelete current[key];\n\t\t\t\treturn current;\n\t\t\t});\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn entry.value as T;\n\t}\n\n\tasync set<T>(key: string, value: T, ttl?: number): Promise<void> {\n\t\tconst seconds = ttl ?? this.defaultTtl;\n\t\tconst expiresAt = addSeconds(new Date(), seconds).toISOString();\n\n\t\tawait this.withLock(async (data) => {\n\t\t\tdata[key] = { value, expiresAt };\n\t\t\treturn data;\n\t\t});\n\t}\n\n\tasync delete(key: string): Promise<void> {\n\t\tawait this.withLock(async (data) => {\n\t\t\tdelete data[key];\n\t\t\treturn data;\n\t\t});\n\t}\n\n\tasync ttl(key: string): Promise<number> {\n\t\tconst data = await this.readData();\n\t\tconst entry = data[key];\n\t\tif (!entry) return 0;\n\n\t\treturn getExpirationInSeconds(entry.expiresAt);\n\t}\n\n\tprivate async readData(): Promise<FileCacheData> {\n\t\ttry {\n\t\t\tconst content = await readFile(this.path, 'utf-8');\n\t\t\treturn JSON.parse(content) as FileCacheData;\n\t\t} catch {\n\t\t\treturn {};\n\t\t}\n\t}\n\n\tprivate async ensureDir(): Promise<void> {\n\t\tconst dir = dirname(this.path);\n\t\tif (!existsSync(dir)) {\n\t\t\tawait mkdir(dir, { recursive: true });\n\t\t}\n\t}\n\n\tprivate async withLock(\n\t\tfn: (data: FileCacheData) => Promise<FileCacheData>,\n\t): Promise<void> {\n\t\t// Queue behind any pending in-process operations\n\t\tconst prev = this.mutex;\n\t\tlet resolve: () => void;\n\t\tthis.mutex = new Promise<void>((r) => {\n\t\t\tresolve = r;\n\t\t});\n\n\t\ttry {\n\t\t\tawait prev;\n\t\t\tawait this.ensureDir();\n\n\t\t\t// Ensure file exists before locking (proper-lockfile requires it)\n\t\t\tif (!existsSync(this.path)) {\n\t\t\t\tawait writeFile(this.path, '{}');\n\t\t\t}\n\n\t\t\tconst release = await lockfile.lock(this.path, {\n\t\t\t\tretries: { retries: 3, minTimeout: 100, maxTimeout: 1000 },\n\t\t\t\tstale: 10000,\n\t\t\t});\n\n\t\t\ttry {\n\t\t\t\tconst data = await this.readData();\n\t\t\t\tconst updated = await fn(data);\n\t\t\t\tawait writeFile(this.path, JSON.stringify(updated, null, 2));\n\t\t\t} finally {\n\t\t\t\tawait release();\n\t\t\t}\n\t\t} finally {\n\t\t\tresolve!();\n\t\t}\n\t}\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"file.cjs","names":["options: FileCacheOptions","key: string","value: T","ttl?: number","fn: (data: FileCacheData) => Promise<FileCacheData>","resolve: () => void"],"sources":["../src/file.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { addSeconds } from 'date-fns';\nimport lockfile from 'proper-lockfile';\nimport { type Cache, getExpirationInSeconds } from './';\n\ninterface FileCacheEntry {\n\tvalue: unknown;\n\texpiresAt: string;\n}\n\ntype FileCacheData = Record<string, FileCacheEntry>;\n\nexport interface FileCacheOptions {\n\t/** Path to the cache JSON file. Default: `process.cwd()/.gkm/cache.json` */\n\tpath?: string;\n\t/** Default TTL in seconds. Default: 600 */\n\tttl?: number;\n}\n\nexport class FileCache implements Cache {\n\tprivate readonly path: string;\n\tprivate readonly defaultTtl: number;\n\t/** In-process mutex to serialize concurrent writes within the same process. */\n\tprivate mutex: Promise<void> = Promise.resolve();\n\n\tconstructor(options: FileCacheOptions = {}) {\n\t\tthis.path = options.path ?? join(process.cwd(), '.gkm', 'cache.json');\n\t\tthis.defaultTtl = options.ttl ?? 600;\n\t}\n\n\tasync get<T>(key: string): Promise<T | undefined> {\n\t\tconst data = await this.readData();\n\t\tconst entry = data[key];\n\t\tif (!entry) return undefined;\n\n\t\tconst remaining = getExpirationInSeconds(entry.expiresAt);\n\t\tif (remaining <= 0) {\n\t\t\tawait this.withLock(async (current) => {\n\t\t\t\tdelete current[key];\n\t\t\t\treturn current;\n\t\t\t});\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn entry.value as T;\n\t}\n\n\tasync set<T>(key: string, value: T, ttl?: number): Promise<void> {\n\t\tconst seconds = ttl ?? this.defaultTtl;\n\t\tconst expiresAt = addSeconds(new Date(), seconds).toISOString();\n\n\t\tawait this.withLock(async (data) => {\n\t\t\tdata[key] = { value, expiresAt };\n\t\t\treturn data;\n\t\t});\n\t}\n\n\tasync delete(key: string): Promise<void> {\n\t\tawait this.withLock(async (data) => {\n\t\t\tdelete data[key];\n\t\t\treturn data;\n\t\t});\n\t}\n\n\tasync ttl(key: string): Promise<number> {\n\t\tconst data = await this.readData();\n\t\tconst entry = data[key];\n\t\tif (!entry) return 0;\n\n\t\treturn getExpirationInSeconds(entry.expiresAt);\n\t}\n\n\tprivate async readData(): Promise<FileCacheData> {\n\t\ttry {\n\t\t\tconst content = await readFile(this.path, 'utf-8');\n\t\t\treturn JSON.parse(content) as FileCacheData;\n\t\t} catch {\n\t\t\treturn {};\n\t\t}\n\t}\n\n\tprivate async ensureDir(): Promise<void> {\n\t\tconst dir = dirname(this.path);\n\t\tif (!existsSync(dir)) {\n\t\t\tawait mkdir(dir, { recursive: true });\n\t\t}\n\t}\n\n\tprivate async withLock(\n\t\tfn: (data: FileCacheData) => Promise<FileCacheData>,\n\t): Promise<void> {\n\t\t// Queue behind any pending in-process operations\n\t\tconst prev = this.mutex;\n\t\tlet resolve: () => void;\n\t\tthis.mutex = new Promise<void>((r) => {\n\t\t\tresolve = r;\n\t\t});\n\n\t\ttry {\n\t\t\tawait prev;\n\t\t\tawait this.ensureDir();\n\n\t\t\t// Ensure file exists before locking (proper-lockfile requires it)\n\t\t\tif (!existsSync(this.path)) {\n\t\t\t\tawait writeFile(this.path, '{}');\n\t\t\t}\n\n\t\t\tconst release = await lockfile.lock(this.path, {\n\t\t\t\tretries: { retries: 3, minTimeout: 100, maxTimeout: 1000 },\n\t\t\t\tstale: 10000,\n\t\t\t});\n\n\t\t\ttry {\n\t\t\t\tconst data = await this.readData();\n\t\t\t\tconst updated = await fn(data);\n\t\t\t\tawait writeFile(this.path, JSON.stringify(updated, null, 2));\n\t\t\t} finally {\n\t\t\t\tawait release();\n\t\t\t}\n\t\t} finally {\n\t\t\tresolve!();\n\t\t}\n\t}\n}\n"],"mappings":";;;;;;;;;;AAqBA,IAAa,YAAb,MAAwC;CACvC,AAAiB;CACjB,AAAiB;;CAEjB,AAAQ,QAAuB,QAAQ,SAAS;CAEhD,YAAYA,UAA4B,CAAE,GAAE;AAC3C,OAAK,OAAO,QAAQ,QAAQ,oBAAK,QAAQ,KAAK,EAAE,QAAQ,aAAa;AACrE,OAAK,aAAa,QAAQ,OAAO;CACjC;CAED,MAAM,IAAOC,KAAqC;EACjD,MAAM,OAAO,MAAM,KAAK,UAAU;EAClC,MAAM,QAAQ,KAAK;AACnB,OAAK,MAAO;EAEZ,MAAM,YAAY,mCAAuB,MAAM,UAAU;AACzD,MAAI,aAAa,GAAG;AACnB,SAAM,KAAK,SAAS,OAAO,YAAY;AACtC,WAAO,QAAQ;AACf,WAAO;GACP,EAAC;AACF;EACA;AAED,SAAO,MAAM;CACb;CAED,MAAM,IAAOA,KAAaC,OAAUC,KAA6B;EAChE,MAAM,UAAU,OAAO,KAAK;EAC5B,MAAM,YAAY,yCAAW,IAAI,QAAQ,QAAQ,CAAC,aAAa;AAE/D,QAAM,KAAK,SAAS,OAAO,SAAS;AACnC,QAAK,OAAO;IAAE;IAAO;GAAW;AAChC,UAAO;EACP,EAAC;CACF;CAED,MAAM,OAAOF,KAA4B;AACxC,QAAM,KAAK,SAAS,OAAO,SAAS;AACnC,UAAO,KAAK;AACZ,UAAO;EACP,EAAC;CACF;CAED,MAAM,IAAIA,KAA8B;EACvC,MAAM,OAAO,MAAM,KAAK,UAAU;EAClC,MAAM,QAAQ,KAAK;AACnB,OAAK,MAAO,QAAO;AAEnB,SAAO,mCAAuB,MAAM,UAAU;CAC9C;CAED,MAAc,WAAmC;AAChD,MAAI;GACH,MAAM,UAAU,MAAM,+BAAS,KAAK,MAAM,QAAQ;AAClD,UAAO,KAAK,MAAM,QAAQ;EAC1B,QAAO;AACP,UAAO,CAAE;EACT;CACD;CAED,MAAc,YAA2B;EACxC,MAAM,MAAM,uBAAQ,KAAK,KAAK;AAC9B,OAAK,wBAAW,IAAI,CACnB,OAAM,4BAAM,KAAK,EAAE,WAAW,KAAM,EAAC;CAEtC;CAED,MAAc,SACbG,IACgB;EAEhB,MAAM,OAAO,KAAK;EAClB,IAAIC;AACJ,OAAK,QAAQ,IAAI,QAAc,CAAC,MAAM;AACrC,aAAU;EACV;AAED,MAAI;AACH,SAAM;AACN,SAAM,KAAK,WAAW;AAGtB,QAAK,wBAAW,KAAK,KAAK,CACzB,OAAM,gCAAU,KAAK,MAAM,KAAK;GAGjC,MAAM,UAAU,MAAM,wBAAS,KAAK,KAAK,MAAM;IAC9C,SAAS;KAAE,SAAS;KAAG,YAAY;KAAK,YAAY;IAAM;IAC1D,OAAO;GACP,EAAC;AAEF,OAAI;IACH,MAAM,OAAO,MAAM,KAAK,UAAU;IAClC,MAAM,UAAU,MAAM,GAAG,KAAK;AAC9B,UAAM,gCAAU,KAAK,MAAM,KAAK,UAAU,SAAS,MAAM,EAAE,CAAC;GAC5D,UAAS;AACT,UAAM,SAAS;GACf;EACD,UAAS;AACT,YAAU;EACV;CACD;AACD"}
|
package/dist/file.d.cts
CHANGED
package/dist/file.d.mts
CHANGED
package/dist/file.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import "./registry-CT0b-rjB.mjs";
|
|
2
|
+
import { getExpirationInSeconds } from "./src-DVpoP03o.mjs";
|
|
2
3
|
import { addSeconds } from "date-fns";
|
|
3
4
|
import { existsSync } from "node:fs";
|
|
4
5
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
package/dist/file.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file.mjs","names":["options: FileCacheOptions","key: string","value: T","ttl?: number","fn: (data: FileCacheData) => Promise<FileCacheData>","resolve: () => void"],"sources":["../src/file.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { addSeconds } from 'date-fns';\nimport lockfile from 'proper-lockfile';\nimport { type Cache, getExpirationInSeconds } from './';\n\ninterface FileCacheEntry {\n\tvalue: unknown;\n\texpiresAt: string;\n}\n\ntype FileCacheData = Record<string, FileCacheEntry>;\n\nexport interface FileCacheOptions {\n\t/** Path to the cache JSON file. Default: `process.cwd()/.gkm/cache.json` */\n\tpath?: string;\n\t/** Default TTL in seconds. Default: 600 */\n\tttl?: number;\n}\n\nexport class FileCache implements Cache {\n\tprivate readonly path: string;\n\tprivate readonly defaultTtl: number;\n\t/** In-process mutex to serialize concurrent writes within the same process. */\n\tprivate mutex: Promise<void> = Promise.resolve();\n\n\tconstructor(options: FileCacheOptions = {}) {\n\t\tthis.path = options.path ?? join(process.cwd(), '.gkm', 'cache.json');\n\t\tthis.defaultTtl = options.ttl ?? 600;\n\t}\n\n\tasync get<T>(key: string): Promise<T | undefined> {\n\t\tconst data = await this.readData();\n\t\tconst entry = data[key];\n\t\tif (!entry) return undefined;\n\n\t\tconst remaining = getExpirationInSeconds(entry.expiresAt);\n\t\tif (remaining <= 0) {\n\t\t\tawait this.withLock(async (current) => {\n\t\t\t\tdelete current[key];\n\t\t\t\treturn current;\n\t\t\t});\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn entry.value as T;\n\t}\n\n\tasync set<T>(key: string, value: T, ttl?: number): Promise<void> {\n\t\tconst seconds = ttl ?? this.defaultTtl;\n\t\tconst expiresAt = addSeconds(new Date(), seconds).toISOString();\n\n\t\tawait this.withLock(async (data) => {\n\t\t\tdata[key] = { value, expiresAt };\n\t\t\treturn data;\n\t\t});\n\t}\n\n\tasync delete(key: string): Promise<void> {\n\t\tawait this.withLock(async (data) => {\n\t\t\tdelete data[key];\n\t\t\treturn data;\n\t\t});\n\t}\n\n\tasync ttl(key: string): Promise<number> {\n\t\tconst data = await this.readData();\n\t\tconst entry = data[key];\n\t\tif (!entry) return 0;\n\n\t\treturn getExpirationInSeconds(entry.expiresAt);\n\t}\n\n\tprivate async readData(): Promise<FileCacheData> {\n\t\ttry {\n\t\t\tconst content = await readFile(this.path, 'utf-8');\n\t\t\treturn JSON.parse(content) as FileCacheData;\n\t\t} catch {\n\t\t\treturn {};\n\t\t}\n\t}\n\n\tprivate async ensureDir(): Promise<void> {\n\t\tconst dir = dirname(this.path);\n\t\tif (!existsSync(dir)) {\n\t\t\tawait mkdir(dir, { recursive: true });\n\t\t}\n\t}\n\n\tprivate async withLock(\n\t\tfn: (data: FileCacheData) => Promise<FileCacheData>,\n\t): Promise<void> {\n\t\t// Queue behind any pending in-process operations\n\t\tconst prev = this.mutex;\n\t\tlet resolve: () => void;\n\t\tthis.mutex = new Promise<void>((r) => {\n\t\t\tresolve = r;\n\t\t});\n\n\t\ttry {\n\t\t\tawait prev;\n\t\t\tawait this.ensureDir();\n\n\t\t\t// Ensure file exists before locking (proper-lockfile requires it)\n\t\t\tif (!existsSync(this.path)) {\n\t\t\t\tawait writeFile(this.path, '{}');\n\t\t\t}\n\n\t\t\tconst release = await lockfile.lock(this.path, {\n\t\t\t\tretries: { retries: 3, minTimeout: 100, maxTimeout: 1000 },\n\t\t\t\tstale: 10000,\n\t\t\t});\n\n\t\t\ttry {\n\t\t\t\tconst data = await this.readData();\n\t\t\t\tconst updated = await fn(data);\n\t\t\t\tawait writeFile(this.path, JSON.stringify(updated, null, 2));\n\t\t\t} finally {\n\t\t\t\tawait release();\n\t\t\t}\n\t\t} finally {\n\t\t\tresolve!();\n\t\t}\n\t}\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"file.mjs","names":["options: FileCacheOptions","key: string","value: T","ttl?: number","fn: (data: FileCacheData) => Promise<FileCacheData>","resolve: () => void"],"sources":["../src/file.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { addSeconds } from 'date-fns';\nimport lockfile from 'proper-lockfile';\nimport { type Cache, getExpirationInSeconds } from './';\n\ninterface FileCacheEntry {\n\tvalue: unknown;\n\texpiresAt: string;\n}\n\ntype FileCacheData = Record<string, FileCacheEntry>;\n\nexport interface FileCacheOptions {\n\t/** Path to the cache JSON file. Default: `process.cwd()/.gkm/cache.json` */\n\tpath?: string;\n\t/** Default TTL in seconds. Default: 600 */\n\tttl?: number;\n}\n\nexport class FileCache implements Cache {\n\tprivate readonly path: string;\n\tprivate readonly defaultTtl: number;\n\t/** In-process mutex to serialize concurrent writes within the same process. */\n\tprivate mutex: Promise<void> = Promise.resolve();\n\n\tconstructor(options: FileCacheOptions = {}) {\n\t\tthis.path = options.path ?? join(process.cwd(), '.gkm', 'cache.json');\n\t\tthis.defaultTtl = options.ttl ?? 600;\n\t}\n\n\tasync get<T>(key: string): Promise<T | undefined> {\n\t\tconst data = await this.readData();\n\t\tconst entry = data[key];\n\t\tif (!entry) return undefined;\n\n\t\tconst remaining = getExpirationInSeconds(entry.expiresAt);\n\t\tif (remaining <= 0) {\n\t\t\tawait this.withLock(async (current) => {\n\t\t\t\tdelete current[key];\n\t\t\t\treturn current;\n\t\t\t});\n\t\t\treturn undefined;\n\t\t}\n\n\t\treturn entry.value as T;\n\t}\n\n\tasync set<T>(key: string, value: T, ttl?: number): Promise<void> {\n\t\tconst seconds = ttl ?? this.defaultTtl;\n\t\tconst expiresAt = addSeconds(new Date(), seconds).toISOString();\n\n\t\tawait this.withLock(async (data) => {\n\t\t\tdata[key] = { value, expiresAt };\n\t\t\treturn data;\n\t\t});\n\t}\n\n\tasync delete(key: string): Promise<void> {\n\t\tawait this.withLock(async (data) => {\n\t\t\tdelete data[key];\n\t\t\treturn data;\n\t\t});\n\t}\n\n\tasync ttl(key: string): Promise<number> {\n\t\tconst data = await this.readData();\n\t\tconst entry = data[key];\n\t\tif (!entry) return 0;\n\n\t\treturn getExpirationInSeconds(entry.expiresAt);\n\t}\n\n\tprivate async readData(): Promise<FileCacheData> {\n\t\ttry {\n\t\t\tconst content = await readFile(this.path, 'utf-8');\n\t\t\treturn JSON.parse(content) as FileCacheData;\n\t\t} catch {\n\t\t\treturn {};\n\t\t}\n\t}\n\n\tprivate async ensureDir(): Promise<void> {\n\t\tconst dir = dirname(this.path);\n\t\tif (!existsSync(dir)) {\n\t\t\tawait mkdir(dir, { recursive: true });\n\t\t}\n\t}\n\n\tprivate async withLock(\n\t\tfn: (data: FileCacheData) => Promise<FileCacheData>,\n\t): Promise<void> {\n\t\t// Queue behind any pending in-process operations\n\t\tconst prev = this.mutex;\n\t\tlet resolve: () => void;\n\t\tthis.mutex = new Promise<void>((r) => {\n\t\t\tresolve = r;\n\t\t});\n\n\t\ttry {\n\t\t\tawait prev;\n\t\t\tawait this.ensureDir();\n\n\t\t\t// Ensure file exists before locking (proper-lockfile requires it)\n\t\t\tif (!existsSync(this.path)) {\n\t\t\t\tawait writeFile(this.path, '{}');\n\t\t\t}\n\n\t\t\tconst release = await lockfile.lock(this.path, {\n\t\t\t\tretries: { retries: 3, minTimeout: 100, maxTimeout: 1000 },\n\t\t\t\tstale: 10000,\n\t\t\t});\n\n\t\t\ttry {\n\t\t\t\tconst data = await this.readData();\n\t\t\t\tconst updated = await fn(data);\n\t\t\t\tawait writeFile(this.path, JSON.stringify(updated, null, 2));\n\t\t\t} finally {\n\t\t\t\tawait release();\n\t\t\t}\n\t\t} finally {\n\t\t\tresolve!();\n\t\t}\n\t}\n}\n"],"mappings":";;;;;;;;;AAqBA,IAAa,YAAb,MAAwC;CACvC,AAAiB;CACjB,AAAiB;;CAEjB,AAAQ,QAAuB,QAAQ,SAAS;CAEhD,YAAYA,UAA4B,CAAE,GAAE;AAC3C,OAAK,OAAO,QAAQ,QAAQ,KAAK,QAAQ,KAAK,EAAE,QAAQ,aAAa;AACrE,OAAK,aAAa,QAAQ,OAAO;CACjC;CAED,MAAM,IAAOC,KAAqC;EACjD,MAAM,OAAO,MAAM,KAAK,UAAU;EAClC,MAAM,QAAQ,KAAK;AACnB,OAAK,MAAO;EAEZ,MAAM,YAAY,uBAAuB,MAAM,UAAU;AACzD,MAAI,aAAa,GAAG;AACnB,SAAM,KAAK,SAAS,OAAO,YAAY;AACtC,WAAO,QAAQ;AACf,WAAO;GACP,EAAC;AACF;EACA;AAED,SAAO,MAAM;CACb;CAED,MAAM,IAAOA,KAAaC,OAAUC,KAA6B;EAChE,MAAM,UAAU,OAAO,KAAK;EAC5B,MAAM,YAAY,2BAAW,IAAI,QAAQ,QAAQ,CAAC,aAAa;AAE/D,QAAM,KAAK,SAAS,OAAO,SAAS;AACnC,QAAK,OAAO;IAAE;IAAO;GAAW;AAChC,UAAO;EACP,EAAC;CACF;CAED,MAAM,OAAOF,KAA4B;AACxC,QAAM,KAAK,SAAS,OAAO,SAAS;AACnC,UAAO,KAAK;AACZ,UAAO;EACP,EAAC;CACF;CAED,MAAM,IAAIA,KAA8B;EACvC,MAAM,OAAO,MAAM,KAAK,UAAU;EAClC,MAAM,QAAQ,KAAK;AACnB,OAAK,MAAO,QAAO;AAEnB,SAAO,uBAAuB,MAAM,UAAU;CAC9C;CAED,MAAc,WAAmC;AAChD,MAAI;GACH,MAAM,UAAU,MAAM,SAAS,KAAK,MAAM,QAAQ;AAClD,UAAO,KAAK,MAAM,QAAQ;EAC1B,QAAO;AACP,UAAO,CAAE;EACT;CACD;CAED,MAAc,YAA2B;EACxC,MAAM,MAAM,QAAQ,KAAK,KAAK;AAC9B,OAAK,WAAW,IAAI,CACnB,OAAM,MAAM,KAAK,EAAE,WAAW,KAAM,EAAC;CAEtC;CAED,MAAc,SACbG,IACgB;EAEhB,MAAM,OAAO,KAAK;EAClB,IAAIC;AACJ,OAAK,QAAQ,IAAI,QAAc,CAAC,MAAM;AACrC,aAAU;EACV;AAED,MAAI;AACH,SAAM;AACN,SAAM,KAAK,WAAW;AAGtB,QAAK,WAAW,KAAK,KAAK,CACzB,OAAM,UAAU,KAAK,MAAM,KAAK;GAGjC,MAAM,UAAU,MAAM,SAAS,KAAK,KAAK,MAAM;IAC9C,SAAS;KAAE,SAAS;KAAG,YAAY;KAAK,YAAY;IAAM;IAC1D,OAAO;GACP,EAAC;AAEF,OAAI;IACH,MAAM,OAAO,MAAM,KAAK,UAAU;IAClC,MAAM,UAAU,MAAM,GAAG,KAAK;AAC9B,UAAM,UAAU,KAAK,MAAM,KAAK,UAAU,SAAS,MAAM,EAAE,CAAC;GAC5D,UAAS;AACT,UAAM,SAAS;GACf;EACD,UAAS;AACT,YAAU;EACV;CACD;AACD"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
//#region src/registry.d.ts
|
|
2
|
+
|
|
3
|
+
/** What a driver has to supply: a scheme it answers to, and a client for a URL. */
|
|
4
|
+
interface CacheDriver {
|
|
5
|
+
/** Including the colon, as `URL.protocol` reports it — `redis:`, `https:`. */
|
|
6
|
+
scheme: string;
|
|
7
|
+
create(url: string): Cache;
|
|
8
|
+
}
|
|
9
|
+
/** Make a driver available to {@link createCacheClient}. Idempotent. */
|
|
10
|
+
declare function registerCacheDriver(driver: CacheDriver): void;
|
|
11
|
+
/** Which schemes are currently registered — the useful half of a failure. */
|
|
12
|
+
declare function registeredCacheSchemes(): string[];
|
|
13
|
+
/**
|
|
14
|
+
* Build a client for a URL.
|
|
15
|
+
*
|
|
16
|
+
* @throws {UnregisteredCacheScheme} when no driver handles the scheme, which in
|
|
17
|
+
* practice means the entry point registered a different one — the backend and
|
|
18
|
+
* the build disagreeing, caught at the first cache call rather than never.
|
|
19
|
+
*/
|
|
20
|
+
declare function createCacheClient(url: string): Cache;
|
|
21
|
+
/** No driver handles the scheme in a cache URL. */
|
|
22
|
+
declare class UnregisteredCacheScheme extends Error {
|
|
23
|
+
readonly url: string;
|
|
24
|
+
readonly scheme: string;
|
|
25
|
+
readonly registered: readonly string[];
|
|
26
|
+
constructor(url: string, scheme: string, registered: readonly string[]);
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/index.d.ts
|
|
31
|
+
declare const getExpirationInSeconds: (expiresAt: string | number | Date | undefined | null) => number;
|
|
32
|
+
interface Cache {
|
|
33
|
+
/**
|
|
34
|
+
* Retrieves a value from the cache.
|
|
35
|
+
*
|
|
36
|
+
* @param key The key to retrieve the value for.
|
|
37
|
+
* @returns The cached value, or undefined if not found.
|
|
38
|
+
*/
|
|
39
|
+
get<T>(key: string): Promise<T | undefined>;
|
|
40
|
+
/**
|
|
41
|
+
* Sets a value in the cache.
|
|
42
|
+
*
|
|
43
|
+
* @param key The key to set the value for.
|
|
44
|
+
* @param value The value to cache.
|
|
45
|
+
* @param ttl Optional time-to-live in seconds. If not provided, the value will not expire.
|
|
46
|
+
*/
|
|
47
|
+
set<T>(key: string, value: T, ttl?: number): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Deletes a value from the cache.
|
|
50
|
+
*
|
|
51
|
+
* @param key The key to delete from the cache.
|
|
52
|
+
*/
|
|
53
|
+
delete(key: string): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Retrieves the time-to-live (TTL) for a cached value.
|
|
56
|
+
*
|
|
57
|
+
* @param key The key to retrieve the TTL for.
|
|
58
|
+
* @returns The TTL in seconds, or 0 if the key does not exist.
|
|
59
|
+
*/
|
|
60
|
+
ttl(key: string): Promise<number>;
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
export { Cache, CacheDriver, UnregisteredCacheScheme, createCacheClient, getExpirationInSeconds, registerCacheDriver, registeredCacheSchemes };
|
|
64
|
+
//# sourceMappingURL=index-1sNRIo6N.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-1sNRIo6N.d.mts","names":[],"sources":["../src/registry.ts","../src/index.ts"],"sourcesContent":[],"mappings":";;;ACgC4B,UDhBX,WAAA,CCgBW;EAAC;EAAwB,MAM/B,EAAA,MAAA;EAAO,MAQV,CAAA,GAAA,EAAA,MAAA,CAAA,ED3BG,KC2BH;AAAO;;iBDrBV,mBAAA,SAA4B;;iBAK5B,sBAAA,CAAA;;;;;;;;iBAWA,iBAAA,eAAgC;;cAYnC,uBAAA,SAAgC,KAAK;;;;;;;;;cCnDrC,sDACiB;ADab,UCCA,KAAA,CDDW;EASZ;AAKhB;AAWA;AAYA;;;uBC7BsB,QAAQ;EAtBjB;AAeb;;;;;;EAeqD,GAM/B,CAAA,CAAA,CAAA,CAAA,GAAA,EAAA,MAAA,EAAA,KAAA,EANM,CAMN,EAAA,GAAA,CAAA,EAAA,MAAA,CAAA,EANwB,OAMxB,CAAA,IAAA,CAAA;EAAO;AAQH;;;;uBARJ;;;;;;;oBAQH"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
//#region src/registry.d.ts
|
|
2
|
+
|
|
3
|
+
/** What a driver has to supply: a scheme it answers to, and a client for a URL. */
|
|
4
|
+
interface CacheDriver {
|
|
5
|
+
/** Including the colon, as `URL.protocol` reports it — `redis:`, `https:`. */
|
|
6
|
+
scheme: string;
|
|
7
|
+
create(url: string): Cache;
|
|
8
|
+
}
|
|
9
|
+
/** Make a driver available to {@link createCacheClient}. Idempotent. */
|
|
10
|
+
declare function registerCacheDriver(driver: CacheDriver): void;
|
|
11
|
+
/** Which schemes are currently registered — the useful half of a failure. */
|
|
12
|
+
declare function registeredCacheSchemes(): string[];
|
|
13
|
+
/**
|
|
14
|
+
* Build a client for a URL.
|
|
15
|
+
*
|
|
16
|
+
* @throws {UnregisteredCacheScheme} when no driver handles the scheme, which in
|
|
17
|
+
* practice means the entry point registered a different one — the backend and
|
|
18
|
+
* the build disagreeing, caught at the first cache call rather than never.
|
|
19
|
+
*/
|
|
20
|
+
declare function createCacheClient(url: string): Cache;
|
|
21
|
+
/** No driver handles the scheme in a cache URL. */
|
|
22
|
+
declare class UnregisteredCacheScheme extends Error {
|
|
23
|
+
readonly url: string;
|
|
24
|
+
readonly scheme: string;
|
|
25
|
+
readonly registered: readonly string[];
|
|
26
|
+
constructor(url: string, scheme: string, registered: readonly string[]);
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/index.d.ts
|
|
31
|
+
declare const getExpirationInSeconds: (expiresAt: string | number | Date | undefined | null) => number;
|
|
32
|
+
interface Cache {
|
|
33
|
+
/**
|
|
34
|
+
* Retrieves a value from the cache.
|
|
35
|
+
*
|
|
36
|
+
* @param key The key to retrieve the value for.
|
|
37
|
+
* @returns The cached value, or undefined if not found.
|
|
38
|
+
*/
|
|
39
|
+
get<T>(key: string): Promise<T | undefined>;
|
|
40
|
+
/**
|
|
41
|
+
* Sets a value in the cache.
|
|
42
|
+
*
|
|
43
|
+
* @param key The key to set the value for.
|
|
44
|
+
* @param value The value to cache.
|
|
45
|
+
* @param ttl Optional time-to-live in seconds. If not provided, the value will not expire.
|
|
46
|
+
*/
|
|
47
|
+
set<T>(key: string, value: T, ttl?: number): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Deletes a value from the cache.
|
|
50
|
+
*
|
|
51
|
+
* @param key The key to delete from the cache.
|
|
52
|
+
*/
|
|
53
|
+
delete(key: string): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Retrieves the time-to-live (TTL) for a cached value.
|
|
56
|
+
*
|
|
57
|
+
* @param key The key to retrieve the TTL for.
|
|
58
|
+
* @returns The TTL in seconds, or 0 if the key does not exist.
|
|
59
|
+
*/
|
|
60
|
+
ttl(key: string): Promise<number>;
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
export { Cache, CacheDriver, UnregisteredCacheScheme, createCacheClient, getExpirationInSeconds, registerCacheDriver, registeredCacheSchemes };
|
|
64
|
+
//# sourceMappingURL=index-CNQ50bVk.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-CNQ50bVk.d.cts","names":[],"sources":["../src/registry.ts","../src/index.ts"],"sourcesContent":[],"mappings":";;;ACgC4B,UDhBX,WAAA,CCgBW;EAAC;EAAwB,MAM/B,EAAA,MAAA;EAAO,MAQV,CAAA,GAAA,EAAA,MAAA,CAAA,ED3BG,KC2BH;AAAO;;iBDrBV,mBAAA,SAA4B;;iBAK5B,sBAAA,CAAA;;;;;;;;iBAWA,iBAAA,eAAgC;;cAYnC,uBAAA,SAAgC,KAAK;;;;;;;;;cCnDrC,sDACiB;ADab,UCCA,KAAA,CDDW;EASZ;AAKhB;AAWA;AAYA;;;uBC7BsB,QAAQ;EAtBjB;AAeb;;;;;;EAeqD,GAM/B,CAAA,CAAA,CAAA,CAAA,GAAA,EAAA,MAAA,EAAA,KAAA,EANM,CAMN,EAAA,GAAA,CAAA,EAAA,MAAA,CAAA,EANwB,OAMxB,CAAA,IAAA,CAAA;EAAO;AAQH;;;;uBARJ;;;;;;;oBAQH"}
|
package/dist/index.cjs
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
const
|
|
1
|
+
const require_registry = require('./registry-CITg73zW.cjs');
|
|
2
|
+
const require_src = require('./src-ysNIqjnr.cjs');
|
|
2
3
|
|
|
3
|
-
exports.
|
|
4
|
+
exports.UnregisteredCacheScheme = require_registry.UnregisteredCacheScheme;
|
|
5
|
+
exports.createCacheClient = require_registry.createCacheClient;
|
|
6
|
+
exports.getExpirationInSeconds = require_src.getExpirationInSeconds;
|
|
7
|
+
exports.registerCacheDriver = require_registry.registerCacheDriver;
|
|
8
|
+
exports.registeredCacheSchemes = require_registry.registeredCacheSchemes;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Cache, getExpirationInSeconds } from "./index-
|
|
2
|
-
export { Cache, getExpirationInSeconds };
|
|
1
|
+
import { Cache, CacheDriver, UnregisteredCacheScheme, createCacheClient, getExpirationInSeconds, registerCacheDriver, registeredCacheSchemes } from "./index-CNQ50bVk.cjs";
|
|
2
|
+
export { Cache, CacheDriver, UnregisteredCacheScheme, createCacheClient, getExpirationInSeconds, registerCacheDriver, registeredCacheSchemes };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Cache, getExpirationInSeconds } from "./index-
|
|
2
|
-
export { Cache, getExpirationInSeconds };
|
|
1
|
+
import { Cache, CacheDriver, UnregisteredCacheScheme, createCacheClient, getExpirationInSeconds, registerCacheDriver, registeredCacheSchemes } from "./index-1sNRIo6N.mjs";
|
|
2
|
+
export { Cache, CacheDriver, UnregisteredCacheScheme, createCacheClient, getExpirationInSeconds, registerCacheDriver, registeredCacheSchemes };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { UnregisteredCacheScheme, createCacheClient, registerCacheDriver, registeredCacheSchemes } from "./registry-CT0b-rjB.mjs";
|
|
2
|
+
import { getExpirationInSeconds } from "./src-DVpoP03o.mjs";
|
|
2
3
|
|
|
3
|
-
export { getExpirationInSeconds };
|
|
4
|
+
export { UnregisteredCacheScheme, createCacheClient, getExpirationInSeconds, registerCacheDriver, registeredCacheSchemes };
|
package/dist/memory.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const require_chunk = require('./chunk-CUT6urMc.cjs');
|
|
2
|
-
|
|
2
|
+
require('./registry-CITg73zW.cjs');
|
|
3
|
+
const require_src = require('./src-ysNIqjnr.cjs');
|
|
3
4
|
const date_fns = require_chunk.__toESM(require("date-fns"));
|
|
4
5
|
|
|
5
6
|
//#region src/memory.ts
|
package/dist/memory.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory.cjs","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":"
|
|
1
|
+
{"version":3,"file":"memory.cjs","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,mCAAuB,WAAW;CACzC;CAED,MAAM,IAAOA,KAAaC,OAAUC,MAAc,KAAoB;AACrE,OAAK,MAAM,IAAI,KAAK,MAAM;EAC1B,MAAM,sBAAM,IAAI;EAChB,MAAM,YAAY,yBAAW,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"}
|
package/dist/memory.d.cts
CHANGED
package/dist/memory.d.mts
CHANGED
package/dist/memory.mjs
CHANGED
package/dist/memory.mjs.map
CHANGED
|
@@ -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":"
|
|
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"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
const require_chunk = require('./chunk-CUT6urMc.cjs');
|
|
2
|
+
const pg = require_chunk.__toESM(require("pg"));
|
|
3
|
+
|
|
4
|
+
//#region src/postgres.ts
|
|
5
|
+
const DEFAULT_TABLE = "cache";
|
|
6
|
+
var PostgresCache = class {
|
|
7
|
+
table;
|
|
8
|
+
constructor(pool, options = {}) {
|
|
9
|
+
this.pool = pool;
|
|
10
|
+
this.table = options.table ?? DEFAULT_TABLE;
|
|
11
|
+
}
|
|
12
|
+
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;
|
|
16
|
+
}
|
|
17
|
+
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
|
+
]);
|
|
26
|
+
}
|
|
27
|
+
async delete(key) {
|
|
28
|
+
await this.query(`DELETE FROM ${this.identifier()} WHERE key = $1`, [key]);
|
|
29
|
+
}
|
|
30
|
+
async ttl(key) {
|
|
31
|
+
const { rows } = await this.query(`SELECT EXTRACT(EPOCH FROM (expires_at - now()))::int AS seconds
|
|
32
|
+
FROM ${this.identifier()}
|
|
33
|
+
WHERE key = $1 AND (expires_at IS NULL OR expires_at > now())`, [key]);
|
|
34
|
+
const row = rows[0];
|
|
35
|
+
if (!row) return 0;
|
|
36
|
+
return row.seconds ?? 0;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Delete every expired entry.
|
|
40
|
+
*
|
|
41
|
+
* Optional: reads already ignore them. Worth running occasionally so the
|
|
42
|
+
* table does not grow without bound in a workload that writes many keys once.
|
|
43
|
+
*/
|
|
44
|
+
async sweep() {
|
|
45
|
+
const { rowCount } = await this.query(`DELETE FROM ${this.identifier()} WHERE expires_at <= now()`);
|
|
46
|
+
return rowCount ?? 0;
|
|
47
|
+
}
|
|
48
|
+
async query(sql, values = []) {
|
|
49
|
+
const result = await this.pool.query(sql, values);
|
|
50
|
+
return {
|
|
51
|
+
rows: result.rows,
|
|
52
|
+
rowCount: result.rowCount
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The table name as an identifier.
|
|
57
|
+
*
|
|
58
|
+
* Quoted per segment so a schema-qualified name stays two identifiers rather
|
|
59
|
+
* than becoming one with a dot in it. The name comes from configuration
|
|
60
|
+
* rather than from a request, so this is defence rather than sanitisation.
|
|
61
|
+
*/
|
|
62
|
+
identifier() {
|
|
63
|
+
return quote(this.table);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* The `postgres://` driver.
|
|
68
|
+
*
|
|
69
|
+
* One pool per URL, cached for the life of the process: a cache that opened a
|
|
70
|
+
* connection per call would be slower than the thing it exists to speed up, and
|
|
71
|
+
* a Lambda reusing a warm container should reuse the pool with it.
|
|
72
|
+
*
|
|
73
|
+
* `pg` is imported at the top of this module rather than lazily, because the
|
|
74
|
+
* *module* is the lazy boundary — an app caching in Redis never imports
|
|
75
|
+
* `@geekmidas/cache/postgres` and so never resolves the driver behind it. Same
|
|
76
|
+
* arrangement `@geekmidas/storage/aws` uses for the AWS SDK.
|
|
77
|
+
*/
|
|
78
|
+
const postgresCacheDriver = {
|
|
79
|
+
scheme: "postgres:",
|
|
80
|
+
create(url) {
|
|
81
|
+
const { connectionString, table } = split(url);
|
|
82
|
+
const existing = pools.get(connectionString);
|
|
83
|
+
const pool = existing ?? new pg.default.Pool({ connectionString });
|
|
84
|
+
if (!existing) pools.set(connectionString, pool);
|
|
85
|
+
return new PostgresCache(pool, table ? { table } : {});
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* A cache URL as its two facts: where to connect, and what to read.
|
|
90
|
+
*
|
|
91
|
+
* `table` is removed from the string handed to `pg` rather than left on it,
|
|
92
|
+
* because a Postgres connection string carries libpq parameters and an unknown
|
|
93
|
+
* one is not guaranteed to be ignored — and because the pool should be shared
|
|
94
|
+
* by every cache in the database, which only works if the key does not vary
|
|
95
|
+
* per cache.
|
|
96
|
+
*/
|
|
97
|
+
function split(url) {
|
|
98
|
+
try {
|
|
99
|
+
const parsed = new URL(url);
|
|
100
|
+
const table = parsed.searchParams.get("table") ?? void 0;
|
|
101
|
+
if (!table) return { connectionString: url };
|
|
102
|
+
parsed.searchParams.delete("table");
|
|
103
|
+
return {
|
|
104
|
+
connectionString: parsed.toString(),
|
|
105
|
+
table
|
|
106
|
+
};
|
|
107
|
+
} catch {
|
|
108
|
+
return { connectionString: url };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const pools = /* @__PURE__ */ new Map();
|
|
112
|
+
/**
|
|
113
|
+
* The DDL a Postgres-backed cache needs, for whoever applies DDL.
|
|
114
|
+
*
|
|
115
|
+
* Generation only, in the shape `@geekmidas/db/pg/roles` uses — the same
|
|
116
|
+
* statements run in-process from `gkm dev` and from a provisioner inside a VPC,
|
|
117
|
+
* and one implementation is what stops "works locally, fails deployed".
|
|
118
|
+
*
|
|
119
|
+
* Run as the schema's **owner**. The role a handler connects as cannot create a
|
|
120
|
+
* table, which is the point of the split and the reason this is not done lazily.
|
|
121
|
+
*/
|
|
122
|
+
function cacheTableStatements(options = {}) {
|
|
123
|
+
const table = options.table ?? DEFAULT_TABLE;
|
|
124
|
+
const [schema, name] = table.includes(".") ? table.split(".") : [void 0, table];
|
|
125
|
+
const qualified = quote(table);
|
|
126
|
+
return [{
|
|
127
|
+
describe: `cache table ${table}`,
|
|
128
|
+
exists: {
|
|
129
|
+
sql: schema ? "SELECT 1 FROM information_schema.tables WHERE table_schema = $1 AND table_name = $2" : "SELECT 1 FROM information_schema.tables WHERE table_name = $1",
|
|
130
|
+
values: schema ? [schema, name] : [name]
|
|
131
|
+
},
|
|
132
|
+
sql: `CREATE TABLE ${qualified} (
|
|
133
|
+
key text PRIMARY KEY,
|
|
134
|
+
value jsonb NOT NULL,
|
|
135
|
+
expires_at timestamptz
|
|
136
|
+
)`
|
|
137
|
+
}, {
|
|
138
|
+
describe: `cache expiry index on ${table}`,
|
|
139
|
+
sql: `CREATE INDEX IF NOT EXISTS ${indexName(table)}
|
|
140
|
+
ON ${qualified} (expires_at) WHERE expires_at IS NOT NULL`
|
|
141
|
+
}];
|
|
142
|
+
}
|
|
143
|
+
/** A possibly schema-qualified name, quoted per segment. */
|
|
144
|
+
function quote(table) {
|
|
145
|
+
return table.split(".").map((part) => `"${part.replace(/"/g, "\"\"")}"`).join(".");
|
|
146
|
+
}
|
|
147
|
+
function indexName(table) {
|
|
148
|
+
return `"${table.replace(/[^a-zA-Z0-9]/g, "_")}_expires_at_idx"`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
//#endregion
|
|
152
|
+
exports.PostgresCache = PostgresCache;
|
|
153
|
+
exports.cacheTableStatements = cacheTableStatements;
|
|
154
|
+
exports.postgresCacheDriver = postgresCacheDriver;
|
|
155
|
+
//# sourceMappingURL=postgres.cjs.map
|
|
@@ -0,0 +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":";;;;AAqCA,MAAM,gBAAgB;AAEtB,IAAa,gBAAb,MAA4C;CAC3C,AAAiB;CAEjB,YACkBA,MACjBC,UAAgC,CAAE,GACjC;EAFgB;AAGjB,OAAK,QAAQ,QAAQ,SAAS;CAC9B;CAED,MAAM,IAAOC,KAAqC;EACjD,MAAM,EAAE,MAAM,GAAG,MAAM,KAAK,OAC1B,oBAAoB,KAAK,YAAY,CAAC;oEAEvC,CAAC,GAAI,EACL;AAED,SAAO,KAAK,IAAI;CAChB;CAED,MAAM,IAAOA,KAAaC,OAAUC,KAA6B;AAKhE,QAAM,KAAK,OACT,cAAc,KAAK,YAAY,CAAC;6BACP,iBAAoB,SAAS,uCAAuC;;mEAG9F,iBACG,CAAC,KAAK,KAAK,UAAU,MAAM,AAAC,IAC5B;GAAC;GAAK,KAAK,UAAU,MAAM;GAAE,OAAO,IAAI;EAAC,EAC5C;CACD;CAED,MAAM,OAAOF,KAA4B;AACxC,QAAM,KAAK,OAAO,cAAc,KAAK,YAAY,CAAC,kBAAkB,CAAC,GAAI,EAAC;CAC1E;CAED,MAAM,IAAIA,KAA8B;EACvC,MAAM,EAAE,MAAM,GAAG,MAAM,KAAK,OAC1B;WACO,KAAK,YAAY,CAAC;oEAE1B,CAAC,GAAI,EACL;EAED,MAAM,MAAM,KAAK;AACjB,OAAK,IAAK,QAAO;AAKjB,SAAO,IAAI,WAAW;CACtB;;;;;;;CAQD,MAAM,QAAyB;EAC9B,MAAM,EAAE,UAAU,GAAG,MAAM,KAAK,OAC9B,cAAc,KAAK,YAAY,CAAC,4BACjC;AAED,SAAO,YAAY;CACnB;CAED,MAAc,MACbG,KACAC,SAAoB,CAAE,GAC4B;EAClD,MAAM,SAAS,MAAM,KAAK,KAAK,MAAM,KAAK,OAAO;AAEjD,SAAO;GAAE,MAAM,OAAO;GAAa,UAAU,OAAO;EAAU;CAC9D;;;;;;;;CASD,AAAQ,aAAqB;AAC5B,SAAO,MAAM,KAAK,MAAM;CACxB;AACD;;;;;;;;;;;;;AAcD,MAAaC,sBAAmC;CAC/C,QAAQ;CACR,OAAO,KAAK;EAQX,MAAM,EAAE,kBAAkB,OAAO,GAAG,MAAM,IAAI;EAE9C,MAAM,WAAW,MAAM,IAAI,iBAAiB;EAC5C,MAAM,OAAO,YAAY,IAAI,WAAG,KAAK,EAAE,iBAAkB;AACzD,OAAK,SAAU,OAAM,IAAI,kBAAkB,KAAK;AAIhD,SAAO,IAAI,cAAc,MAAM,QAAQ,EAAE,MAAO,IAAG,CAAE;CACrD;AACD;;;;;;;;;;AAWD,SAAS,MAAMC,KAA2D;AACzE,KAAI;EACH,MAAM,SAAS,IAAI,IAAI;EACvB,MAAM,QAAQ,OAAO,aAAa,IAAI,QAAQ;AAE9C,OAAK,MAAO,QAAO,EAAE,kBAAkB,IAAK;AAE5C,SAAO,aAAa,OAAO,QAAQ;AAEnC,SAAO;GAAE,kBAAkB,OAAO,UAAU;GAAE;EAAO;CACrD,QAAO;AAIP,SAAO,EAAE,kBAAkB,IAAK;CAChC;AACD;AAED,MAAM,wBAAQ,IAAI;;;;;;;;;;;AAYlB,SAAgB,qBAAqBP,UAAgC,CAAE,GAInE;CACH,MAAM,QAAQ,QAAQ,SAAS;CAC/B,MAAM,CAAC,QAAQ,KAAK,GAAG,MAAM,SAAS,IAAI,GACtC,MAAM,MAAM,IAAI,GACjB,SAAY,KAAM;CACrB,MAAM,YAAY,MAAM,MAAM;AAE9B,QAAO,CACN;EACC,WAAW,cAAc,MAAM;EAC/B,QAAQ;GACP,KAAK,SACF,wFACA;GACH,QAAQ,SAAS,CAAC,QAAQ,IAAK,IAAG,CAAC,IAAK;EACxC;EACD,MAAM,eAAe,UAAU;;;;;CAK/B,GACD;EAGC,WAAW,wBAAwB,MAAM;EACzC,MAAM,6BAA6B,UAAU,MAAM,CAAC;cACzC,UAAU;CACrB,CACD;AACD;;AAGD,SAAS,MAAMQ,OAAuB;AACrC,QAAO,MACL,MAAM,IAAI,CACV,IAAI,CAAC,UAAU,GAAG,KAAK,QAAQ,MAAM,OAAK,CAAC,GAAG,CAC9C,KAAK,IAAI;AACX;AAED,SAAS,UAAUA,OAAuB;AACzC,SAAQ,GAAG,MAAM,QAAQ,iBAAiB,IAAI,CAAC;AAC/C"}
|