-
Notifications
You must be signed in to change notification settings - Fork 179
Expand file tree
/
Copy pathhttpServers.ts
More file actions
180 lines (152 loc) · 5.19 KB
/
httpServers.ts
File metadata and controls
180 lines (152 loc) · 5.19 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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
import * as http from 'http'
import type { AddressInfo } from 'net'
import { test } from '@playwright/test'
import type { Browser } from '@playwright/test'
import { getIp } from '../../../envUtils'
const MAX_SERVER_CREATION_RETRY = 5
// Not all port are available with BrowserStack, see https://www.browserstack.com/question/664
// Pick a ports in range 9200-9400
const PORT_MIN = 9200
const PORT_MAX = 9400
export type ServerApp = (req: http.IncomingMessage, res: http.ServerResponse) => any
export type MockServerApp = ServerApp & {
getLargeResponseWroteSize(): number
}
export type IntakeServerApp = ServerApp & {
setDebuggerProbes(probes: object[]): void
}
export interface Server<App extends ServerApp> {
origin: string
app: App
bindServerApp(serverApp: App): void
waitForIdle(): Promise<void>
}
export interface Servers {
base: Server<MockServerApp>
intake: Server<IntakeServerApp>
crossOrigin: Server<MockServerApp>
}
let serversSingleton: undefined | Servers
export async function getTestServers() {
if (!serversSingleton) {
serversSingleton = {
base: await createServer(),
crossOrigin: await createServer(),
intake: await createServer(),
}
}
return serversSingleton
}
export async function waitForServersIdle() {
// Wait for `idleWaitDuration` ms before checking idle state, to account for requests that may
// still be in-flight from the browser and haven't reached the server yet.
await new Promise((resolve) => setTimeout(resolve, idleWaitDuration))
const servers = await getTestServers()
await Promise.all([servers.base.waitForIdle(), servers.crossOrigin.waitForIdle(), servers.intake.waitForIdle()])
}
async function createServer<App extends ServerApp>(): Promise<Server<App>> {
const server = await instantiateServer()
const address = getIp()
const { port } = server.address() as AddressInfo
let serverApp: App | undefined
server.on('request', (req: http.IncomingMessage, res: http.ServerResponse) => {
if (serverApp) {
serverApp(req, res)
} else {
res.writeHead(202)
res.end()
}
})
return {
bindServerApp(newServerApp: App) {
serverApp = newServerApp
},
get app() {
if (!serverApp) {
throw new Error('no server app bound')
}
return serverApp
},
origin: `http://${address}:${port}`,
waitForIdle: createServerIdleWaiter(server),
}
}
async function instantiateServer(): Promise<http.Server> {
for (let tryNumber = 0; tryNumber < MAX_SERVER_CREATION_RETRY; tryNumber += 1) {
const port = PORT_MIN + Math.floor(Math.random() * (PORT_MAX - PORT_MIN + 1))
try {
return await instantiateServerOnPort(port)
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'EADDRINUSE') {
continue
}
throw error
}
}
throw new Error(`Failed to create a server after ${MAX_SERVER_CREATION_RETRY} retries`)
}
function instantiateServerOnPort(port: number): Promise<http.Server> {
return new Promise((resolve, reject) => {
const server = http.createServer()
server.on('error', reject)
server.listen(port, () => {
resolve(server)
})
})
}
function createServerIdleWaiter(server: http.Server) {
const idleWaiter = createIdleWaiter()
server.on('request', (_, res: http.ServerResponse) => {
idleWaiter.pushActivity(new Promise((resolve) => res.on('close', resolve)))
})
return () => idleWaiter.idlePromise
}
let idleWaitDuration = 500
test.beforeAll(async ({ browser }) => {
const latency = await measureServerLatency(browser)
idleWaitDuration = Math.max(500, latency * 1.5)
console.log(`Server latency: ${latency}ms`)
})
async function measureServerLatency(browser: Browser): Promise<number> {
// We measure the round-trip time to the test server to calibrate how long we wait after the
// last pending request before considering the server idle. In BrowserStack, the browser runs
// remotely, so this latency can be significant.
const { intake: server } = await getTestServers()
const page = await browser.newPage()
const start = Date.now()
await page.goto(server.origin)
return Date.now() - start
}
function createIdleWaiter() {
let idlePromise = Promise.resolve()
const pendingActivities = new Set<Promise<void>>()
let waitTimeoutId: NodeJS.Timeout
let resolveIdlePromise: undefined | (() => void)
return {
pushActivity(activity: Promise<void>) {
if (!resolveIdlePromise) {
// Before this activity, we were idle, so create a new promise that will be resolved when we
// are idle again.
idlePromise = new Promise((resolve) => {
resolveIdlePromise = resolve
})
}
// Cancel any timeout that would resolve the idle promise.
clearTimeout(waitTimeoutId)
pendingActivities.add(activity)
void activity.then(() => {
pendingActivities.delete(activity)
if (pendingActivities.size === 0) {
// If no more activity is pending, wait a bit before switching to idle state.
waitTimeoutId = setTimeout(() => {
resolveIdlePromise!()
resolveIdlePromise = undefined
}, idleWaitDuration)
}
})
},
get idlePromise() {
return idlePromise
},
}
}