Service worker & client communication. Fetch Event finished - javascript

I am trying to communicate a Service worker with a client in a bidirectional way. My aim is to delegate the processing of the request to the client and come back to the service worker with a proper response. The sequence of steps are the following:
Service Worker:
The Service worker gets a request
It checks if it must be ignored or properly managed
A message with the request and a correlation ID is created
The message is sent to the client via the Incoming message
The pair {id: event} is registered in Messages store
Client:
A request message comes via the Incoming Channel
The message is destructured to get the correlation ID & the request
A proper response is created for the request
The response & correlation ID is sent back to worker via the Outgoing Channel
Service Worker:
A response message comes via the Outgoing Channel
The message is destructured to get the correlation ID & the response
The pending message for this ID is retrieved from the Messages store
A new Response object is created for the response
The response object is returned via respondWith
This is my Service Worker:
const INSTALL = 'install'
const ACTIVE = 'activate'
const FETCH = 'fetch'
const MESSAGE = 'message'
const INSTALLED = 'Worker Installed'
const ACTIVATED = 'Worker Activated'
const FETCHING = 'Worker fetching'
const ICHANNEL = 'whale-ichannel'
const OCHANNEL = 'whale-ochannel'
const HEADERS = { 'Content-Type' : 'text/javascript' }
const PREFIX = '/whale/'
let toJson = JSON.stringify
let toJs = JSON.parse
let Messages = new Map ()
let idx = 0
let iChannel = new BroadcastChannel (ICHANNEL) // Incoming Channel
let oChannel = new BroadcastChannel (OCHANNEL) // Outgoing Channel
self.addEventListener (INSTALL, function (event) {
console.log (INSTALLED)
self.skipWaiting ()
})
self.addEventListener (ACTIVE, function (event) {
console.log (ACTIVATED)
event.waitUntil (clients.claim ())
oChannel.addEventListener (MESSAGE, function ({ data }) {
let id = data.id
let response = data.response
let message = Messages.get (id)
Messages.delete (id)
message.send (response)
})
})
self.addEventListener (FETCH, async function (event) {
if (isRequest (event)) await doRequest (event)
})
function isRequest (event) {
let { request } = event
let { url } = request
let uri = new URL (url)
let path = uri.pathname
let ok = path.startsWith (PREFIX)
return ok
}
function getRequest (event) {
let { request } = event
let { url } = request
let { referrer } = request
let headers = toJs (toJson (request)) || {}
return {
url,
referrer,
headers
}
}
function getResponse (data) {
let headers = HEADERS
let text = data
let response = new Response (text, { headers })
return response
}
function doRequest (event) {
let request = getRequest (event)
let {id, wait} = getMessage (event)
iChannel.postMessage ({ id, request })
return wait
}
function getMessage (event) {
let signal
let wait = new Promise (function (ok) { signal = ok })
let id = idx++
Messages.set (id, {
send : function (data) {
let response = getResponse (data)
event.respondWith (response) // [1]
signal (data)
}
})
return { id, wait }
}
This is my Client:
const ICHANNEL = 'whale-ichannel' // Incoming Channel
const OCHANNEL = 'whale-ochannel' // Outgoing Channel
// Register worker ...
let iChannel = new BroadcastChannel (ICHANNEL)
let oChannel = new BroadcastChannel (OCHANNEL)
iChannel.addEventListener (MESSAGE, async function ({ data }) {
let { id } = data
let { request } = data
let response = MyFancyResponse (...)
oChannel.postMessage ({
id,
response
})
})
When I run this code the following message error is emitted. See [1] in the Service Worker code. Please notice the entire communication flow is correctly executed.
Uncaught DOMException: Failed to execute 'respondWith' on 'FetchEvent': The event handler is already finished.
If I do not misunderstand, that means when the worker is abandoned to find the client-side the fetch event is finished. So my approach is not valid. My question is: how can be done this kind of two-way communication where the request-response process is split via message events?

