-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathgraphile-cache.test.ts
More file actions
133 lines (112 loc) · 4.28 KB
/
graphile-cache.test.ts
File metadata and controls
133 lines (112 loc) · 4.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import { createServer } from 'http';
import express from 'express';
import { LRUCache } from 'lru-cache';
/**
* Regression test for double-disposal of PostGraphile instances.
*
* The bug: closeAllCaches() manually disposed each entry via disposeEntry(),
* which tracked disposal by key string in a Set and deleted the key in a
* `finally` block. Then graphileCache.clear() triggered the LRU dispose
* callback for the same entries — but the guard key was already gone, so
* pgl.release() was called a second time on an already-released instance.
*
* The fix: track disposal by entry object reference (WeakSet) so the guard
* persists across the manual-dispose → clear() sequence.
*/
// ── Helpers ──────────────────────────────────────────────────────────
/** Minimal mock that records release() calls and throws on double-release */
function createMockPgl() {
let released = false;
return {
release: jest.fn(async () => {
if (released) {
throw new Error('PostGraphile instance has been released');
}
released = true;
}),
get isReleased() {
return released;
}
};
}
function createMockEntry(key: string) {
const pgl = createMockPgl();
const handler = express();
const httpServer = createServer(handler);
return {
entry: {
pgl: pgl as any,
serv: {} as any,
handler,
httpServer,
cacheKey: key,
createdAt: Date.now()
},
pgl
};
}
// ── Tests ────────────────────────────────────────────────────────────
// We import the real module so the WeakSet-based guard is exercised.
// pg-cache is a workspace dependency that may not resolve in isolation,
// so we mock it along with @pgpmjs/logger.
jest.mock('pg-cache', () => ({
pgCache: {
registerCleanupCallback: jest.fn(() => jest.fn()),
close: jest.fn(async () => {})
}
}));
jest.mock('@pgpmjs/logger', () => ({
Logger: class {
info = jest.fn();
warn = jest.fn();
error = jest.fn();
debug = jest.fn();
success = jest.fn();
}
}));
import {
graphileCache,
closeAllCaches,
type GraphileCacheEntry
} from '../src/graphile-cache';
describe('graphile-cache', () => {
afterEach(async () => {
// Ensure the cache is clean between tests.
// closeAllCaches resets the singleton promise, so we can call it again.
graphileCache.clear();
});
describe('closeAllCaches – double-disposal regression', () => {
it('should call pgl.release() exactly once per entry', async () => {
const { entry: entry1, pgl: pgl1 } = createMockEntry('api-one');
const { entry: entry2, pgl: pgl2 } = createMockEntry('api-two');
graphileCache.set('api-one', entry1 as GraphileCacheEntry);
graphileCache.set('api-two', entry2 as GraphileCacheEntry);
await closeAllCaches();
// Each release should be called exactly once — not twice.
expect(pgl1.release).toHaveBeenCalledTimes(1);
expect(pgl2.release).toHaveBeenCalledTimes(1);
});
it('should not throw when closeAllCaches triggers LRU dispose after manual disposal', async () => {
const { entry, pgl } = createMockEntry('api-disposable');
graphileCache.set('api-disposable', entry as GraphileCacheEntry);
// This should complete without the mock throwing
// "PostGraphile instance has been released"
await expect(closeAllCaches()).resolves.toBeUndefined();
expect(pgl.release).toHaveBeenCalledTimes(1);
});
it('should handle empty cache gracefully', async () => {
await expect(closeAllCaches()).resolves.toBeUndefined();
});
});
describe('LRU eviction – single disposal', () => {
it('should dispose entry once on LRU eviction', async () => {
const { entry, pgl } = createMockEntry('api-evict');
graphileCache.set('api-evict', entry as GraphileCacheEntry);
// Delete triggers the LRU dispose callback
graphileCache.delete('api-evict');
// Give the async fire-and-forget disposal time to complete
await new Promise((r) => setTimeout(r, 50));
expect(pgl.release).toHaveBeenCalledTimes(1);
});
});
});