graphile-cache 2.1.0 → 3.0.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/README.md CHANGED
@@ -1,32 +1,29 @@
1
1
  # graphile-cache
2
2
 
3
3
  <p align="center" width="100%">
4
- <img height="250" src="https://raw.githubusercontent.com/constructive-io/constructive/refs/heads/main/assets/outline-logo.svg" />
4
+ <img height="250" src="https://raw.githubusercontent.com/launchql/launchql/refs/heads/main/assets/outline-logo.svg" />
5
5
  </p>
6
6
 
7
7
  <p align="center" width="100%">
8
- <a href="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml">
9
- <img height="20" src="https://github.com/constructive-io/constructive/actions/workflows/run-tests.yaml/badge.svg" />
10
- </a>
11
- <a href="https://github.com/constructive-io/constructive/blob/main/LICENSE">
12
- <img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/>
13
- </a>
14
- <a href="https://www.npmjs.com/package/graphile-cache">
15
- <img height="20" src="https://img.shields.io/github/package-json/v/constructive-io/constructive?filename=graphile%2Fgraphile-cache%2Fpackage.json"/>
8
+ <a href="https://github.com/launchql/launchql/actions/workflows/run-tests.yaml">
9
+ <img height="20" src="https://github.com/launchql/launchql/actions/workflows/run-tests.yaml/badge.svg" />
16
10
  </a>
11
+ <a href="https://github.com/launchql/launchql/blob/main/LICENSE"><img height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"/></a>
12
+ <a href="https://www.npmjs.com/package/graphile-cache"><img height="20" src="https://img.shields.io/github/package-json/v/launchql/launchql?filename=packages%2Fgraphile-cache%2Fpackage.json"/></a>
17
13
  </p>
18
14
 
19
- **`graphile-cache`** is an LRU cache for PostGraphile handlers with automatic cleanup when PostgreSQL pools are disposed.
20
15
 
21
- > This package integrates with `pg-cache` for PostgreSQL pool management.
16
+ PostGraphile instance LRU cache with automatic cleanup when PostgreSQL pools are disposed.
22
17
 
23
- ## 🚀 Installation
18
+ ## Installation
24
19
 
25
20
  ```bash
26
21
  npm install graphile-cache pg-cache
27
22
  ```
28
23
 
29
- ## Features
24
+ Note: This package depends on `pg-cache` for the PostgreSQL pool management.
25
+
26
+ ## Features
30
27
 
31
28
  - LRU cache for PostGraphile instances
32
29
  - Automatic cleanup when associated PostgreSQL pools are disposed
@@ -34,24 +31,26 @@ npm install graphile-cache pg-cache
34
31
  - Service cache re-exported for convenience
35
32
  - TypeScript support
36
33
 
37
- ## 🧠 How It Works
34
+ ## How It Works
38
35
 
39
36
  When you import this package, it automatically registers a cleanup callback with `pg-cache`. When a PostgreSQL pool is disposed, any PostGraphile instances using that pool are automatically removed from the cache.
40
37
 
41
- ## 📦 Usage
38
+ ## Usage
42
39
 
43
- ### Basic caching flow
40
+ ### Basic Usage
44
41
 
45
42
  ```typescript
46
43
  import { graphileCache, GraphileCache } from 'graphile-cache';
47
44
  import { getPgPool } from 'pg-cache';
48
45
  import { postgraphile } from 'postgraphile';
49
46
 
47
+ // Create a PostGraphile instance
50
48
  const pgPool = getPgPool({ database: 'mydb' });
51
49
  const handler = postgraphile(pgPool, 'public', {
52
50
  // PostGraphile options
53
51
  });
54
52
 
53
+ // Cache it
55
54
  const cacheEntry: GraphileCache = {
56
55
  pgPool,
57
56
  pgPoolKey: 'mydb',
@@ -67,7 +66,7 @@ if (cached) {
67
66
  }
68
67
  ```
69
68
 
70
- ### Automatic cleanup
69
+ ### Automatic Cleanup
71
70
 
72
71
  The cleanup happens automatically:
73
72
 
@@ -87,7 +86,7 @@ console.log(graphileCache.has('mydb.public')); // false
87
86
  console.log(graphileCache.has('mydb.private')); // false
88
87
  ```
89
88
 
90
- ### Complete example with Express
89
+ ### Complete Example
91
90
 
92
91
  ```typescript
93
92
  import { graphileCache, GraphileCache } from 'graphile-cache';
@@ -96,13 +95,13 @@ import { postgraphile } from 'postgraphile';
96
95
 