I think, the problem is, that the onFetch event needs immediate response with
event.respondWith()
If the onFetch doesn't get an event.respondWith, it generates one, and You cannot pass Your response later, and You'll get this error: The event handler is already finished.
So, instead of
self.addEventListener (FETCH, async function (event) {
if (isRequest (event)) await doRequest (event)
})
I'd try
self.addEventListener (FETCH, function (event) {
event.respondWith( ( async function() {
if (isRequest (event)) return await doRequest (event); // note the RETURN
}) () ); // note the (), what will call the inline async function to get a promise
})
and change the inner event.respondWith(xxx) to return xxx;
Please tell, if this solves the problem.

Related

How to trigger an API call when a message is sent to Azure service bus topic?

I am working on Azure service bus topic. Following the documentation, created a sender and reciever code.
This is the sender code i am having,
const { ServiceBusClient } = require("#azure/service-bus");
const connectionString = "<SERVICE BUS NAMESPACE CONNECTION STRING>"
const topicName = "<TOPIC NAME>";
const messages = [
{ body: "Albert Einstein" },
{ body: "Werner Heisenberg" },
{ body: "Marie Curie" },
{ body: "Steven Hawking" },
{ body: "Isaac Newton" },
{ body: "Niels Bohr" },
{ body: "Michael Faraday" },
{ body: "Galileo Galilei" },
{ body: "Johannes Kepler" },
{ body: "Nikolaus Kopernikus" }
];
async function main() {
// create a Service Bus client using the connection string to the Service Bus namespace
const sbClient = new ServiceBusClient(connectionString);
// createSender() can also be used to create a sender for a queue.
const sender = sbClient.createSender(topicName);
try {
// Tries to send all messages in a single batch.
// Will fail if the messages cannot fit in a batch.
// await sender.sendMessages(messages);
// create a batch object
let batch = await sender.createMessageBatch();
for (let i = 0; i < messages.length; i++) {
// for each message in the arry
// try to add the message to the batch
if (!batch.tryAddMessage(messages[i])) {
// if it fails to add the message to the current batch
// send the current batch as it is full
await sender.sendMessages(batch);
// then, create a new batch
batch = await sender.createMessageBatch();
// now, add the message failed to be added to the previous batch to this batch
if (!batch.tryAddMessage(messages[i])) {
// if it still can't be added to the batch, the message is probably too big to fit in a batch
throw new Error("Message too big to fit in a batch");
}
}
}
// Send the last created batch of messages to the topic
await sender.sendMessages(batch);
console.log(`Sent a batch of messages to the topic: ${topicName}`);
// Close the sender
await sender.close();
} finally {
await sbClient.close();
}
}
// call the main function
main().catch((err) => {
console.log("Error occurred: ", err);
process.exit(1);
});
This code is working fine, but instead of sending a batch of dummy data to the service bus topic i want to implement my use case here.
My use case is I will be using this sender code in a react front end application, where there is a node API call happening at the end of a form submission. So at the end of form submission, i will send that unique form ID to the topic and i need to somehow trigger the api call for that form id.
I am unable to connect the dots. How to do this?
Added reciever side code.
const { delay, ServiceBusClient, ServiceBusMessage } = require("#azure/service-bus");
const axios = require("axios").default;
const connectionString = "<ConnectionString>"
const topicName = "<TopicName>";
const subscriptionName = "<Subscription>";
async function main() {
// create a Service Bus client using the connection string to the Service Bus namespace
const sbClient = new ServiceBusClient(connectionString);
// createReceiver() can also be used to create a receiver for a queue.
const receiver = sbClient.createReceiver(topicName, subscriptionName);
// function to handle messages
const myMessageHandler = async (messageReceived) => {
console.log(`Received message: ${messageReceived.body}`);
const response = axios({
method: 'post',
url: 'http://localhost:8080/gitWrite?userprojectid=63874e2e3981e40a6f4e04a7',
});
console.log(response);
};
// function to handle any errors
const myErrorHandler = async (error) => {
console.log(error);
};
// subscribe and specify the message and error handlers
receiver.subscribe({
processMessage: myMessageHandler,
processError: myErrorHandler
});
// Waiting long enough before closing the sender to send messages
await delay(5000);
await receiver.close();
await sbClient.close();
}
// call the main function
main().catch((err) => {
console.log("Error occurred: ", err);
process.exit(1);
});
While messages are published to a topic, they are recieved by subscriptions under the topic. You'll need to define one or more subscriptions to receive the messages. That's on the broker. For your code, you'll need a receiving code on the server-side/backend. Could be something like a node.js service or Azure Function. But a code that would receive from the subscription(s).
I would review the idea of publishing messages from the client side directly to Azure Service Bus. If the code is a React front end application, make sure the connection string is not embedded in resources or can be revealed.

