layercache 1.3.4 → 1.4.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.
Files changed (40) hide show
  1. package/README.md +42 -41
  2. package/dist/{chunk-BORDQ3LA.js → chunk-7KMKQ6QZ.js} +15 -1
  3. package/dist/{chunk-5RCAX2BQ.js → chunk-FFZCC7EQ.js} +3 -3
  4. package/dist/{chunk-4PPBOOXT.js → chunk-KJDFYE5T.js} +38 -26
  5. package/dist/cli.cjs +9 -9
  6. package/dist/cli.js +4 -4
  7. package/dist/{edge-DKkrQ_Ky.d.cts → edge-D2FpRlyS.d.cts} +71 -22
  8. package/dist/{edge-DKkrQ_Ky.d.ts → edge-D2FpRlyS.d.ts} +71 -22
  9. package/dist/edge.cjs +9 -9
  10. package/dist/edge.d.cts +1 -1
  11. package/dist/edge.d.ts +1 -1
  12. package/dist/edge.js +2 -2
  13. package/dist/index.cjs +399 -164
  14. package/dist/index.d.cts +6 -6
  15. package/dist/index.d.ts +6 -6
  16. package/dist/index.js +294 -81
  17. package/package.json +5 -5
  18. package/benchmarks/direct.ts +0 -221
  19. package/benchmarks/edge-utils.ts +0 -28
  20. package/benchmarks/edge.ts +0 -491
  21. package/benchmarks/http.ts +0 -99
  22. package/benchmarks/latency.ts +0 -45
  23. package/benchmarks/memory-pressure.ts +0 -144
  24. package/benchmarks/multi-process-fanout.ts +0 -231
  25. package/benchmarks/multi-process-worker.ts +0 -151
  26. package/benchmarks/paths.ts +0 -25
  27. package/benchmarks/queue-amplification-utils.ts +0 -48
  28. package/benchmarks/queue-amplification.ts +0 -230
  29. package/benchmarks/redis-latency-proxy.ts +0 -100
  30. package/benchmarks/redis.ts +0 -107
  31. package/benchmarks/scenario-utils.ts +0 -38
  32. package/benchmarks/server.ts +0 -157
  33. package/benchmarks/slow-redis-latency.ts +0 -309
  34. package/benchmarks/slow-redis-utils.ts +0 -29
  35. package/benchmarks/slow-redis.ts +0 -47
  36. package/benchmarks/stampede.ts +0 -26
  37. package/benchmarks/stats.ts +0 -46
  38. package/benchmarks/workload.ts +0 -77
  39. package/examples/express-api/index.ts +0 -31
  40. package/examples/nextjs-api-routes/route.ts +0 -16
@@ -1,230 +0,0 @@
1
- import { Redis } from 'ioredis'
2
- import { CacheStack, MemoryLayer, RedisLayer, RedisSingleFlightCoordinator } from '../src'
3
- import { buildConcurrencyLabel, summarizeQueueAmplification } from './queue-amplification-utils'
4
- import { REDIS_PORT, ensureRedisContainer, stopRedisContainer, unpauseRedisContainer, waitForRedisReady } from './redis'
5
- import { startRedisLatencyProxy } from './redis-latency-proxy'
6
- import { type CountedFetcher, createCountedFetcher, runConcurrent } from './scenario-utils'
7
- import { buildDelayLabel } from './slow-redis-utils'
8
-
9
- const LATENCY_LEVELS = [0, 100, 500] as const
10
- const CONCURRENCY_LEVELS = [1, 10, 50, 100, 250, 500] as const
11
- const TIMEOUT_MS = 30_000
12
-
13
- interface QueueAmplificationResult {
14
- label: string
15
- count: number
16
- minMs: number
17
- maxMs: number
18
- avgMs: number
19
- medianMs: number
20
- p95Ms: number
21
- delayLabel: string
22
- scenario: string
23
- concurrency: number
24
- concurrencyLabel: string
25
- totalWallClockMs: number
26
- amplificationVsSingle: number
27
- linearityRatio: number
28
- }
29
-
30
- interface BenchmarkCacheContext {
31
- cache: CacheStack
32
- memory: MemoryLayer
33
- originFetcher: CountedFetcher<[], { source: string; warmedAt: number }>
34
- }
35
-
36
- function round(value: number): number {
37
- return Number(value.toFixed(3))
38
- }
39
-
40
- function createBenchmarkRedis(port: number): Redis {
41
- const client = new Redis({
42
- host: '127.0.0.1',
43
- port,
44
- lazyConnect: true,
45
- maxRetriesPerRequest: 1,
46
- connectTimeout: 6_000,
47
- enableOfflineQueue: false,
48
- autoResendUnfulfilledCommands: false,
49
- retryStrategy: () => null
50
- })
51
-
52
- client.on('error', () => {})
53
- return client
54
- }
55
-
56
- async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
57
- return Promise.race([
58
- promise,
59
- new Promise<T>((_, reject) => {
60
- setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs)
61
- })
62
- ])
63
- }
64
-
65
- function createLayeredCache(redis: Redis, prefix: string, gracefulDegradation: boolean): BenchmarkCacheContext {
66
- const memory = new MemoryLayer({ ttl: 60, maxSize: 50 })
67
- const originFetcher = createCountedFetcher(async () => ({
68
- source: 'origin',
69
- warmedAt: Date.now()
70
- }))
71
-
72
- return {
73
- memory,
74
- originFetcher,
75
- cache: new CacheStack([memory, new RedisLayer({ client: redis, ttl: 300, prefix: `${prefix}:cache:` })], {
76
- stampedePrevention: true,
77
- ...(gracefulDegradation ? { gracefulDegradation: { retryAfterMs: 10_000 } } : {}),
78
- singleFlightCoordinator: new RedisSingleFlightCoordinator({
79
- client: redis,
80
- prefix: `${prefix}:sf:`
81
- })
82
- })
83
- }
84
- }
85
-
86
- async function warmCache(context: BenchmarkCacheContext, key: string): Promise<void> {
87
- await context.cache.get(key, () => context.originFetcher.run(), { ttl: 60 })
88
- }
89
-
90
- async function measureBatch(
91
- context: BenchmarkCacheContext,
92
- key: string,
93
- delayLabel: string,
94
- scenario: string,
95
- concurrency: number,
96
- baselineWallClockMs: number
97
- ): Promise<QueueAmplificationResult> {
98
- await context.memory.clear()
99
-
100
- const originCountBefore = context.originFetcher.getCount()
101
- const batchStartedAt = process.hrtime.bigint()
102
- const requestLatenciesMs = await runConcurrent(concurrency, async () => {
103
- const requestStartedAt = process.hrtime.bigint()
104
- await withTimeout(
105
- context.cache.get(key, () => context.originFetcher.run(), { ttl: 60 }),
106
- TIMEOUT_MS,
107
- `${delayLabel} ${scenario} ${buildConcurrencyLabel(concurrency)}`
108
- )
109
-
110
- return Number(process.hrtime.bigint() - requestStartedAt) / 1_000_000
111
- })
112
- const totalWallClockMs = Number(process.hrtime.bigint() - batchStartedAt) / 1_000_000
113
-
114
- const originFetchCount = context.originFetcher.getCount() - originCountBefore
115
- if (originFetchCount !== 0) {
116
- throw new Error(
117
- `${delayLabel} ${scenario} ${buildConcurrencyLabel(concurrency)} unexpectedly fetched origin ${originFetchCount} times`
118
- )
119
- }
120
-
121
- return summarizeQueueAmplification({
122
- delayLabel,
123
- scenario,
124
- concurrency,
125
- totalWallClockMs,
126
- requestLatenciesMs,
127
- baselineWallClockMs
128
- })
129
- }
130
-
131
- async function runDelayBenchmarks(delayMs: number): Promise<QueueAmplificationResult[]> {
132
- const delayLabel = buildDelayLabel(delayMs)
133
- const proxy = await startRedisLatencyProxy(REDIS_PORT)
134
- proxy.setLatencyMs(delayMs)
135
-
136
- const strictRedis = createBenchmarkRedis(proxy.port)
137
- const gracefulRedis = createBenchmarkRedis(proxy.port)
138
-
139
- await Promise.all([strictRedis.connect(), gracefulRedis.connect()])
140
- await Promise.all([strictRedis.ping(), gracefulRedis.ping()])
141
-
142
- const strict = createLayeredCache(strictRedis, `queue:${delayMs}:strict:${Date.now()}`, false)
143
- const graceful = createLayeredCache(gracefulRedis, `queue:${delayMs}:graceful:${Date.now()}`, true)
144
-
145
- try {
146
- const strictKey = `queue:strict:${delayMs}`
147
- const gracefulKey = `queue:graceful:${delayMs}`
148
-
149
- await warmCache(strict, strictKey)
150
- await warmCache(graceful, gracefulKey)
151
-
152
- const scenarios = [
153
- { scenario: 'strict-l2-hit', context: strict, key: strictKey },
154
- { scenario: 'graceful-l2-hit', context: graceful, key: gracefulKey }
155
- ]
156
-
157
- const results: QueueAmplificationResult[] = []
158
-
159
- for (const { scenario, context, key } of scenarios) {
160
- let baselineWallClockMs = 0
161
-
162
- for (const concurrency of CONCURRENCY_LEVELS) {
163
- const effectiveBaseline = baselineWallClockMs || 1
164
- const summary = await measureBatch(
165
- context,
166
- key,
167
- delayLabel,
168
- scenario,
169
- concurrency,
170
- concurrency === 1 ? effectiveBaseline : baselineWallClockMs
171
- )
172
-
173
- if (concurrency === 1) {
174
- baselineWallClockMs = summary.totalWallClockMs
175
- results.push({
176
- ...summary,
177
- amplificationVsSingle: 1,
178
- linearityRatio: 1
179
- })
180
- continue
181
- }
182
-
183
- results.push(summary)
184
- }
185
- }
186
-
187
- return results
188
- } finally {
189
- await strict.cache.disconnect().catch(() => {})
190
- await graceful.cache.disconnect().catch(() => {})
191
- await strictRedis.quit().catch(() => {})
192
- await gracefulRedis.quit().catch(() => {})
193
- await proxy.close().catch(() => {})
194
- }
195
- }
196
-
197
- async function main(): Promise<void> {
198
- await ensureRedisContainer()
199
- await waitForRedisReady()
200
-
201
- try {
202
- const results: QueueAmplificationResult[] = []
203
- for (const delayMs of LATENCY_LEVELS) {
204
- results.push(...(await runDelayBenchmarks(delayMs)))
205
- }
206
-
207
- console.table(
208
- results.map((result) => ({
209
- delay: result.delayLabel,
210
- scenario: result.scenario,
211
- concurrency: result.concurrency,
212
- totalWallClockMs: round(result.totalWallClockMs),
213
- avgMs: result.avgMs,
214
- p95Ms: result.p95Ms,
215
- maxMs: result.maxMs,
216
- amplificationVsSingle: result.amplificationVsSingle,
217
- linearityRatio: result.linearityRatio
218
- }))
219
- )
220
- console.log(JSON.stringify({ type: 'queue-amplification-benchmark', results }, null, 2))
221
- } finally {
222
- await unpauseRedisContainer().catch(() => {})
223
- await stopRedisContainer()
224
- }
225
- }
226
-
227
- void main().catch((error) => {
228
- console.error(error)
229
- process.exitCode = 1
230
- })
@@ -1,100 +0,0 @@
1
- import { type Server, type Socket, connect, createServer } from 'node:net'
2
-
3
- export interface RedisLatencyProxy {
4
- port: number
5
- setLatencyMs: (latencyMs: number) => void
6
- close: () => Promise<void>
7
- }
8
-
9
- export async function startRedisLatencyProxy(targetPort: number): Promise<RedisLatencyProxy> {
10
- let latencyMs = 0
11
- const sockets = new Set<Socket>()
12
-
13
- const server = createServer((clientSocket) => {
14
- const upstreamSocket = connect({
15
- host: '127.0.0.1',
16
- port: targetPort
17
- })
18
-
19
- sockets.add(clientSocket)
20
- sockets.add(upstreamSocket)
21
-
22
- clientSocket.on('error', () => {})
23
- upstreamSocket.on('error', () => {})
24
- clientSocket.on('close', () => {
25
- sockets.delete(clientSocket)
26
- if (!upstreamSocket.destroyed) {
27
- upstreamSocket.destroy()
28
- }
29
- })
30
- upstreamSocket.on('close', () => {
31
- sockets.delete(upstreamSocket)
32
- if (!clientSocket.destroyed) {
33
- clientSocket.destroy()
34
- }
35
- })
36
-
37
- clientSocket.on('data', (chunk) => {
38
- setTimeout(() => {
39
- if (!upstreamSocket.destroyed) {
40
- upstreamSocket.write(chunk)
41
- }
42
- }, latencyMs / 2)
43
- })
44
-
45
- upstreamSocket.on('data', (chunk) => {
46
- setTimeout(() => {
47
- if (!clientSocket.destroyed) {
48
- clientSocket.write(chunk)
49
- }
50
- }, latencyMs / 2)
51
- })
52
-
53
- clientSocket.on('end', () => upstreamSocket.end())
54
- upstreamSocket.on('end', () => clientSocket.end())
55
- })
56
-
57
- await listen(server)
58
-
59
- const address = server.address()
60
- if (!address || typeof address === 'string') {
61
- throw new Error('Latency proxy failed to acquire a TCP port')
62
- }
63
-
64
- return {
65
- port: address.port,
66
- setLatencyMs(nextLatencyMs: number) {
67
- latencyMs = nextLatencyMs
68
- },
69
- async close() {
70
- for (const socket of sockets) {
71
- socket.destroy()
72
- }
73
-
74
- await closeServer(server)
75
- }
76
- }
77
- }
78
-
79
- function listen(server: Server): Promise<void> {
80
- return new Promise((resolve, reject) => {
81
- server.once('error', reject)
82
- server.listen(0, '127.0.0.1', () => {
83
- server.off('error', reject)
84
- resolve()
85
- })
86
- })
87
- }
88
-
89
- function closeServer(server: Server): Promise<void> {
90
- return new Promise((resolve, reject) => {
91
- server.close((error) => {
92
- if (error) {
93
- reject(error)
94
- return
95
- }
96
-
97
- resolve()
98
- })
99
- })
100
- }
@@ -1,107 +0,0 @@
1
- import { execFile } from 'node:child_process'
2
- import { promisify } from 'node:util'
3
- import { Redis } from 'ioredis'
4
-
5
- const execFileAsync = promisify(execFile)
6
-
7
- export const REDIS_CONTAINER_NAME = 'layercache-bench-redis'
8
- export const REDIS_PORT = 6390
9
- export const REDIS_IMAGE = 'redis:7-alpine'
10
-
11
- async function docker(args: string[]): Promise<string> {
12
- const { stdout } = await execFileAsync('docker', args, {
13
- cwd: process.cwd(),
14
- maxBuffer: 10 * 1024 * 1024
15
- })
16
-
17
- return stdout.trim()
18
- }
19
-
20
- async function containerExists(): Promise<boolean> {
21
- try {
22
- await docker(['inspect', REDIS_CONTAINER_NAME])
23
- return true
24
- } catch {
25
- return false
26
- }
27
- }
28
-
29
- async function isContainerRunning(): Promise<boolean> {
30
- try {
31
- return (await docker(['inspect', '-f', '{{.State.Running}}', REDIS_CONTAINER_NAME])) === 'true'
32
- } catch {
33
- return false
34
- }
35
- }
36
-
37
- async function isContainerPaused(): Promise<boolean> {
38
- try {
39
- return (await docker(['inspect', '-f', '{{.State.Paused}}', REDIS_CONTAINER_NAME])) === 'true'
40
- } catch {
41
- return false
42
- }
43
- }
44
-
45
- export async function ensureRedisContainer(): Promise<void> {
46
- await docker(['version', '--format', '{{.Server.Version}}'])
47
-
48
- if (await isContainerRunning()) {
49
- return
50
- }
51
-
52
- if (await containerExists()) {
53
- await docker(['start', REDIS_CONTAINER_NAME])
54
- return
55
- }
56
-
57
- await docker(['run', '-d', '--rm', '--name', REDIS_CONTAINER_NAME, '-p', `${REDIS_PORT}:6379`, REDIS_IMAGE])
58
- }
59
-
60
- export async function stopRedisContainer(): Promise<void> {
61
- try {
62
- await docker(['rm', '-f', REDIS_CONTAINER_NAME])
63
- } catch {
64
- // Ignore cleanup failures so benchmark reporting can continue.
65
- }
66
- }
67
-
68
- export async function pauseRedisContainer(): Promise<void> {
69
- if (await isContainerPaused()) {
70
- return
71
- }
72
-
73
- await docker(['pause', REDIS_CONTAINER_NAME])
74
- }
75
-
76
- export async function unpauseRedisContainer(): Promise<void> {
77
- if (!(await isContainerPaused())) {
78
- return
79
- }
80
-
81
- await docker(['unpause', REDIS_CONTAINER_NAME])
82
- }
83
-
84
- export function createRedisClient(port = REDIS_PORT): Redis {
85
- return new Redis(`redis://127.0.0.1:${port}`, {
86
- maxRetriesPerRequest: null
87
- })
88
- }
89
-
90
- export async function waitForRedisReady(retries = 40): Promise<void> {
91
- for (let attempt = 0; attempt < retries; attempt += 1) {
92
- try {
93
- const response = await docker(['exec', REDIS_CONTAINER_NAME, 'redis-cli', 'ping'])
94
- if (response === 'PONG') {
95
- return
96
- }
97
- } catch {
98
- // Allow Redis a short time to start up after the container launches.
99
- }
100
-
101
- await new Promise((resolve) => {
102
- setTimeout(resolve, 250)
103
- })
104
- }
105
-
106
- throw new Error('Redis did not become ready in time')
107
- }
@@ -1,38 +0,0 @@
1
- import { type DurationSummary, summarizeDurations } from './stats'
2
-
3
- export interface CountedFetcher<TArgs extends unknown[], TResult> {
4
- run: (...args: TArgs) => Promise<TResult>
5
- getCount: () => number
6
- }
7
-
8
- export interface ScenarioSummary extends DurationSummary {
9
- fetchCount: number
10
- }
11
-
12
- export function createCountedFetcher<TArgs extends unknown[], TResult>(
13
- fetcher: (...args: TArgs) => Promise<TResult>
14
- ): CountedFetcher<TArgs, TResult> {
15
- let count = 0
16
-
17
- return {
18
- run: async (...args: TArgs) => {
19
- count += 1
20
- return fetcher(...args)
21
- },
22
- getCount: () => count
23
- }
24
- }
25
-
26
- export async function runConcurrent<TResult>(
27
- count: number,
28
- task: (index: number) => Promise<TResult>
29
- ): Promise<TResult[]> {
30
- return Promise.all(Array.from({ length: count }, (_, index) => task(index)))
31
- }
32
-
33
- export function summarizeScenario(label: string, samples: number[], fetchCount: number): ScenarioSummary {
34
- return {
35
- ...summarizeDurations(label, samples),
36
- fetchCount
37
- }
38
- }
@@ -1,157 +0,0 @@
1
- import { type IncomingMessage, type Server, type ServerResponse, createServer } from 'node:http'
2
-
3
- import type { Redis } from 'ioredis'
4
- import { CacheStack, MemoryLayer, RedisLayer, RedisSingleFlightCoordinator } from '../src'
5
- import { resolveBenchmarkFixturePath } from './paths'
6
- import { createCountedFetcher } from './scenario-utils'
7
- import { ensureFixtureFile, loadUserFromFixture } from './workload'
8
-
9
- export interface BenchmarkServerHandle {
10
- port: number
11
- close: () => Promise<void>
12
- reset: () => Promise<void>
13
- warm: () => Promise<void>
14
- }
15
-
16
- const DEFAULT_USER_ID = 4242
17
-
18
- function createOriginFetcher(filePath: string) {
19
- return createCountedFetcher(async (userId: number) => loadUserFromFixture(filePath, userId))
20
- }
21
-
22
- function respondJson(serverResponse: ServerResponse<IncomingMessage>, statusCode: number, payload: unknown): void {
23
- serverResponse.statusCode = statusCode
24
- serverResponse.setHeader('content-type', 'application/json')
25
- serverResponse.end(JSON.stringify(payload))
26
- }
27
-
28
- export async function startBenchmarkServer(redis: Redis): Promise<BenchmarkServerHandle> {
29
- const fixturePath = resolveBenchmarkFixturePath()
30
- await ensureFixtureFile(fixturePath)
31
-
32
- const memoryCache = new CacheStack([new MemoryLayer({ ttl: 60, maxSize: 2_000 })], {
33
- stampedePrevention: true
34
- })
35
-
36
- const layeredCache = new CacheStack(
37
- [
38
- new MemoryLayer({ ttl: 60, maxSize: 2_000 }),
39
- new RedisLayer({ client: redis, ttl: 300, prefix: 'layercache-bench:http:' })
40
- ],
41
- {
42
- stampedePrevention: true,
43
- singleFlightCoordinator: new RedisSingleFlightCoordinator({
44
- client: redis,
45
- prefix: 'layercache-bench:http:single-flight:'
46
- })
47
- }
48
- )
49
-
50
- const gracefulLayeredCache = new CacheStack(
51
- [
52
- new MemoryLayer({ ttl: 60, maxSize: 2_000 }),
53
- new RedisLayer({ client: redis, ttl: 300, prefix: 'layercache-bench:http:graceful:' })
54
- ],
55
- {
56
- stampedePrevention: true,
57
- gracefulDegradation: { retryAfterMs: 10_000 },
58
- singleFlightCoordinator: new RedisSingleFlightCoordinator({
59
- client: redis,
60
- prefix: 'layercache-bench:http:graceful:single-flight:'
61
- })
62
- }
63
- )
64
-
65
- const origin = createOriginFetcher(fixturePath)
66
-
67
- const server = createServer(async (request, response) => {
68
- const url = new URL(request.url ?? '/', 'http://127.0.0.1')
69
- const userId = Number(url.searchParams.get('id') ?? DEFAULT_USER_ID)
70
-
71
- try {
72
- if (url.pathname === '/nocache') {
73
- const user = await origin.run(userId)
74
- respondJson(response, 200, { route: 'nocache', user })
75
- return
76
- }
77
-
78
- if (url.pathname === '/memory') {
79
- const user = await memoryCache.get(`http:user:${userId}`, () => origin.run(userId))
80
- respondJson(response, 200, { route: 'memory', user })
81
- return
82
- }
83
-
84
- if (url.pathname === '/layered') {
85
- const user = await layeredCache.get(`http:user:${userId}`, () => origin.run(userId))
86
- respondJson(response, 200, { route: 'layered', user })
87
- return
88
- }
89
-
90
- if (url.pathname === '/layered-graceful') {
91
- const user = await gracefulLayeredCache.get(`http:user:${userId}`, () => origin.run(userId))
92
- respondJson(response, 200, { route: 'layered-graceful', user })
93
- return
94
- }
95
-
96
- if (url.pathname === '/health') {
97
- respondJson(response, 200, { ok: true, fetchCount: origin.getCount() })
98
- return
99
- }
100
-
101
- respondJson(response, 404, { error: 'not-found' })
102
- } catch (error) {
103
- respondJson(response, 500, {
104
- error: error instanceof Error ? error.message : String(error)
105
- })
106
- }
107
- })
108
-
109
- await listen(server)
110
-
111
- const address = server.address()
112
- if (!address || typeof address === 'string') {
113
- throw new Error('Unexpected server address state')
114
- }
115
-
116
- return {
117
- port: address.port,
118
- reset: async () => {
119
- await memoryCache.clear()
120
- await layeredCache.clear()
121
- await gracefulLayeredCache.clear()
122
- await redis.flushdb()
123
- },
124
- warm: async () => {
125
- await memoryCache.get(`http:user:${DEFAULT_USER_ID}`, () => origin.run(DEFAULT_USER_ID))
126
- await layeredCache.get(`http:user:${DEFAULT_USER_ID}`, () => origin.run(DEFAULT_USER_ID))
127
- await gracefulLayeredCache.get(`http:user:${DEFAULT_USER_ID}`, () => origin.run(DEFAULT_USER_ID))
128
- },
129
- close: async () => {
130
- await Promise.all([memoryCache.disconnect(), layeredCache.disconnect(), gracefulLayeredCache.disconnect()])
131
- await closeServer(server)
132
- }
133
- }
134
- }
135
-
136
- function listen(server: Server): Promise<void> {
137
- return new Promise((resolve, reject) => {
138
- server.once('error', reject)
139
- server.listen(0, '127.0.0.1', () => {
140
- server.off('error', reject)
141
- resolve()
142
- })
143
- })
144
- }
145
-
146
- function closeServer(server: Server): Promise<void> {
147
- return new Promise((resolve, reject) => {
148
- server.close((error) => {
149
- if (error) {
150
- reject(error)
151
- return
152
- }
153
-
154
- resolve()
155
- })
156
- })
157
- }