graphile-cache 2.0.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 +24 -25
- package/create-instance.d.ts +17 -0
- package/create-instance.js +38 -0
- package/esm/create-instance.js +31 -0
- package/esm/graphile-cache.js +164 -8
- package/esm/index.js +11 -1
- package/graphile-cache.d.ts +80 -8
- package/graphile-cache.js +169 -9
- package/index.d.ts +2 -1
- package/index.js +16 -2
- package/package.json +11 -9
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/
|
|
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/
|
|
9
|
-
<img height="20" src="https://github.com/
|
|
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
|
-
|
|
16
|
+
PostGraphile instance LRU cache with automatic cleanup when PostgreSQL pools are disposed.
|
|
22
17
|
|
|
23
|
-
##
|
|
18
|
+
## Installation
|
|
24
19
|
|
|
25
20
|
```bash
|
|
26
21
|
npm install graphile-cache pg-cache
|
|
27
22
|
```
|
|
28
23
|
|
|
29
|
-
|
|
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
|
-
##
|
|
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
|
-
##
|
|
38
|
+
## Usage
|
|
42
39
|
|
|
43
|
-
### Basic
|
|
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
|
|
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
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
+
};
|
package/esm/graphile-cache.js
CHANGED
|
@@ -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
|
-
|
|
6
|
-
const
|
|
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:
|
|
11
|
-
ttl:
|
|
98
|
+
max: initialConfig.max,
|
|
99
|
+
ttl: initialConfig.ttl,
|
|
12
100
|
updateAgeOnGet: true,
|
|
13
|
-
dispose: (
|
|
14
|
-
|
|
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
|
|
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 {
|
|
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';
|
package/graphile-cache.d.ts
CHANGED
|
@@ -1,10 +1,82 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
1
2
|
import { LRUCache } from 'lru-cache';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export
|
|
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
|
-
|
|
9
|
-
|
|
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:
|
|
14
|
-
ttl:
|
|
105
|
+
max: initialConfig.max,
|
|
106
|
+
ttl: initialConfig.ttl,
|
|
15
107
|
updateAgeOnGet: true,
|
|
16
|
-
dispose: (
|
|
17
|
-
|
|
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
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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": "
|
|
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",
|
|
@@ -29,14 +29,15 @@
|
|
|
29
29
|
"test:watch": "jest --watch"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@pgpmjs/logger": "^2.
|
|
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": "^
|
|
35
|
-
"
|
|
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/
|
|
40
|
+
"@types/express": "^5.0.6",
|
|
40
41
|
"makage": "^0.1.10",
|
|
41
42
|
"nodemon": "^3.1.10",
|
|
42
43
|
"ts-node": "^10.9.2"
|
|
@@ -47,7 +48,8 @@
|
|
|
47
48
|
"cache",
|
|
48
49
|
"lru",
|
|
49
50
|
"postgresql",
|
|
50
|
-
"constructive"
|
|
51
|
+
"constructive",
|
|
52
|
+
"v5"
|
|
51
53
|
],
|
|
52
|
-
"gitHead": "
|
|
54
|
+
"gitHead": "b2daeefe49cdefb3d01ea63cf778fb9b847ab5fe"
|
|
53
55
|
}
|