Unit test a custom authorizer lambda

I have a customized authorizer lambda which takes an event from a API Gateway and calls another internal api which validates if a user has access to a particular resource.
This API takes in a jwttoken and a id and returns back response.
I have been trying to write unit test for this lambda. I am lost as to how can I mock data - specifically the token and id.
Here is what I wrote (Jest)
describe('lambdaService', () => {
beforeEach(() => {
jest.restoreAllMocks();
});
test('should return data', async () => {
const response = { statusCode: 200 };
const event = {
"headers" : {
'Authorization' : 'token'
},
"pathParameters": {
"proxy": "/my-path/{id}"
},
}
const retrieveData = jest.spyOn(authorizeResourceJWTUser, 'doGetRequest').mockResolvedValueOnce(response)
const actualValue = await handler(event);
expect(actualValue).toEqual(response);
});
const {validate} = require('./helper/validateResource')
exports.handler = async (event, context, callback) => {
try {
let id, token, HOST
id = extractId(path)//some function to return id from url
let response = await validate(id, token)
callback(null,generatePolicy(...)
} catch(error) {
callback("Unauthorized")
}
};
function generatePolicy {...};
But it fails giving 401 error since the token and the {id} is not valid.
There is no proper documentation on how can I mock this API call rather than calling the actual service

Electron BrowserWindow cannot get response when debugger is attached

I'm writing an Electron app which creates a BrowserWindow. I want to capture a few requests sent to a server, I also want responses for there requests. Using Electron WebRequest api I can't get responses, so searched the web and found that I can attach a debugger programatically.
I attach debugger using the below code and I get almost all responses correctly. But for one bigger request I can't get the response. I get an error
Error: No resource with given identifier found
If I launch DevTools and navigate to that request I also can't get the response: Failed to load response data. If I comment out below code the response in DevTools show correctly.
Note that this happens only for one specific request which returns about 1MB response. For all other requests I can get the response using getResponseData().
const dbg = win.webContents.debugger
var getResponseData = async (reqId) => {
const res = await dbg.sendCommand("Network.getResponseBody", {requestId: reqId});
return res.body
}
try {
dbg.attach('1.3')
dbg.sendCommand('Network.enable')
} catch (err) {
console.log('Debugger attach failed : ', err)
}
dbg.on('detach', async (event, reason) => {
console.log('Debugger detached due to : ', reason)
})
dbg.on('message', (e, m, p) => {
if (m === 'Network.requestWillBeSent') {
if (p.request.url === someURL) {
const j = JSON.parse(p.request.postData)
console.log("req " + p.requestId)
global.webReqs[p.requestId] = { reqData: j}
}
} else if (m === 'Network.loadingFinished') {
if (p.requestId in global.webReqs) {
console.log("res " + p.requestId)
getResponseData(p.requestId).then(res => {
console.log(res.slice(0,60))
}).catch(err => {
console.error(err)
})
}
}
});
Short update
The event stack for this particular request is as follows, where 13548.212 is simply requestId
Network.requestWillBeSentExtraInfo 13548.212
Network.requestWillBeSent 13548.212
Network.responseReceivedExtraInfo 13548.212
Network.responseReceived 13548.212
Network.dataReceived 13548.212 [repeated 135 times]
...
Network.loadingFinished 13548.212
Looks that I found a solution. It's rather a workaround, but it works. I didn't use Network.getResponseBody. I used Fetch(https://chromedevtools.github.io/devtools-protocol/tot/Fetch).
To use that one need to subscribe for Responses matching a pattern. Then you can react on Fetch.requestPaused events. During that you have direct access to a request and indirect to a response. To get the response call Fetch.getResponseBody with proper requestId. Also remember to send Fetch.continueRequest as
The request is paused until the client responds with one of continueRequest, failRequest or fulfillRequest
https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#event-requestPaused
dbg.sendCommand('Fetch.enable', {
patterns: [
{ urlPattern: interestingURLpattern, requestStage: "Response" }
]})
var getResponseJson = async (requestId) => {
const res = await dbg.sendCommand("Fetch.getResponseBody", {requestId: requestId})
return JSON.parse(res.base64Encoded ? Buffer.from(res.body, 'base64').toString() : res.body)
}
dbg.on('message', (e, m, p) => {
if(m === 'Fetch.requestPaused') {
var reqJson = JSON.parse(p.request.postData)
var resJson = await getResponseJson(p.requestId)
...
await dbg.sendCommand("Fetch.continueRequest", {requestId: p.requestId})
}
});

How to call a function outside of an emitted event call function

I apologize if this is unclear, it's late and I don't know how best to explain it.
I'm using an event emitter to pass data from a server response to a function inside of a separate class in another file, but when trying to use methods in those classes, the this keyword obviously doesn't work (because in this scenario, this refers to the server event emitter) - how would I reference a function within the class itself? I've provided code to help illustrate my point a bit better
ServiceClass.js
class StreamService {
/**
*
* #param {} database
* #param {Collection<Guild>} guilds
*/
constructor (database, guilds,) {
.....
twitchListener.on('live', this.sendLiveAlert) // fire test method when we get a notification
// if there are streamers to monitor, being monitoring
winston.info('Stream service initialized')
}
..............
async get (url, params = null, headers = this.defaultHeaders) {
// check oauth token
const expirationDate = this.token.expires_in || 0
if (expirationDate <= Date.now() || !this.token) await this.getAccessToken()
// build URL
const index = 0
let paramsString = ''
for (const [key, value] of params.entries()) {
if (index === 0) {
paramsString += `?${key}=${value}`
} else {
paramsString += `&${key}=${value}`
}
}
const res = await fetch(url + paramsString, { method: 'GET', headers: headers })
if (!res.ok) {
winston.error(`Error performing GET request to ${url}`)
return null
}
return await res.json()
}
async sendLiveAlert(streamTitle, streamURL, avatar, userName, gameId, viewerCount, thumbnail, startDateTime) {
// get game name first (no headers needed)
const params = new Map()
params.set('id', gameId)
const gameData = await this.get('https://api.twitch.tv/heliix/games', params, this.defaultHeaders)
if(gameData) {
// get webhook and send message to channel
const webhookClient = new WebhookClient('755641606555697305', 'OWZvI01kUUf4AAIR9uv2z4CxRse3Ik8b0LKOluaOYKmhE33h0ypMLT0JJm3laomlZ05o')
const embed = new MessageEmbed()
.setTitle(`${userName} just went live on Twitch!`)
.setURL(streamURL)
.setThumbnail(avatar)
.addFields(
{ name: 'Now Playing', value: gameData.data[0].name },
{ name: 'Stream Title', value: streamTitle }
)
.setImage(thumbnail)
}
webhookClient.send('Webhook test', embed)
}
}
Server.js
class TwitchWebhookListener extends EventEmitter {
......................
// Routes
server
.post((req, res) => {
console.log('Incoming POST request on /webhooks')
............................
const data = req.body.data[0]
if(!this.streamerLiveStatus.get(data.user_id) && data.type === 'live') {
// pass request body to bot for processing
this.emit(
'live',
data.title, // stream title
`https://twitch.tv/${data.user_name}`, // channel link
`https://avatar.glue-bot.xyz/twitch/${data.user_name}`, // streamer avatar
data.user_name,
data.game_id,
data.viewer_count,
data.thumbnail_url,
data.started_at // do we need this?
)
}
break
default:
res.send(`Unknown webhook for ${req.params.id}`)
break
}
} else {
console.log('The Signature did not match')
res.send('Ok')
}
} else {
console.log('It didn\'t seem to be a Twitch Hook')
res.send('Ok')
}
})
}
}
const listener = new TwitchWebhookListener()
listener.listen()
module.exports = listener
Within the sendLiveAlert method, I'm trying to call the get method of the StreamService class - but because it's called directly via the emitter within server.js, this refers specifically to the Server.js class - is there any way I can use StreamService.get()? I could obviously just rewrite the code inside the method itself, but that seems unnecessary when its right there?
Change this:
twitchListener.on('live', this.sendLiveAlert)
to this:
twitchListener.on('live', this.sendLiveAlert.bind(this))
Or, you could also do this:
twitchListener.on('live', (...args) => {
this.sendLiveAlert(...args);
});
With .bind() it creates a function wrapper that resets the proper value of this for you. In the case of the arrow function, it preserves the lexical value of this for you.

