Skip to content

Instantly share code, notes, and snippets.

@sicet7
Last active February 7, 2024 12:41
Show Gist options
  • Save sicet7/818259bdf317709d55874b30ffce90cc to your computer and use it in GitHub Desktop.
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
<!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>
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