97
96
  function getGraphileInstance(database: string, schema: string): GraphileCache {
98
97
  const key = `${database}.${schema}`;
99
-
98
+
100
99
  // Check cache first
101
100
  const cached = graphileCache.get(key);
102
101
  if (cached) {
103
102
  return cached;
104
103
  }
105
-
104
+
106
105
  // Create new instance
107
106
  const pgPool = getPgPool({ database });
108
107
  const handler = postgraphile(pgPool, schema, {
@@ -110,13 +109,13 @@ function getGraphileInstance(database: string, schema: string): GraphileCache {
110
109
  graphiqlRoute: '/graphiql',
111
110
  // other options...
112
111
  });
113
-
112
+
114
113
  const entry: GraphileCache = {
115
114
  pgPool,
116
115
  pgPoolKey: database,
117
116
  handler
118
117
  };
119
-
118
+
120
119
  // Cache it
121
120
  graphileCache.set(key, entry);
122
121
  return entry;
@@ -134,14 +133,14 @@ app.use((req, res, next) => {
134
133
  ```typescript
135
134
  import { closeAllCaches } from 'graphile-cache';
136
135
 
136
+ // This closes all caches including pg pools
137
137
  process.on('SIGTERM', async () => {
138
- // Closes all caches including pg pools
139
138
  await closeAllCaches();
140
139
  process.exit(0);
141
140
  });
142
141
  ```
143
142
 
144
- ## 📘 API Reference
143
+ ## API Reference
145
144
 
146
145
  ### graphileCache
147
146
 
@@ -171,7 +170,7 @@ Closes all caches including the service cache, graphile cache, and all PostgreSQ
171
170
 
172
171
  Re-exported from `pg-cache` for convenience.
173
172
 
174
- ## 🔌 Integration Details
173
+ ## Integration Details
175
174
 
176
175
  The integration with `pg-cache` happens automatically when this module is imported. The cleanup callback is registered immediately, ensuring that PostGraphile instances are cleaned up whenever their associated PostgreSQL pools are disposed.
177
176
 
@@ -0,0 +1,17 @@
1
+ import type { GraphileCacheEntry } from './graphile-cache';
2
+ interface GraphileInstanceOptions {
3
+ preset: any;
4
+ cacheKey: string;
5
+ }
6
+ /**
7
+ * Create a PostGraphile v5 instance backed by grafserv/express.
8
+ *
9
+ * This is the shared factory used by both graphql/server and graphql/explorer
10
+ * to spin up a fully-initialised PostGraphile handler that fits into the
11
+ * graphile-cache LRU cache.
12
+ *
13
+ * Callers are responsible for building the `GraphileConfig.Preset` (including
14
+ * pgServices, grafserv options, grafast context, etc.) before passing it here.
15
+ */
16
+ export declare const createGraphileInstance: (opts: GraphileInstanceOptions) => Promise<GraphileCacheEntry>;
17
+ export {};
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createGraphileInstance = void 0;
7
+ const node_http_1 = require("node:http");
8
+ const express_1 = __importDefault(require("express"));
9
+ const postgraphile_1 = require("postgraphile");
10
+ const v4_1 = require("grafserv/express/v4");
11
+ /**
12
+ * Create a PostGraphile v5 instance backed by grafserv/express.
13
+ *
14
+ * This is the shared factory used by both graphql/server and graphql/explorer
15
+ * to spin up a fully-initialised PostGraphile handler that fits into the
16
+ * graphile-cache LRU cache.
17
+ *
18
+ * Callers are responsible for building the `GraphileConfig.Preset` (including
19
+ * pgServices, grafserv options, grafast context, etc.) before passing it here.
20
+ */
21
+ const createGraphileInstance = async (opts) => {
22
+ const { preset, cacheKey } = opts;
23
+ const pgl = (0, postgraphile_1.postgraphile)(preset);
24
+ const serv = pgl.createServ(v4_1.grafserv);
25
+ const handler = (0, express_1.default)();
26
+ const httpServer = (0, node_http_1.createServer)(handler);
27
+ await serv.addTo(handler, httpServer);
28
+ await serv.ready();
29
+ return {
30
+ pgl,
31
+ serv,
32
+ handler,
33
+ httpServer,
34
+ cacheKey,
35
+ createdAt: Date.now(),
36
+ };
37
+ };
38
+ exports.createGraphileInstance = createGraphileInstance;
@@ -0,0 +1,31 @@
1
+ import { createServer } from 'node:http';
2
+ import express from 'express';
3
+ import { postgraphile } from 'postgraphile';
4
+ import { grafserv } from 'grafserv/express/v4';
5
+ /**
6
+ * Create a PostGraphile v5 instance backed by grafserv/express.
7
+ *
8
+ * This is the shared factory used by both graphql/server and graphql/explorer
9
+ * to spin up a fully-initialised PostGraphile handler that fits into the
10
+ * graphile-cache LRU cache.
11
+ *
12
+ * Callers are responsible for building the `GraphileConfig.Preset` (including
13
+ * pgServices, grafserv options, grafast context, etc.) before passing it here.
14
+ */
15
+ export const createGraphileInstance = async (opts) => {
16
+ const { preset, cacheKey } = opts;
17
+ const pgl = postgraphile(preset);
18
+ const serv = pgl.createServ(grafserv);
19
+ const handler = express();
20
+ const httpServer = createServer(handler);
21
+ await serv.addTo(handler, httpServer);
22
+ await serv.ready();
23
+ return {
24
+ pgl,
25
+ serv,
26
+ handler,
27
+ httpServer,
28
+ cacheKey,
29
+ createdAt: Date.now(),
30
+ };
31
+ };
@@ -1,38 +1,194 @@
1
+ import { EventEmitter } from 'events';
1
2
  import { Logger } from '@pgpmjs/logger';
2
3
  import { LRUCache } from 'lru-cache';
3
4
  import { pgCache } from 'pg-cache';
4
5
  const log = new Logger('graphile-cache');
5
- const ONE_HOUR_IN_MS = 1000 * 60 * 60;
6
- const ONE_DAY = ONE_HOUR_IN_MS * 24;
6
+ // --- Time Constants ---
7
+ export const ONE_HOUR_MS = 1000 * 60 * 60;
8
+ export const FIVE_MINUTES_MS = 1000 * 60 * 5;
9
+ const ONE_DAY = ONE_HOUR_MS * 24;
7
10
  const ONE_YEAR = ONE_DAY * 366;
11
+ export class CacheEventEmitter extends EventEmitter {
12
+ emitEviction(event) {
13
+ this.emit('eviction', event);
14
+ }
15
+ onEviction(handler) {
16
+ this.on('eviction', handler);
17
+ }
18
+ }
19
+ export const cacheEvents = new CacheEventEmitter();
20
+ /**
21
+ * Get cache configuration from environment variables
22
+ *
23
+ * Supports:
24
+ * - GRAPHILE_CACHE_MAX: Maximum number of entries (default: 15)
25
+ * - GRAPHILE_CACHE_TTL_MS: TTL in milliseconds
26
+ * - Production default: ONE_YEAR
27
+ * - Development default: FIVE_MINUTES_MS
28
+ */
29
+ export function getCacheConfig() {
30
+ const isDevelopment = process.env.NODE_ENV === 'development';
31
+ const max = process.env.GRAPHILE_CACHE_MAX
32
+ ? parseInt(process.env.GRAPHILE_CACHE_MAX, 10)
33
+ : 15;
34
+ const ttl = process.env.GRAPHILE_CACHE_TTL_MS
35
+ ? parseInt(process.env.GRAPHILE_CACHE_TTL_MS, 10)
36
+ : isDevelopment
37
+ ? FIVE_MINUTES_MS
38
+ : ONE_YEAR;
39
+ return { max, ttl };
40
+ }
41
+ // Track disposed entries to prevent double-disposal
42
+ const disposedKeys = new Set();
43
+ // Track keys that are being manually evicted for accurate eviction reason
44
+ const manualEvictionKeys = new Set();
45
+ /**
46
+ * Dispose a PostGraphile v5 cache entry
47
+ *
48
+ * Properly releases resources by:
49
+ * 1. Closing the HTTP server if listening
50
+ * 2. Releasing the PostGraphile instance (which internally releases grafserv)
51
+ *
52
+ * Uses disposedKeys set to prevent double-disposal when closeAllCaches()
53
+ * explicitly disposes entries and then clear() triggers the dispose callback.
54
+ */
55
+ const disposeEntry = async (entry, key) => {
56
+ // Prevent double-disposal
57
+ if (disposedKeys.has(key)) {
58
+ return;
59
+ }
60
+ disposedKeys.add(key);
61
+ log.debug(`Disposing PostGraphile[${key}]`);
62
+ try {
63
+ // Close HTTP server if it's listening
64
+ if (entry.httpServer?.listening) {
65
+ await new Promise((resolve) => {
66
+ entry.httpServer.close(() => resolve());
67
+ });
68
+ }
69
+ // Release PostGraphile instance (this also releases grafserv internally)
70
+ if (entry.pgl) {
71
+ await entry.pgl.release();
72
+ }
73
+ }
74
+ catch (err) {
75
+ log.error(`Error disposing PostGraphile[${key}]:`, err);
76
+ }
77
+ };
78
+ /**
79
+ * Determine the eviction reason for a cache entry
80
+ */
81
+ const getEvictionReason = (key, entry) => {
82
+ if (manualEvictionKeys.has(key)) {
83
+ manualEvictionKeys.delete(key);
84
+ return 'manual';
85
+ }
86
+ // Check if TTL expired
87
+ const age = Date.now() - entry.createdAt;
88
+ const config = getCacheConfig();
89
+ if (age >= config.ttl) {
90
+ return 'ttl';
91
+ }
92
+ return 'lru';
93
+ };
94
+ // Get initial cache configuration
95
+ const initialConfig = getCacheConfig();
8
96
  // --- Graphile Cache ---
9
97
  export const graphileCache = new LRUCache({
10
- max: 15,
11
- ttl: ONE_YEAR,
98
+ max: initialConfig.max,
99
+ ttl: initialConfig.ttl,
12
100
  updateAgeOnGet: true,
13
- dispose: (_, key) => {
14
- log.debug(`Disposing PostGraphile[${key}]`);
101
+ dispose: (entry, key) => {
102
+ // Determine eviction reason before disposal
103
+ const reason = getEvictionReason(key, entry);
104
+ // Emit eviction event
105
+ cacheEvents.emitEviction({ key, reason, entry });
106
+ log.debug(`Evicting PostGraphile[${key}] (reason: ${reason})`);
107
+ // LRU dispose is synchronous, but v5 disposal is async
108
+ // Fire and forget the async cleanup
109
+ disposeEntry(entry, key).catch((err) => {
110
+ log.error(`Failed to dispose PostGraphile[${key}]:`, err);
111
+ });
15
112
  }
16
113
  });
114
+ /**
115
+ * Get current cache statistics
116
+ */
117
+ export function getCacheStats() {
118
+ const config = getCacheConfig();
119
+ return {
120
+ size: graphileCache.size,
121
+ max: config.max,
122
+ ttl: config.ttl,
123
+ keys: [...graphileCache.keys()]
124
+ };
125
+ }
126
+ // --- Clear Matching Entries ---
127
+ /**
128
+ * Clear cache entries matching a regex pattern
129
+ *
130
+ * @param pattern - RegExp to match against cache keys
131
+ * @returns Number of entries cleared
132
+ */
133
+ export function clearMatchingEntries(pattern) {
134
+ let cleared = 0;
135
+ for (const key of graphileCache.keys()) {
136
+ if (pattern.test(key)) {
137
+ // Mark as manual eviction before deleting
138
+ manualEvictionKeys.add(key);
139
+ graphileCache.delete(key);
140
+ cleared++;
141
+ }
142
+ }
143
+ return cleared;
144
+ }
17
145
  // Register cleanup callback with pgCache
18
146
  // When a pg pool is disposed, clean up any graphile instances using it
19
147
  const unregister = pgCache.registerCleanupCallback((pgPoolKey) => {
148
+ log.debug(`pgPool[${pgPoolKey}] disposed - checking graphile entries`);
149
+ // Remove graphile entries that reference this pool key
20
150
  graphileCache.forEach((entry, k) => {
21
- if (entry.pgPoolKey === pgPoolKey) {
22
- log.debug(`Removing graphileCache[${k}] due to pgPool[${pgPoolKey}]`);
151
+ if (entry.cacheKey.includes(pgPoolKey)) {
152
+ log.debug(`Removing graphileCache[${k}] due to pgPool[${pgPoolKey}] disposal`);
153
+ manualEvictionKeys.add(k);
23
154
  graphileCache.delete(k);
24
155
  }
25
156
  });
26
157
  });
27
158
  // Enhanced close function that handles all caches
28
159
  const closePromise = { promise: null };
160
+ /**
161
+ * Close all caches and release resources
162
+ *
163
+ * This function:
164
+ * 1. Disposes all PostGraphile v5 instances (async)
165
+ * 2. Clears the graphile cache
166
+ * 3. Closes all pg pools via pgCache
167
+ *
168
+ * The function is idempotent - calling it multiple times
169
+ * returns the same promise.
170
+ */
29
171
  export const closeAllCaches = async (verbose = false) => {
30
172
  if (closePromise.promise)
31
173
  return closePromise.promise;
32
174
  closePromise.promise = (async () => {
33
175
  if (verbose)
34
176
  log.info('Closing all server caches...');
177
+ // Collect all entries and dispose them properly
178
+ const entries = [...graphileCache.entries()];
179
+ // Mark all as manual evictions
180
+ for (const [key] of entries) {
181
+ manualEvictionKeys.add(key);
182
+ }
183
+ const disposePromises = entries.map(([key, entry]) => disposeEntry(entry, key));
184
+ // Wait for all disposals to complete
185
+ await Promise.allSettled(disposePromises);
186
+ // Clear the cache after disposal (dispose callback will no-op due to disposedKeys)
35
187
  graphileCache.clear();
188
+ // Clear disposed keys tracking after full cleanup
189
+ disposedKeys.clear();
190
+ manualEvictionKeys.clear();
191
+ // Close pg pools
36
192
  await pgCache.close();
37
193
  if (verbose)
38
194
  log.success('All caches disposed.');
package/esm/index.js CHANGED
@@ -1,2 +1,12 @@
1
1
  // Main exports from graphile-cache package
2
- export { closeAllCaches, graphileCache } from './graphile-cache';
2
+ export {
3
+ // Cache instance and entry type
4
+ graphileCache, closeAllCaches,
5
+ // Time constants
6
+ ONE_HOUR_MS, FIVE_MINUTES_MS,
7
+ // Event emitter for cache events
8
+ CacheEventEmitter, cacheEvents, getCacheConfig, getCacheStats,
9
+ // Clear matching entries
10
+ clearMatchingEntries } from './graphile-cache';
11
+ // Factory for creating PostGraphile v5 instances
12
+ export { createGraphileInstance } from './create-instance';
@@ -1,10 +1,82 @@
1
+ import { EventEmitter } from 'events';
1
2
  import { LRUCache } from 'lru-cache';
2
- import pg from 'pg';
3
- import { HttpRequestHandler } from 'postgraphile';
4
- export interface GraphileCache {
5
- pgPool: pg.Pool;
6
- pgPoolKey: string;
7
- handler: HttpRequestHandler;
8
- }
9
- export declare const graphileCache: LRUCache<string, GraphileCache, unknown>;
3
+ import type { Express } from 'express';
4
+ import type { Server as HttpServer } from 'http';
5
+ import type { PostGraphileInstance } from 'postgraphile';
6
+ import type { GrafservBase } from 'grafserv';
7
+ export declare const ONE_HOUR_MS: number;
8
+ export declare const FIVE_MINUTES_MS: number;
9
+ export type EvictionReason = 'lru' | 'ttl' | 'manual';
10
+ export interface CacheEvictionEvent {
11
+ key: string;
12
+ reason: EvictionReason;
13
+ entry: GraphileCacheEntry;
14
+ }
15
+ export declare class CacheEventEmitter extends EventEmitter {
16
+ emitEviction(event: CacheEvictionEvent): void;
17
+ onEviction(handler: (event: CacheEvictionEvent) => void): void;
18
+ }
19
+ export declare const cacheEvents: CacheEventEmitter;
20
+ export interface CacheConfig {
21
+ max: number;
22
+ ttl: number;
23
+ }
24
+ /**
25
+ * Get cache configuration from environment variables
26
+ *
27
+ * Supports:
28
+ * - GRAPHILE_CACHE_MAX: Maximum number of entries (default: 15)
29
+ * - GRAPHILE_CACHE_TTL_MS: TTL in milliseconds
30
+ * - Production default: ONE_YEAR
31
+ * - Development default: FIVE_MINUTES_MS
32
+ */
33
+ export declare function getCacheConfig(): CacheConfig;
34
+ /**
35
+ * Cache entry for PostGraphile v5 instances
36
+ *
37
+ * Each entry contains:
38
+ * - pgl: The PostGraphile instance (manages schema, plugins, etc.)
39
+ * - serv: The Grafserv server instance (handles HTTP/WS)
40
+ * - handler: Express app for routing requests
41
+ * - httpServer: Node HTTP server (required by grafserv)
42
+ * - cacheKey: Unique identifier for this entry
43
+ * - createdAt: Timestamp when this entry was created
44
+ */
45
+ export interface GraphileCacheEntry {
46
+ pgl: PostGraphileInstance;
47
+ serv: GrafservBase;
48
+ handler: Express;
49
+ httpServer: HttpServer;
50
+ cacheKey: string;
51
+ createdAt: number;
52
+ }
53
+ export declare const graphileCache: LRUCache<string, GraphileCacheEntry, unknown>;
54
+ export interface CacheStats {
55
+ size: number;
56
+ max: number;
57
+ ttl: number;
58
+ keys: string[];
59
+ }
60
+ /**
61
+ * Get current cache statistics
62
+ */
63
+ export declare function getCacheStats(): CacheStats;
64
+ /**
65
+ * Clear cache entries matching a regex pattern
66
+ *
67
+ * @param pattern - RegExp to match against cache keys
68
+ * @returns Number of entries cleared
69
+ */
70
+ export declare function clearMatchingEntries(pattern: RegExp): number;
71
+ /**
72
+ * Close all caches and release resources
73
+ *
74
+ * This function:
75
+ * 1. Disposes all PostGraphile v5 instances (async)
76
+ * 2. Clears the graphile cache
77
+ * 3. Closes all pg pools via pgCache
78
+ *
79
+ * The function is idempotent - calling it multiple times
80
+ * returns the same promise.
81
+ */
10
82
  export declare const closeAllCaches: (verbose?: boolean) => Promise<void>;
package/graphile-cache.js CHANGED
@@ -1,41 +1,201 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.closeAllCaches = exports.graphileCache = void 0;
3
+ exports.closeAllCaches = exports.graphileCache = exports.cacheEvents = exports.CacheEventEmitter = exports.FIVE_MINUTES_MS = exports.ONE_HOUR_MS = void 0;
4
+ exports.getCacheConfig = getCacheConfig;
5
+ exports.getCacheStats = getCacheStats;
6
+ exports.clearMatchingEntries = clearMatchingEntries;
7
+ const events_1 = require("events");
4
8
  const logger_1 = require("@pgpmjs/logger");
5
9
  const lru_cache_1 = require("lru-cache");
6
10
  const pg_cache_1 = require("pg-cache");
7
11
  const log = new logger_1.Logger('graphile-cache');
8
- const ONE_HOUR_IN_MS = 1000 * 60 * 60;
9
- const ONE_DAY = ONE_HOUR_IN_MS * 24;
12
+ // --- Time Constants ---
13
+ exports.ONE_HOUR_MS = 1000 * 60 * 60;
14
+ exports.FIVE_MINUTES_MS = 1000 * 60 * 5;
15
+ const ONE_DAY = exports.ONE_HOUR_MS * 24;
10
16
  const ONE_YEAR = ONE_DAY * 366;
17
+ class CacheEventEmitter extends events_1.EventEmitter {
18
+ emitEviction(event) {
19
+ this.emit('eviction', event);
20
+ }
21
+ onEviction(handler) {
22
+ this.on('eviction', handler);
23
+ }
24
+ }
25
+ exports.CacheEventEmitter = CacheEventEmitter;
26
+ exports.cacheEvents = new CacheEventEmitter();
27
+ /**
28
+ * Get cache configuration from environment variables
29
+ *
30
+ * Supports:
31
+ * - GRAPHILE_CACHE_MAX: Maximum number of entries (default: 15)
32
+ * - GRAPHILE_CACHE_TTL_MS: TTL in milliseconds
33
+ * - Production default: ONE_YEAR
34
+ * - Development default: FIVE_MINUTES_MS
35
+ */
36
+ function getCacheConfig() {
37
+ const isDevelopment = process.env.NODE_ENV === 'development';
38
+ const max = process.env.GRAPHILE_CACHE_MAX
39
+ ? parseInt(process.env.GRAPHILE_CACHE_MAX, 10)
40
+ : 15;
41
+ const ttl = process.env.GRAPHILE_CACHE_TTL_MS
42
+ ? parseInt(process.env.GRAPHILE_CACHE_TTL_MS, 10)
43
+ : isDevelopment
44
+ ? exports.FIVE_MINUTES_MS
45
+ : ONE_YEAR;
46
+ return { max, ttl };
47
+ }
48
+ // Track disposed entries to prevent double-disposal
49
+ const disposedKeys = new Set();
50
+ // Track keys that are being manually evicted for accurate eviction reason
51
+ const manualEvictionKeys = new Set();
52
+ /**
53
+ * Dispose a PostGraphile v5 cache entry
54
+ *
55
+ * Properly releases resources by:
56
+ * 1. Closing the HTTP server if listening
57
+ * 2. Releasing the PostGraphile instance (which internally releases grafserv)
58
+ *
59
+ * Uses disposedKeys set to prevent double-disposal when closeAllCaches()
60
+ * explicitly disposes entries and then clear() triggers the dispose callback.
61
+ */
62
+ const disposeEntry = async (entry, key) => {
63
+ // Prevent double-disposal
64
+ if (disposedKeys.has(key)) {
65
+ return;
66
+ }
67
+ disposedKeys.add(key);
68
+ log.debug(`Disposing PostGraphile[${key}]`);
69
+ try {
70
+ // Close HTTP server if it's listening
71
+ if (entry.httpServer?.listening) {
72
+ await new Promise((resolve) => {
73
+ entry.httpServer.close(() => resolve());
74
+ });
75
+ }
76
+ // Release PostGraphile instance (this also releases grafserv internally)
77
+ if (entry.pgl) {
78
+ await entry.pgl.release();
79
+ }
80
+ }
81
+ catch (err) {
82
+ log.error(`Error disposing PostGraphile[${key}]:`, err);
83
+ }
84
+ };
85
+ /**
86
+ * Determine the eviction reason for a cache entry
87
+ */
88
+ const getEvictionReason = (key, entry) => {
89
+ if (manualEvictionKeys.has(key)) {
90
+ manualEvictionKeys.delete(key);
91
+ return 'manual';
92
+ }
93
+ // Check if TTL expired
94
+ const age = Date.now() - entry.createdAt;
95
+ const config = getCacheConfig();
96
+ if (age >= config.ttl) {
97
+ return 'ttl';
98
+ }
99
+ return 'lru';
100
+ };
101
+ // Get initial cache configuration
102
+ const initialConfig = getCacheConfig();
11
103
  // --- Graphile Cache ---
12
104
  exports.graphileCache = new lru_cache_1.LRUCache({
13
- max: 15,
14
- ttl: ONE_YEAR,
105
+ max: initialConfig.max,
106
+ ttl: initialConfig.ttl,
15
107
  updateAgeOnGet: true,
16
- dispose: (_, key) => {
17
- log.debug(`Disposing PostGraphile[${key}]`);
108
+ dispose: (entry, key) => {
109
+ // Determine eviction reason before disposal
110
+ const reason = getEvictionReason(key, entry);
111
+ // Emit eviction event
112
+ exports.cacheEvents.emitEviction({ key, reason, entry });
113
+ log.debug(`Evicting PostGraphile[${key}] (reason: ${reason})`);
114
+ // LRU dispose is synchronous, but v5 disposal is async
115
+ // Fire and forget the async cleanup
116
+ disposeEntry(entry, key).catch((err) => {
117
+ log.error(`Failed to dispose PostGraphile[${key}]:`, err);
118
+ });
18
119
  }
19
120
  });
121
+ /**
122
+ * Get current cache statistics
123
+ */
124
+ function getCacheStats() {
125
+ const config = getCacheConfig();
126
+ return {
127
+ size: exports.graphileCache.size,
128
+ max: config.max,
129
+ ttl: config.ttl,
130
+ keys: [...exports.graphileCache.keys()]
131
+ };
132
+ }
133
+ // --- Clear Matching Entries ---
134
+ /**
135
+ * Clear cache entries matching a regex pattern
136
+ *
137
+ * @param pattern - RegExp to match against cache keys
138
+ * @returns Number of entries cleared
139
+ */
140
+ function clearMatchingEntries(pattern) {
141
+ let cleared = 0;
142
+ for (const key of exports.graphileCache.keys()) {
143
+ if (pattern.test(key)) {
144
+ // Mark as manual eviction before deleting
145
+ manualEvictionKeys.add(key);
146
+ exports.graphileCache.delete(key);
147
+ cleared++;
148
+ }
149
+ }
150
+ return cleared;
151
+ }
20
152
  // Register cleanup callback with pgCache
21
153
  // When a pg pool is disposed, clean up any graphile instances using it
22
154
  const unregister = pg_cache_1.pgCache.registerCleanupCallback((pgPoolKey) => {
155
+ log.debug(`pgPool[${pgPoolKey}] disposed - checking graphile entries`);
156
+ // Remove graphile entries that reference this pool key
23
157
  exports.graphileCache.forEach((entry, k) => {
24
- if (entry.pgPoolKey === pgPoolKey) {
25
- log.debug(`Removing graphileCache[${k}] due to pgPool[${pgPoolKey}]`);
158
+ if (entry.cacheKey.includes(pgPoolKey)) {
159
+ log.debug(`Removing graphileCache[${k}] due to pgPool[${pgPoolKey}] disposal`);
160
+ manualEvictionKeys.add(k);
26
161
  exports.graphileCache.delete(k);
27
162
  }
28
163
  });
29
164
  });
30
165
  // Enhanced close function that handles all caches
31
166
  const closePromise = { promise: null };
167
+ /**
168
+ * Close all caches and release resources
169
+ *
170
+ * This function:
171
+ * 1. Disposes all PostGraphile v5 instances (async)
172
+ * 2. Clears the graphile cache
173
+ * 3. Closes all pg pools via pgCache
174
+ *
175
+ * The function is idempotent - calling it multiple times
176
+ * returns the same promise.
177
+ */
32
178
  const closeAllCaches = async (verbose = false) => {
33
179
  if (closePromise.promise)
34
180
  return closePromise.promise;
35
181
  closePromise.promise = (async () => {
36
182
  if (verbose)
37
183
  log.info('Closing all server caches...');
184
+ // Collect all entries and dispose them properly
185
+ const entries = [...exports.graphileCache.entries()];
186
+ // Mark all as manual evictions
187
+ for (const [key] of entries) {
188
+ manualEvictionKeys.add(key);
189
+ }
190
+ const disposePromises = entries.map(([key, entry]) => disposeEntry(entry, key));
191
+ // Wait for all disposals to complete
192
+ await Promise.allSettled(disposePromises);
193
+ // Clear the cache after disposal (dispose callback will no-op due to disposedKeys)
38
194
  exports.graphileCache.clear();
195
+ // Clear disposed keys tracking after full cleanup
196
+ disposedKeys.clear();
197
+ manualEvictionKeys.clear();
198
+ // Close pg pools
39
199
  await pg_cache_1.pgCache.close();
40
200
  if (verbose)
41
201
  log.success('All caches disposed.');
package/index.d.ts CHANGED
@@ -1 +1,2 @@
1
- export { closeAllCaches, GraphileCache, graphileCache } from './graphile-cache';
1
+ export { graphileCache, GraphileCacheEntry, closeAllCaches, ONE_HOUR_MS, FIVE_MINUTES_MS, EvictionReason, CacheEventEmitter, CacheEvictionEvent, cacheEvents, CacheConfig, getCacheConfig, CacheStats, getCacheStats, clearMatchingEntries } from './graphile-cache';
2
+ export { createGraphileInstance } from './create-instance';
package/index.js CHANGED
@@ -1,7 +1,21 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.graphileCache = exports.closeAllCaches = void 0;
3
+ exports.createGraphileInstance = exports.clearMatchingEntries = exports.getCacheStats = exports.getCacheConfig = exports.cacheEvents = exports.CacheEventEmitter = exports.FIVE_MINUTES_MS = exports.ONE_HOUR_MS = exports.closeAllCaches = exports.graphileCache = void 0;
4
4
  // Main exports from graphile-cache package
5
5
  var graphile_cache_1 = require("./graphile-cache");
6
- Object.defineProperty(exports, "closeAllCaches", { enumerable: true, get: function () { return graphile_cache_1.closeAllCaches; } });
6
+ // Cache instance and entry type
7
7
  Object.defineProperty(exports, "graphileCache", { enumerable: true, get: function () { return graphile_cache_1.graphileCache; } });
8
+ Object.defineProperty(exports, "closeAllCaches", { enumerable: true, get: function () { return graphile_cache_1.closeAllCaches; } });
9
+ // Time constants
10
+ Object.defineProperty(exports, "ONE_HOUR_MS", { enumerable: true, get: function () { return graphile_cache_1.ONE_HOUR_MS; } });
11
+ Object.defineProperty(exports, "FIVE_MINUTES_MS", { enumerable: true, get: function () { return graphile_cache_1.FIVE_MINUTES_MS; } });
12
+ // Event emitter for cache events
13
+ Object.defineProperty(exports, "CacheEventEmitter", { enumerable: true, get: function () { return graphile_cache_1.CacheEventEmitter; } });
14
+ Object.defineProperty(exports, "cacheEvents", { enumerable: true, get: function () { return graphile_cache_1.cacheEvents; } });
15
+ Object.defineProperty(exports, "getCacheConfig", { enumerable: true, get: function () { return graphile_cache_1.getCacheConfig; } });
16
+ Object.defineProperty(exports, "getCacheStats", { enumerable: true, get: function () { return graphile_cache_1.getCacheStats; } });
17
+ // Clear matching entries
18
+ Object.defineProperty(exports, "clearMatchingEntries", { enumerable: true, get: function () { return graphile_cache_1.clearMatchingEntries; } });
19
+ // Factory for creating PostGraphile v5 instances
20
+ var create_instance_1 = require("./create-instance");
21
+ Object.defineProperty(exports, "createGraphileInstance", { enumerable: true, get: function () { return create_instance_1.createGraphileInstance; } });
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "graphile-cache",
3
- "version": "2.1.0",
3
+ "version": "3.0.0",
4
4
  "author": "Constructive <developers@constructive.io>",
5
- "description": "PostGraphile LRU cache with automatic pool cleanup integration",
5
+ "description": "PostGraphile v5 LRU cache with automatic pool cleanup integration",
6
6
  "main": "index.js",
7
7
  "module": "esm/index.js",
8
8
  "types": "index.d.ts",
@@ -30,14 +30,15 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@pgpmjs/logger": "^2.1.0",
33
+ "express": "^5.2.1",
34
+ "grafserv": "^1.0.0-rc.4",
33
35
  "lru-cache": "^11.2.4",
34
- "pg": "^8.17.1",
35
- "pg-cache": "^2.1.0",
36
- "postgraphile": "^4.14.1"
36
+ "pg-cache": "^3.0.0",
37
+ "postgraphile": "^5.0.0-rc.4"
37
38
  },
38
39
  "devDependencies": {
39
- "@types/pg": "^8.16.0",
40
- "makage": "^0.1.12",
40
+ "@types/express": "^5.0.6",
41
+ "makage": "^0.1.10",
41
42
  "nodemon": "^3.1.10",
42
43
  "ts-node": "^10.9.2"
43
44
  },
@@ -47,7 +48,8 @@
47
48
  "cache",
48
49
  "lru",
49
50
  "postgresql",
50
- "constructive"
51
+ "constructive",
52
+ "v5"
51
53
  ],
52
- "gitHead": "048188f6b43ffaa6146e7694b2b0d35d34cb2945"
54
+ "gitHead": "b2daeefe49cdefb3d01ea63cf778fb9b847ab5fe"
53
55
  }