Push WebAPI + IndexedDB + ServiceWorker

I've implemented the Push WebAPI in my web application using Service Worker as many articles explain on the web.
Now I need to store some data inside IndexedDB to make them available while the web app is closed (chrome tab closed, service worker in background execution).
In particular I would like to store a simple url from where retrieve the notification data (from server).
Here is my code:
self.addEventListener("push", (event) => {
console.log("[serviceWorker] Push message received", event);
notify({ event: "push" }); // This notifies the push service for handling the notification
var open = indexedDB.open("pushServiceWorkerDb", 1);
open.onsuccess = () => {
var db = open.result;
var tx = db.transaction("urls");
var store = tx.objectStore("urls");
var request = store.get("fetchNotificationDataUrl");
request.onsuccess = (ev) => {
var fetchNotificationDataUrl = request.result;
console.log("[serviceWorker] Fetching notification data from ->", fetchNotificationDataUrl);
if (!(!fetchNotificationDataUrl || fetchNotificationDataUrl.length === 0 || !fetchNotificationDataUrl.trim().length === 0)) {
event.waitUntil(
fetch(fetchNotificationDataUrl, {
credentials: "include"
}).then((response) => {
if (response.status !== 200) {
console.log("[serviceWorker] Looks like there was a problem. Status Code: " + response.status);
throw new Error();
}
return response.json().then((data) => {
if (!data) {
console.error("[serviceWorker] The API returned no data. Showing default notification", data);
//throw new Error();
showDefaultNotification({ url: "/" });
}
var title = data.Title;
var message = data.Message;
var icon = data.Icon;
var tag = data.Tag;
var url = data.Url;
return self.registration.showNotification(title, {
body: message,
icon: icon,
tag: tag,
data: {
url: url
},
requireInteraction: true
});
});
}).catch((err) => {
console.error("[serviceWorker] Unable to retrieve data", err);
var title = "An error occurred";
var message = "We were unable to get the information for this push message";
var icon = "/favicon.ico";
var tag = "notification-error";
return self.registration.showNotification(title, {
body: message,
icon: icon,
tag: tag,
data: {
url: "/"
},
requireInteraction: true
});
})
);
} else {
showDefaultNotification({ url: "/" });
}
}
};
});
Unfortunately when I receive a new push event it doesn't work, showing this exception:
Uncaught DOMException: Failed to execute 'waitUntil' on 'ExtendableEvent': The event handler is already finished.
at IDBRequest.request.onsuccess (https://192.168.0.102/pushServiceWorker.js:99:23)
How can I resolve this?
Thanks in advance
The initial call to event.waitUntil() needs to be done synchronously when the event handler is first invoked. You can then pass in a promise chain to event.waitUntil(), and inside that promise chain, carry out any number of asynchronous actions.
Your current code invokes an asynchronous IndexedDB callback before it calls event.waitUntil(), which is why you're seeing that error.
The easiest way to include IndexedDB operations inside a promise chain is to use a wrapper library, like idb-keyval, which takes the callback-based IndexedDB API and converts it into a promise-based API.
Your code could then look like:
self.addEventListener('push', event => {
// Call event.waitUntil() immediately:
event.waitUntil(
// You can chain together promises:
idbKeyval.get('fetchNotificationDataUrl')
.then(url => fetch(url))
.then(response => response.json())
.then(json => self.registration.showNotification(...)
);
});

Categories