TypeScript uses type inference to provide some type safety. It “compiles” into JavaScript, and has become a popular enhancement to JavaScript applications.
JavaScript is single-threaded. Its concurrency mechanism is its event loop. In a nutshell, here’s how the JavaScript event loop works:
There are two pieces to the JavaScript event loop: The event loop itself, and the event FIFO (First In First Out) queue.
This is very useful where it comes to IO, such as networking IO, or reading files. These two mechanisms work in similar ways; we’re going to focus on networking.
Where the OS makes a network HTTP request, or any other type of request where it expects a response, it opens a file descriptor, and returns immediately. A separate mechanism (“select”, “poll”, or “epoll”) polls for the response, and, where the response is available, writes the response to that file descriptor.
This is called “non-blocking IO”, and JavaScript takes advantage of this with its event loop. A non-blocking IO request is put onto the event loop and returns control to the JS main thread immediately. The request is handed off to the OS as described above. The OS gets the response, which is placed into JavaScript’s FIFO queue. The return values from the queue are injected into the main function code where it calls “await”. If the main function does not call “await”, the values will return after the main function returns.
Our Code
Here is a Go application that provides a way to generate fake “transactions”. It has three endpoints:
Each endpoint sleeps for 1 second before it delivers its response, in order to illustrate the power of JS concurrency.
Here is our TypeScript app It’s a simple little HTTP service with two endpoints.
The two endpoints make requests to all three of the Go services endpoints, combine the results, and return them. /api/v1/concurrent_transactions performs its requests concurrently, and /api/v1/serial_transactions performs its requests serially.
Here are the relevant part of the charlie-typescript-demo code. In api/v1/concurrent_transactions:
1 if (req.method === 'GET' && req.url === '/api/v1/concurrent_transactions') {
2 authenticateRequest(req, res, async () => {
3 try {
4 // Make concurrent requests to the external transactions service
5 const [visaResponse, amexResponse, masterCardResponse] = await Promise.all([
6 getVisaTransactions(),
7 getAmexTransactions(),
8 getMasterCardTransactions()
9 ]);
10
11 const allTransactions = [
12 ...visaResponse.transactions,
13 ...amexResponse.transactions,
14 ...masterCardResponse.transactions
15 ];
16
17 res.statusCode = 200;
18 res.setHeader('Content-Type', 'application/json');
19 res.end(JSON.stringify({ transactions: allTransactions }));
20 ...
By contrast, in /api/v1/serial_transactions, we send off one request, await its response, then send the next one:
1 } else if (req.method === 'GET' && req.url === '/api/v1/serial_transactions') {
2 authenticateRequest(req, res, async () => {
3 try {
4 // Make serial requests to the external transactions service
5 const visaResponse = await getVisaTransactions();
6 const amexResponse = await getAmexTransactions();
7 const masterCardResponse = await getMasterCardTransactions();
8
9 // Combine transactions from all responses
10 const allTransactions = [
11 ...visaResponse.transactions,
12 ...amexResponse.transactions,
13 ...masterCardResponse.transactions
14 ];
15
16 res.statusCode = 200;
17 res.setHeader('Content-Type', 'application/json');
18 res.end(JSON.stringify({ transactions: allTransactions }));
19 ...
As you’ll see in this brief video, the response for /api/v1/concurrent_transactions returns in about 1 second, whereas the response for /api/v1/serial_transactions returns in about 3 seconds.