With everything that we have learned, we are going to focus on a use case that is quite prevalent in reporting systems and most types of operation GUIs—a large chunk of data that needs to have other data added to it (some call this decorating the data and others call this attribution). An example of this is that we have the buy and sell orders for a list of customers.
This data may come back in the following manner:
{
customerId : "<guid>",
buy : 1000000,
sell : 1000000
}
With this data, we may want to add some context that the customer ID is associated with. We could go about this in two ways:
- First, we could have a join operation done in the database that adds the required information for the user.
- Second, and the one we will be illustrating here, is adding this data on the frontend when we get the base-level query. This means when our application starts, we would fetch all of this attribution data and store it in some background cache. Next, when we make a request, we will also make a request to the cache for the corresponding data.
For us to achieve the second option, we will implement two of the technologies that we learned previously, the SharedWorker and the postMessage interface:
- We create a base-level HTML file that has a template for each row of data. We will not go into a deep dive of creating a web component as we did in Chapter 3, Vanilla Land – Looking at the Modern Web, but we will use it to create our table rows on demand:
<body>
<template id="row">
<tr>
<td class="name"></td>
<td class="zip"></td>
<td class="phone"></td>
<td class="email"></td>
<td class="buy"></td>
<td class="sell"></td>
</tr>
</template>
<table id="buysellorders">
<thead>
<tr>
<th>Customer Name</th>
<th>Zipcode</th>
<th>Phone Number</th>
<th>Email</th>
<th>Buy Order Amount</th>
<th>Sell Order Amount</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</body>
- We set up some pointers to our template and table so we can do quick inserts. On top of this, we can create a placeholder for the SharedWorker that we are about to create:
const tableBody = document.querySelector('#buysellorders > tbody');
const rowTemplate = document.querySelector('#row');
const worker = new SharedWorker('<fill in>', {name : 'cache'});
- With this basic setup, we can create our SharedWorker and give it some base-level data. To do this, we are going to use the website https://www.mockaroo.com/. This will allow us to create a bunch of random data without having to think of it ourselves. We can change the data to whatever we want but, in our case, we will go with the following options:
- id: Row number
- full_name: Full name
- email: Email address
- phone: Phone
- zipcode: Digit sequence: ######
- With these options filled in, we can change the format to JSON and save by clicking Download Data. With this done, we can build out our SharedWorker. Similar to our other SharedWorker, we will take the onconnect handler and add an onmessage handler for the port that comes in:
onconnect = function(e) {
let port = e.ports[0];
port.onmessage = function(e) {
// do something
}
}
- Next, we launch our SharedWorker back in our HTML file:
const worker = new SharedWorker('cache_shared.js', 'cache');
- Now, when our SharedWorker is launched, we will load the file by utilizing importScripts. This allows us to load in outside JavaScript files, just like we would do in HTML, with the script tag. To do this, we will need to modify the JSON file to point the object to a variable and rename it to a JavaScript file:
let cache = [{"id":1,"full_name":"Binky Bibey","email":"bbibey0@furl.net","phone":"370-576-9587","zipcode":"640069"}, //rest of the data];
// SharedWorker.js
importScripts('./mock_customer_data.js');
- Now that we have brought the cache of data in, we will respond to messages sent from the ports. We will expect only arrays of numbers. These will correspond to the ID that is associated with a user. For now, we will loop through all of the items in our dictionary to see whether we have them. If we do, we will add them to an array that we will respond with:
const handleReq = function(arr) {
const res = new Array(arr.length)
for(let i = 0; i < arr.length; i++) {
const num = arr[i];
for(let j = 0; j < cache.length; j++) {
if( num === cache[j].id ) {
res[i] = cache[j];
break;
}
}
}
return res;
}
onconnect = function(e) {
let port = e.ports[0];
port.onmessage = function(e) {
const request = e.data;
if( Array.isArray(request) ) {
const response = handleReq(request);
port.postMessage(response);
}
}
}
- With this, we will need to add the corresponding code inside of our HTML file. We will add a button that is going to send 100 random IDs to our SharedWorker. This will simulate when we make a request and get back the IDs associated with the data. The simulation function looks like this:
// developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/
// Global_Objects/Math/random
const getRandomIntInclusive = function(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
const simulateRequest = function() {
const MAX_BUY_SELL = 1000000;
const MIN_BUY_SELL = -1000000;
const ids = [];
const createdIds = [];
for(let i = 0; i < 100; i++) {
const id = getRandomIntInclusive(1, 1000);
if(!createdIds.includes(id)) {
const obj = {
id,
buy : getRandomIntInclusive(MIN_BUY_SELL,
MAX_BUY_SELL),
sell : getRandomIntInclusive(MIN_BUY_SELL,
MAX_BUY_SELL)
};
ids.push(obj);
}
}
return ids;
}
- With the preceding simulation, we can now add in our input for a request and then send that to our SharedWorker:
requestButton.addEventListener('click', (ev) => {
const res = simulateRequest();
worker.port.postMessage(res);
});
- Now, we are currently posting the wrong data to our SharedWorker. We only want to post the IDs, but how are we going to tie our request to the responses from our SharedWorker? We will need to slightly modify the structure that we have for our request and response methods. We will now tie an ID to our message so we can have the SharedWorker post that back to us. This way, we can have a map on the frontend of requests and the IDs associated with them. Make the following changes:
// HTML file
const requestMap = new Map();
let reqCounter = 0;
requestButton.addEventListener('click', (ev) => {
const res = simulateRequest();
const reqId = reqCounter;
reqCounter += 1;
worker.port.postMessage({
id : reqId,
data : res
});
});
// Shared worker
port.onmessage = function(e) {
const request = e.data;
if( request.id &&
Array.isArray(request.data) ) {
const response = handleReq(request.data);
port.postMessage({
id : request.id,
data : response
});
}
}
- With these changes, we still need to make sure we only pass the IDs to the SharedWorker. We can pull these off from the request before we send it:
requestButton.addEventListener('click', (ev) => {
const res = simulateRequest();
const reqId = reqCounter;
reqCounter += 1;
requestMap.set(reqId, res);
const attribute = [];
for(let i = 0; i < res.length; i++) {
attribute.push(res[i].id);
}
worker.port.postMessage({
id : reqId,
data : attribute
});
});
- Now we need to handle the data coming back to us inside of our HTML file. First, we attach an onmessage handler to the port:
worker.port.onmessage = function(ev) {
console.log('data', ev.data);
}
- Finally, we grab the associated buy/sell order from our map and populate it with the returned cache data. Once we have done this, we just have to clone our row template and fill in the corresponding fields:
worker.port.onmessage = function(ev) {
const data = ev.data;
const baseData = requestMap.get(data.id);
requestMap.delete(data.id);
const attribution = data.data;
tableBody.innerHTML = '';
for(let i = 0; i < baseData.length; i++) {
const _d = baseData[i];
for(let j = 0; j < attribution.length; j++) {
if( _d.id === attribution[j].id ) {
const final = {..._d, ...attribution[j]};
const newRow = rowTemplate.content.cloneNode(true);
newRow.querySelector('.name').innerText =
final.full_name;
newRow.querySelector('.zip').innerText =
final.zipcode;
newRow.querySelector('.phone').innerText =
final.phone;
newRow.querySelector('.email').innerText =
final.email;
newRow.querySelector('.buy').innerText =
final.buy;
newRow.querySelector('.sell').innerText =
final.sell;
tableBody.appendChild(newRow);
}
}
}
}
With the preceding example, we have created a shared cache that any page that has the same domain can use. While there are certain optimizations (we could store the data as a map and have the ID as the key), we are still going to run a bit faster than having to potentially wait on a database connection (especially when we are in places that have limited bandwidth).