Last active
February 7, 2024 12:41
-
-
Save sicet7/818259bdf317709d55874b30ffce90cc to your computer and use it in GitHub Desktop.
[POC] Using SharedWorker in vanilla javascript to synchronize data between tabs/windows when polling an expensive computation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>test</title> | |
</head> | |
<body> | |
<p id="data-output">Nothing</p> | |
<script type="text/javascript"> | |
const DomUpdateWorker = { | |
_pollingInterval: 1000, | |
_sharedWorker: new SharedWorker('./worker.js'), | |
_connectionId: null, | |
_updateCallback: null, | |
_lastTriggeringMessageId: null, | |
_intervalPointer: null, | |
/** | |
* @callback updateCallback | |
* @param {any} data | |
*/ | |
/** | |
* @param cb {updateCallback} | |
*/ | |
init: function (cb) { | |
this._updateCallback = cb; | |
this._sharedWorker.port.onmessage = this._onResponse.bind(this); | |
this._sharedWorker.port.start(); | |
this.startPolling(); | |
}, | |
startPolling: function () { | |
this.stopPolling(); | |
this._intervalPointer = setInterval( | |
this._intervalTrigger.bind(this), | |
this._pollingInterval | |
); | |
}, | |
stopPolling: function () { | |
if (typeof this._intervalPointer === 'number') { | |
clearInterval(this._intervalPointer); | |
this._intervalPointer = null; | |
} | |
}, | |
/** | |
* @return {string} | |
* @private | |
*/ | |
_genId: function () { | |
try { | |
return crypto.randomUUID(); | |
} catch (e) { | |
return "10000000-1000-4000-8000-100000000000".replace( | |
/[018]/g, | |
c => | |
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) | |
); | |
} | |
}, | |
_onResponse: function (e) { | |
switch (e?.data?.type) { | |
case 'connection': | |
if (typeof e?.data?.id === 'string') { | |
this._connectionId = e?.data?.id; | |
} | |
break; | |
case 'data': | |
const data = e?.data?.data; | |
const dataId = e?.data?.id; | |
if (typeof this._updateCallback === 'function') { | |
if (typeof dataId === 'string') { | |
if (dataId === this._lastTriggeringMessageId) { | |
return; | |
} | |
this._lastTriggeringMessageId = dataId; | |
} | |
this._updateCallback(data); | |
} | |
break; | |
default: | |
console.warn('Invalid response', e) | |
break; | |
} | |
}, | |
/** | |
* @param type {string} | |
* @param data {any|null} | |
* @private | |
*/ | |
_sendRequest: function (type, data = null) { | |
this._sharedWorker.port.postMessage({ | |
type: type, | |
connectionId: this._connectionId, | |
messageId: this._genId(), | |
data: data, | |
}) | |
}, | |
_intervalTrigger: function () { | |
this._sendRequest('data') | |
}, | |
}; | |
DomUpdateWorker.init((data) => { | |
console.log(data); | |
document.getElementById('data-output').innerHTML = data; | |
}); | |
</script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const _sharedWorker = { | |
expensiveCalculation: async function () { | |
//TODO: Implement expensive calculation/request | |
//The code below is just a placeholder sleeps for 2 secs and then returns a uuid. | |
await (new Promise(resolve => setTimeout(resolve, 2000))); | |
try { | |
return crypto.randomUUID(); | |
} catch (e) { | |
return "10000000-1000-4000-8000-100000000000".replace( | |
/[018]/g, | |
c => | |
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) | |
); | |
} | |
}, | |
_intervalTime: 5000, | |
/** | |
* @return {string} | |
*/ | |
_idGen: () => { | |
try { | |
return crypto.randomUUID(); | |
} catch (e) { | |
return "10000000-1000-4000-8000-100000000000".replace( | |
/[018]/g, | |
c => | |
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) | |
); | |
} | |
}, | |
_onConnect: async function (ev) { | |
//get the port that is connecting. | |
let port = ev.ports[0]; | |
//Assign message handler for the connecting port. | |
port.onmessage = this._onRequest.bind(this) | |
//Start the port. | |
port.start(); | |
//Generate a identifier for the connection. | |
const uuid = this._idGen(); | |
//Save the connection under the identifier. | |
this._ports[uuid] = port; | |
//Let the client know what its identifier is. | |
this._sendResponse(uuid, { type: 'connection', id: uuid }) | |
}, | |
_onRequest: async function (e) { | |
//Make sure the request we are getting has a valid connectionId and a messageId | |
if (!this._isValidRequest(e)) { | |
return; | |
} | |
//Identifiers from the request. | |
const type = e?.data?.type; | |
const messageId = e?.data?.messageId; | |
const connectionId = e?.data?.connectionId; | |
switch (type) { | |
case 'data': | |
//Make a date in the past to check if the data we have is too old. | |
const inThePast = new Date(Date.now() - this._intervalTime); | |
//if the data we have is to old. | |
if (!(this._data?.timestamp instanceof Date) || this._data?.timestamp < inThePast) { | |
// If a request already has triggered the expensive calculation, and it is not this request, then just return. | |
if (this._calculatingFlag !== null && this._calculatingFlag !== messageId) { | |
return; | |
} | |
//Lock the computation so that other requests cant do it at the same time. | |
this._calculatingFlag = messageId; | |
//perform the expensive calculation and assign data to worker state.. | |
const dataFromCalc = await this.expensiveCalculation(); | |
this._data = { | |
id: this._idGen(), | |
timestamp: new Date(Date.now()), | |
type: 'data', | |
data: dataFromCalc | |
}; | |
//Open for new expensive calculations. | |
this._calculatingFlag = null; | |
} | |
//Send the data we have to the requester. | |
this._sendResponse(connectionId, this._data) | |
break; | |
} | |
}, | |
/** | |
* @param connectionId {string} | |
* @param data {any} | |
* @private | |
*/ | |
_sendResponse: function (connectionId, data) { | |
if (!Object.keys(this._ports).includes(connectionId)) { | |
return; | |
} | |
this._ports[connectionId].postMessage(data); | |
}, | |
/** | |
* @param request {any} | |
* @return {boolean} | |
* @private | |
*/ | |
_isValidRequest: function (request) { | |
return typeof request?.data?.connectionId === 'string' && | |
Object.keys(this._ports).includes(request?.data?.connectionId) && | |
typeof request?.data?.messageId === 'string' && | |
typeof request?.data?.type === 'string' | |
}, | |
//internal state for the worker, should not be modified. | |
_ports: {}, | |
_data: {}, | |
_calculatingFlag: null, | |
}; | |
onconnect = _sharedWorker._onConnect.bind(_sharedWorker) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment