I'm doing an online auction. Accordingly, a timer is needed that, after the expiration of time, will change the status of the item to inactive. With the help of a web worker, I'm managed to do this. I check the end date (which I take from the firestore) with the current one and if they match, then I change the status. But unfortunately, it only works by pressing a button on the page that launches the webworker. If I change, for example, to onload, then ngOnInit does not have time to receive data from the firestore and, accordingly, the webworker is not executed. Is there a way that can help solve this problem?
I know that firebase has such a thing named 'function', but unfortunately it is paid.
component.ts
ngOnInit(){
this.initializeWorker();
}
timeWorker(){
if(this.products.status==='active'){
this.worker.postMessage(this.products.closureDate);
this.worker.addEventListener('message', ({data})=>{
this.checkStatus(data);
})
}
}
initializeWorker(){
if (typeof Worker !== 'undefined') {
// Create a new
this.worker = new Worker(new URL('./random.worker', import.meta.url));
this.worker.postMessage('');
} else {
// Web workers are not supported in this environment.
// You should add a fallback so that your program still executes correctly.
}
}
worker.ts
addEventListener('message', ({ data }) => {
console.log("data:" + data);
const response = endUpAuc(data);
postMessage(response);
});
function endUpAuc(ps) {
console.log("ps: "+ps);
var cDate = new Date(ps);
console.log("cdate: "+cDate)
if(cDate.getTime()>new Date().getTime())
{
console.log(cDate.getTime());
console.log(new Date().getTime());
console.log("active");
return "active";
}
else if(cDate.getTime()<=new Date().getTime()){
console.log("inactive");
//this.updateStatus("inactive");
return "inactive";
//this.router.navigate(['/results'])
}
}
component.html
<div class="about" onload="timeWorker()">
...
</div>
Related
Trying to add GA's client ID to Amplitude's User property. Once the page is loaded, an visit page event is triggered. As used GTM script is still not loaded, a new client (visitor) won't have assigned the client ID. This is the problem I'm trying to fix.
My initial "solution" is based on storing the tracking events to a queue till client ID is received. The second part is "handshaking" - the app is puling until the client ID is loaded or a timeout is happened.
The "brutal force" implementation of handshake between GA and Amp didn't work. GTM loader is modified as (j.onload is added):
`a(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.onload=function() {window.gaLoaded?.()};...`
so once the script is loaded, a global fn gaLoaded is called.
The pulling function is:
window.gaLoaded = () => {
let count = 50 // 5 sec and we will go wo GA client id
clearTimeout(timerId)
function setGaClientId() {
count -= 1
if (count < 0) {
isWaitingForClientId = false
flushTrackQueue()
return
}
// wait till gtm is initialized - ga function will be injected to window object
if (typeof window.ga === 'function') {
// https://developers.google.com/analytics/devguides/collection/analyticsjs/cookies-user-id#getClientId
const clientId = window.ga?.getAll?.()[0].get(GTM_CLIENT_ID)
if (!clientId) {
setTimeout(setGaClientId, 100)
return
}
const identifyObj = new Identify()
identifyObj.set('GAclientId', clientId)
identify(identifyObj)
isWaitingForClientId = false
flushTrackQueue()
} else {
setTimeout(setGaClientId, 100)
}
}
if (isWaitingForClientId) {
setGaClientId()
}
}
window.gaLoaded(); // GA might be loaded before this script is executed so check for the GA's client id
Please could you share your experience on this topic?
I'm working on an Angular 8 (with Electron 6 and Ionic 4) project and right now we are having evaluation phase where we are deciding whether to replace polling with SSE (Server-sent events) or Web Sockets. My part of the job is to research SSE.
I created small express application which generates random numbers and it all works fine. The only thing that bugs me is correct way to reconnect on server error.
My implementation looks like this:
private createSseSource(): Observable<MessageEvent> {
return Observable.create(observer => {
this.eventSource = new EventSource(SSE_URL);
this.eventSource.onmessage = (event) => {
this.zone.run(() => observer.next(event));
};
this.eventSource.onopen = (event) => {
console.log('connection open');
};
this.eventSource.onerror = (error) => {
console.log('looks like the best thing to do is to do nothing');
// this.zone.run(() => observer.error(error));
// this.closeSseConnection();
// this.reconnectOnError();
};
});
}
I tried to implement reconnectOnError() function following this answer, but I just wasn't able to make it work. Then I ditched the reconnectOnError() function and it seems like it's a better thing to do. Do not try to close and reconnect nor propagate error to observable. Just sit and wait and when the server is running again it will reconnect automatically.
Question is, is this really the best thing to do? Important thing to mention is, that the FE application communicates with it's own server which can't be accessed by another instance of the app (built-in device).
I see that my question is getting some attention so I decided to post my solution. To answer my question: "Is this really the best thing to do, to omit reconnect function?" I don't know :). But this solution works for me and it was proven in production, that it offers way how to actually control SSE reconnect to some extent.
Here's what I did:
Rewritten createSseSource function so the return type is void
Instead of returning observable, data from SSE are fed to subjects/NgRx actions
Added public openSseChannel and private reconnectOnError functions for better control
Added private function processSseEvent to handle custom message types
Since I'm using NgRx on this project every SSE message dispatches corresponding action, but this can be replaced by ReplaySubject and exposed as observable.
// Public function, initializes connection, returns true if successful
openSseChannel(): boolean {
this.createSseEventSource();
return !!this.eventSource;
}
// Creates SSE event source, handles SSE events
protected createSseEventSource(): void {
// Close event source if current instance of SSE service has some
if (this.eventSource) {
this.closeSseConnection();
this.eventSource = null;
}
// Open new channel, create new EventSource
this.eventSource = new EventSource(this.sseChannelUrl);
// Process default event
this.eventSource.onmessage = (event: MessageEvent) => {
this.zone.run(() => this.processSseEvent(event));
};
// Add custom events
Object.keys(SSE_EVENTS).forEach(key => {
this.eventSource.addEventListener(SSE_EVENTS[key], event => {
this.zone.run(() => this.processSseEvent(event));
});
});
// Process connection opened
this.eventSource.onopen = () => {
this.reconnectFrequencySec = 1;
};
// Process error
this.eventSource.onerror = (error: any) => {
this.reconnectOnError();
};
}
// Processes custom event types
private processSseEvent(sseEvent: MessageEvent): void {
const parsed = sseEvent.data ? JSON.parse(sseEvent.data) : {};
switch (sseEvent.type) {
case SSE_EVENTS.STATUS: {
this.store.dispatch(StatusActions.setStatus({ status: parsed }));
// or
// this.someReplaySubject.next(parsed);
break;
}
// Add others if neccessary
default: {
console.error('Unknown event:', sseEvent.type);
break;
}
}
}
// Handles reconnect attempts when the connection fails for some reason.
// const SSE_RECONNECT_UPPER_LIMIT = 64;
private reconnectOnError(): void {
const self = this;
this.closeSseConnection();
clearTimeout(this.reconnectTimeout);
this.reconnectTimeout = setTimeout(() => {
self.openSseChannel();
self.reconnectFrequencySec *= 2;
if (self.reconnectFrequencySec >= SSE_RECONNECT_UPPER_LIMIT) {
self.reconnectFrequencySec = SSE_RECONNECT_UPPER_LIMIT;
}
}, this.reconnectFrequencySec * 1000);
}
Since the SSE events are fed to subject/actions it doesn't matter if the connection is lost since at least last event is preserved within subject or store. Attempts to reconnect can then happen silently and when new data are send, there are processed seamlessly.
I want to use web worker to handle my zipcode checker function, I haven't worked with web worker before so the concept is new to me
This is my zipcode function
``
function checkZipCode() {
event.preventDefault();
if(document.getElementById('zipcode').value < 20000) {
document.getElementById('zip-result').innerHTML = 'Sorry, we haven’t expanded to that area yet';
} else if (document.getElementById('zipcode').value >= 20000) {
document.getElementById('zip-result').innerHTML = 'We’ve got your area covered!'
} else {
return null
}
};
As per the docs workers are pretty easy to spin up:
//in a JS file
const myWorker = new Worker('./myWorker.js');//worker requested and top-level scope code executed
myWorker.postMessage('hello');
myWorker.addEventListener('message', e => {
//e.data will hold data sent from worker
const message = e.data;
console.log(message); // HELLO
//if it's just a one-time thing, you can kill the worker
myWorker.terminate();
}
myWorker.addEventListener('error', e => {//worker might throw an error
e.preventDefault();
console.log(e.message, `on line ${e.lineno}`);
});
//myWorker.js
//run whatever you need, just no DOM stuff, no window etc
console.log('this line runs when worker loads');
addEventListener('message', (e) => {
postMessage(e.data.toUpperCase());//up-case message and send it right back
});
I have a service worker in sw.js, it uses a template engine to get the commit numbre as a version number. I set the cache name like this:
var version = {{ commit_hash }};
self.cacheName = `cache-` + version;
I have some scripts being added to the cache on the worker's install, but there are scripts that are dynamically loaded on the page. I would like to load all the scripts/css on the first load without forcing the user to wait for the app to install first.
I can get all the content on the page with the following code in the bottom of index.html:
var toCache = ['/'];
var css = document.getElementsByTagName("link");
for(el of css) {
var href = el.getAttribute("href");
if(href) {
toCache.push(href);
}
}
var js = document.getElementsByTagName("script");
for(el of js) {
var src = el.getAttribute("src");
if(src) {
toCache.push(src);
}
}
That works fine, now I would just need to open the correct cache, fetch files that aren't already present, and store them. Something like:
toCache.forEach(function(url) {
caches.match(url).then(function(result) {
if(!result) {
fetch(url).then(function(response) {
caches.open(cacheName).then(cache => {
cache.put(url, response)
});
});
}
});
});
Is there a way to get the cacheName from the service worker inside a script tag in a different file?
And yes, I know that I could simplify this greatly by doing the check in the for/of loops. I broke it apart so it would be easier to describe.
No.
JavaScript executing in the window context cannot access SW's context and vice versa. You have to implement a workaround of some sort.
Remember that you can use postMessage to communicate between the two.
Using this blog I was able to pass messages from the service worker and back. First, I added the following function at the top of sw.js:
function clientPostMessage(client, message){
return new Promise(function(resolve, reject){
var channel = new MessageChannel();
channel.port1.onmessage = function(event){
if(event.data.error){
reject(event.data.error);
}
else {
resolve(event.data);
}
};
client.postMessage(message, [channel.port2]);
});
}
This allows my service worker to post a message to the window, and then do a callback with a promise.
Then, in my index.html file I added the following to a script tag:
navigator.serviceWorker.addEventListener('message', event => {
switch(event.data) {
case "addAll":
var toCache = [];
var css = document.getElementsByTagName("link");
for(el of css) {
var href = el.getAttribute("href");
if(href) {
toCache.push(href);
}
}
var js = document.getElementsByTagName("script");
for(el of js) {
var src = el.getAttribute("src");
if(src) {
toCache.push(src);
}
}
event.ports[0].postMessage(toCache);
break;
default:
console.log(event.data);
}
});
This listens to any service workers asking for messages, and if it is a "addAll" message, it will get all the scripts and linked content on the page and return an array of the scripts.
Finally, I added the following to my activate event listener function in sw.js:
// Get all the clients, and for each post a message
clients.matchAll().then(clients => {
clients.forEach(client => {
// Post "addAll" to get a list of files to cache
clientPostMessage(client, "addAll").then(message => {
// For each file, check if it already exists in the cache
message.forEach(url => {
caches.match(url).then(result => {
// If there's nothing in the cache, fetch the file and cache it
if(!result) {
fetch(url).then(response => {
caches.open(cacheName).then(cache => {
cache.put(url, response);
});
});
}
})
});
});
})
});
For all clients the service worker sends an "addAll" message to the page and gets the result. For each item in the result, it checks if the value is already in the cache and if not, fetches and adds it.
With this method, the install listener of the service worker only needs to contain:
self.addEventListener('install', event => {
if(self.skipWaiting) {
self.skipWaiting();
}
event.waitUntil(
caches.open(cacheName).then(cache => {
return cache.addAll([
'/',
'/index.html',
])
})
);
});
It seems to be working well so far, if anyone has any suggestions or sees any errors I'd be happy to hear! You can also tell me how improper this is, but it makes my life a lot easier for adding service workers for pre-existing projects that rely on scripts that aren't bundled together.
I gotta a companion script for a serviceworker and I'm trialling right now.
The script works like so:
((n, d) => {
if (!(n.serviceWorker && (typeof Cache !== 'undefined' && Cache.prototype.addAll))) return;
n.serviceWorker.register('/serviceworker.js', { scope: './book/' })
.then(function(reg) {
if (!n.serviceWorker.controller) return;
reg.onupdatefound = () => {
let installingWorker = reg.installing;
installingWorker.onstatechange = () => {
switch (installingWorker.state) {
case 'installed':
if (navigator.serviceWorker.controller) {
updateReady(reg.waiting);
} else {
// This is the initial serviceworker…
console.log('May be skipwaiting here?');
}
break;
case 'waiting':
updateReady(reg.waiting);
break;
case 'redundant':
// Something went wrong?
console.log('[Companion] new SW could not install…')
break;
}
};
};
}).catch((err) => {
//console.log('[Companion] Something went wrong…', err);
});
function updateReady(worker) {
d.getElementById('swNotifier').classList.remove('hidden');
λ('refreshServiceWorkerButton').on('click', function(event) {
event.preventDefault();
worker.postMessage({ 'refreshServiceWorker': true } );
});
λ('cancelRefresh').on('click', function(event) {
event.preventDefault();
d.getElementById('swNotifier').classList.add('hidden');
});
}
function λ(selector) {
let self = {};
self.selector = selector;
self.element = d.getElementById(self.selector);
self.on = function(type, callback) {
self.element['on' + type] = callback;
};
return self;
}
let refreshing;
n.serviceWorker.addEventListener('controllerchange', function() {
if (refreshing) return;
window.location.reload();
refreshing = true;
});
})(navigator, document);
I'm a bit overwhelmed right now by the enormity of the service workers api and unable to "see" what one would do with reg.installing returning a redundant state?
Apologies if this seems like a dumb question but I'm new to serviceworkers.
It's kinda difficult to work out what your intent is here so I'll try and answer the question generally.
A service worker will become redundant if it fails to install or if it's superseded by a newer service worker.
What you do when this happens is up to you. What do you want to do in these cases?
Based on the definition here https://www.w3.org/TR/service-workers/#service-worker-state-attribute I am guessing just print a log in case it comes up in debugging otherwise do nothing.
You should remove any UI prompts you created that ask the user to do something in order to activate the latest service worker. And be patient a little longer.
You have 3 service workers, as you can see on the registration:
active: the one that is running
waiting: the one that was downloaded, and is ready to become active
installing: the one that we just found, being downloaded, after which it becomes waiting
When a service worker reaches #2, you may display a prompt to the user about the new version of the app being just a click away. Let's say they don't act on it.
Then you publish a new version. Your app detects the new version, and starts to download it. At this point, you have 3 service workers. The one at #2 changes to redundant. The one at #3 is not ready yet. You should remove that prompt.
Once #3 is downloaded, it takes the place of #2, and you can show that prompt again.
Write catch function to see the error. It could be SSL issue.
/* In main.js */
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./sw.js')
.then(function(registration) {
console.log("Service Worker Registered", registration);
})
.catch(function(err) {
console.log("Service Worker Failed to Register", err);
})
}