Faster API calls using WebSockets without having to subscribe to updates
The goal is simple to manage, fast, and concurrent api calls, for when subscriptions aren’t necessary and are overhead.
Here’s the code, explanation to follow:
client:
class DataStreamPromisifier {
constructor() {
this.pending = {}
}
getPromise(id) {
return new Promise((resolve, reject) => {
this.pending[id] = data => data.error ? reject(data.error) : resolve(data.responses)
})
}
pushData(id, data) {
if (typeof this.pending[id] !== "function") {
console.log("nothing to do for " + id)
return
}
this.pending[id](data)
delete this.pending[id]
}
}
const dataStreamPromisifier = new DataStreamPromisifier()
const uuidv4Regex = //unimportant//primus is our WebSocket connector
primus.on("data", data => {
if (!uuidv4Regex.test(data.requestId)) return
dataStreamPromisifier.pushData(data.requestId, data)
});//the parameter schema of this function can be domain specific
const getRemoteData = async (type, payload) => {
let requestId = uuidv4()
let dataPromise = dataStreamPromisifier.getPromise(requestId)
primus.write({
requestId: requestId,
type: type,
payload: payload
});
return await dataPromise
}
server:
const primus = Primus.createServer(function connection(spark) {
spark.on("data", async data => {
if (data.requestId === undefined) return
if (data.type === "datarequest") {
const response = await processPayload(data.payload)
spark.write({requestId: requestId, responses: response})
} else {
spark.write({requestId: requestId, error: {code: 500, message: "an error occurred"}})
}
})
}, {port: 18080, transformer: 'engine.io'});
So the effect is an async function `getRemoteData()` that’s gets data using async-await, while all calls to that function go over the same websocket.
This is faster because it uses websockets and doesn’t have the overhead of a full HTTP request for every call.
And when there’s no need for a subscription setup such as when the data is only necessary on demand, this avoids subscriptions and the code is more readable (it flows).
So, what’s going on here?
We want to call a function and have it return when it gets its response from the server.
First we’ll need to be able to await
a response from a server. For that, we have class DataStreamPromisifier()
which has a getPromise()
method that gives promises and resolve()
‘s them when data identified by requestId
comes back from the server. Once you have a Promise
you can await
it.
DataStreamPromisifier
gets fed all data directly from the websocket.
So our function is as simple as 1, 2, 3:
- Generate a
requestId
- Get a promise for that
requestId
from the Data Stream Promisifier - Send a payload with the
requestId
to the websocket data stream - whoops, forgot about this one, return a promise to the data.
On the server it’s just about making sure we respond with the requestId
attached.
return Promise.resolve(“The End”)
Footnote: The Promisifier could be improved to include a max TTL on the request using Promise.race() with timeout promise.