How can I set global variables in chrome extension - javascript

I work on a chrome extension which has a const user that is set in the first lines of the background.js, when I change its values and use them later in a different function they always return to their set values in the beginning.
I read online about hoisting and that it is to source of the problem but how can I avoid it so i can actually manipulate the variables and use them later on with no problem.
I also saw the use of globalThis but when i tried to call or set the values in the functions with globalThis it didn't work as well.
Here is my first lines of background.js:
var user={
connection:undefined,
id:"id",
username:"username",
room:["id","password","url","tabid","ishost"],
room_members:[],
in_room:false,
room_process:[false,"intervelId"],
current_tab:["url","id"]
}
I also tried working with globalThis when setting some of the values of user like showen here:
function connect(){
globalThis.user.connection = new WebSocket('ws://192.168.3.16:8765');
globalThis.user.connection.onopen = function(e) {
globalThis.user.connection.send("get_id");
};
globalThis.user.connection.onmessage=function(event){
let data=event.data.split(',');
let temp_i=data[0]
globalThis.user.id=temp_i;
let temp_u=data[1]
globalThis.user.username=temp_u;
console.log("connection: ",user.id,",",user.username);
};
}
and for example if I got from the last function that user.id="Oak" when I try to recall the value in a different function like this:
function test(){
console.log(user.id);
}
it shows "id" and not "Oak"
EDIT
I tried to use the storage.local by putting the var user in the onInstalled event of the program and then by having two functions I can get the current value of user out of the local storage and then update it as well.
here is my code:
first init of the var user:
chrome.runtime.onInstalled.addListener(async () => {
//opens welcome page
let url = chrome.runtime.getURL("htmls/hello.html");
let tab = await chrome.tabs.create({ url });
//setting up the user var in chrome's local storage
chrome.storage.local.get(['userLocal'], async function (result) {
var user={
connection:undefined,
id:"id",
username:"username",
room:["id","password","url","tabid","ishost"],
room_members:[],
in_room:false,
room_process:[false,"intervelId"],
current_tab:["url","id"]
}
chrome.storage.local.set({userLocal: user}, function () {}); // save it in local.
});
//connect to server and recive id
connect()
});
the two functions function:
function get_user(){
chrome.storage.local.get(['userLocal'], async function (result) {
var userLocal = result.userLocal;
return userLocal;
});
}
function update_user(tmp_user){
chrome.storage.local.get(['userLocal'], async function (result) {
var userLocal = tmp_user;
chrome.storage.local.set({userLocal: userLocal}, function () {}); // save it in local.
return userLocal;
});
}
and for example if I use what I've written:
function connect(){
var user=get_user();
user.connection = new WebSocket('ws://192.168.3.16:8765'); //'ws://192.168.3.16:8765'
user=update_user(user);
}
I get this error: "Uncaught (in promise) TypeError: Cannot set properties of undefined (setting 'connection')"

The problem is that a background service worker is loaded when it is needed, and unloaded when it goes idle. So, after a few seconds of being idle, the background script unloads itself, making you lose the changes you made to the user object.
You could save the user object in chrome.storage.local, and then read and modify from there. Something like this:
// background.js; "initial lines" =>
chrome.storage.local.get(['userLocal'], async function (result) {
let userLocal = result.userLocal
if (userLocal === undefined) {
// it means there was nothing before. This way you don't overwrite
// the user object every time the backgorund.js loads.
var user={
connection:undefined,
id:"id",
username:"username",
room:["id","password","url","tabid","ishost"],
room_members:[],
in_room:false,
room_process:[false,"intervelId"],
current_tab:["url","id"]
}
chrome.storage.local.set({userLocal: user}, function () {}); // save it in local.
}})
When you want to modify the user, you retrieve it from local and modify the result before saving it back in the localStorage.
// example of modifying a property =>
chrome.storage.local.get(['userLocal'], async function (result) {
let userLocal = result.userLocal
if (userLocal !== undefined) {
userLocal.connection = new WebSocket('ws://192.168.3.16:8765');
// ...
chrome.storage.local.set({userLocal: userLocal}, function () {}); // save it in local.
}})
You can also just read the user properties, without modifying them and saving them back to the localStorage.
############ EDIT:
because chrome.storage.local is asynchronous, try to transform you code like this:
async function get_user(){
return new Promise(async function (res, rej) {
chrome.storage.local.get(['userLocal'], async function (result) {
var userLocal = result.userLocal;
res(userLocal);
});
});
}
var user= await get_user(); // call it like this.

Related

Injecting data object to window with Puppeteer

Background
I am using Puppeteer to create some PDFs. I need to inject some data into the page when Puppeteer loads it.
Problem
I have tried using evaluateOnNewDocument() which was successful when using a String only. When I try with an Object it fails. I also tried with evaluate() and it fails regardless of what I pass in.
Example
// Works
await page.evaluateOnNewDocument(() => {
window.pdfData = {};
window.pdfData = "Some String";
});
// Does not work
await page.evaluateOnNewDocument(() => {
window.pdfData = {};
window.pdfData = data;
});
// Fails
await page.evaluate(data => {
window.pdfData = {};
window.pdfData = data;
}, data);
I would like to access this object like this,
const data = window.pdfData;
Question
What is the proper way to pass a data object into window on a loaded Puppeteer page so that it can be accessed within the page to use the data client side?
Passing object to evaluate
You can pass data which will be serialized as JSON.
await page.evaluateOnNewDocument(data => { // <-- pass as parameter
window.pdfData = data; // <-- read it here
}, data); // <-- pass as argument
Passing object to evaluateOnNewDocument
evaluateOnNewDocument works similarly to evaluate, except it will run whenever there is a new window/navigation/frame. This way the data will stay even if you navigate away to another page.
You can pass data and read inside the function.
await page.evaluateOnNewDocument(data => {
window.pdfData = data;
}, data);

How to run a firebase function correctly?

This function will return the value of initiate_session, initiate_session[user_session].events, user_session
before the firebase function runs.
How to run the firebase function first ??
function createSession() {
var user_session = randomCharacters(20) //8iKhA4Aq2!6gZ)890ip#;
var ip = '127.0.0.1';
var initiate_session = new Object();
var session_started = new Date().getTime();
firebase.database().ref(user_session).once('value', function (snapshot) {
if (snapshot.exists()) {
console.log('session exists');
createSession();
} else {
console.log('session not exists')
initiate_session[user_session] = {
ip: ip,
session_started: session_started,
events: {}
};
firebase.database().ref(user_session).set({
ip: ip,
session_started: session_started,
events: {}
});
}
});
console.log('new session', initiate_session);
return [initiate_session, initiate_session[user_session].events, user_session];}
This is because the Firebase function is asynchronous - the code inside the method that gets the snapshot is a callback that is executed after the Firebase read is finished, which may take several seconds. However, as soon as you dispatch the read request via .once(...) your execution flow continues and the return is called.
There are a few possible solutions:
Pass a callback argument to your createSession() method that is called with the values you are trying to return, instead of returning them directly.
Return a promise from your method that resolves to give the values you're trying to return.
Use async/await syntax for your Firebase call. This is covered already in this question: running queries in firebase using async / await
Rough Example of #1
function createSession(onSessionCreated) {
var user_session = "whatever";
var initiate_session = new Object();
firebase.database().ref(user_session).once('value', function (snapshot) {
// do things with the snapshot
onSessionCreated(initiate_session, initiate_session[user_session].events, user_session)
});
}
// usage:
createSession(function (initiate_session, events, user_session) {
// do things with initiate_session, events and user_session
});
Rough Example of #2
function createSession() {
var user_session = "whatever";
var initiate_session = new Object();
firebase.database().ref(user_session).once('value').then(function (snapshot) {
// do things with the snapshot
return [initiate_session, initiate_session[user_session].events, user_session];
});
}
// usage:
createSession().then(function (results) {
// do things with results (i.e. the three elements in the array above)
});
Rough Example of 3
async function createSession() {
var user_session = "whatever";
var initiate_session = new Object();
const snapshot = await firebase.database().ref(user_session).once('value');
// do things with the snapshot
return [initiate_session, initiate_session[user_session].events, user_session];
}
// usage:
const results = await createSession();
If you're new to async/await code it probably won't be the easiest place to start as it may require changes elsewhere in your code, but this article is a good resource if you're keen to learn.

How can I keep a firebase function open when it's in another file

I have a file called db.js from which I make all my firebase calls.
I am calling a function in db.js from another file called home.js
How do I make it that the firebase connection stays open and the data gets passed back to home.js? I can't use a promise because that closes the connection.
Here is the function from db.js:
export function getShopNames() {
let userID = auth.currentUser.uid
let stores = []
userDB.ref('/users/' + userID + "/stores").on('value', snap => {
snap.forEach(storeNames => {
stores.push(storeNames.key)
})
return stores
})
}
and I call it from home like this:
let stores = db.getShopNames()
I want it to work so if a new store gets added to the real-time database, the variable updates
There is no concept of file based scope in JavaScript. The listener will stay active from the moment you call on('value', until you either call off on that same location or until you load a new page.
But your return stores doesn't do anything meaningful right now. It returns a value from the callback function that nobody will ever see/use.
Data is loaded from Firebase asynchronously, which means you can't return it from a function in the normal way. By the time the return statement runs, the data hasn't loaded yet. That's why you'll usually return a so-called promise, which then resolves when the data has loaded.
In your function that'd be:
export function getShopNames() {
return new Promise(function(resolve, reject) {
let userID = auth.currentUser.uid
let stores = []
userDB.ref('/users/' + userID + "/stores").once('value', snap => {
snap.forEach(storeNames => {
stores.push(storeNames.key)
})
resolve(stores);
}, (error) => {
reject(error);
})
}
Now you can call this function like this:
getShopNames().then((shopnames) => {
console.log(shopnames);
})
Update: you commented that you also want to handle updates to the shop names, you can't use once() and can't use promises (since those only resolve once).
Instead pass in a custom callback, and invoke that every time the shop names change:
export function getShopNames(callback) {
let userID = auth.currentUser.uid
let stores = []
userDB.ref('/users/' + userID + "/stores").once('value', snap => {
snap.forEach(storeNames => {
stores.push(storeNames.key)
})
callback(stores);
})
}
And then call it like:
getShopnames(function(shopnames) {
console.log(shopnames);
});

JavaScript bookmarklet code to get URL of tab that was launched from current tab

I am trying to develop a bookmarklet. The purpose of the bookmarklet is to display the URL of both the current tab and the new tab that was launched as a result of a window.open call. To simplify things, assume that the launcher.com launches a random URL to visit.
function getBothURLs() {
var currentURL, newWin;
function launchNew () {
currentURL = window.location.href;
newWin = window.open("https://www.launcher.com");
}
launchNew();
alert(currentURL);
alert(newWin.location.href); // displays 'about:blank'
}
I am unable to get the URL of the newly launched tab. The alert() (at the very end of the function below) does not correctly display the newly launch tab's URL; instead it displays
about:blank
When I was troubleshooting this within the Chrome console, I moved the definition of var currentURL, newWin to outside the scope of the getTwoURLs() function. When I invoked the function getBothURLs() from within the console, both currentURL and newWin had valid data.
How should the function getBothURLs() be modified to achieve the desired purpose?
A note on window.open from MDN:
...remote URLs won't load immediately. When window.open() returns, the window always contains about:blank. The actual fetching of the URL is deferred and starts after the current script block finishes executing...
In the above case the current blocks ends with the getBothURLs() function, but you attempt to check the new URL within that block itself, and according to the quote above, you see the about:blank url.
To fix this just defer the lookup until the next task. You can use something like setTimeout to post the lookup onto the next task or, even better, use Promises if you wish to return the value from the function.
A sample implementation could look like:
function getBothURLs() {
return new Promise((resolve) => {
var newWin = window.open("//www.stackoverflow.com");
setTimeout(() => resolve(newWin))
}).then((win) => {
return new Promise((resolve, reject) => {
var check = setInterval(() => {
try {
if(win.location.href !== "about:blank"){
clearInterval(check);
resolve(win.location.href)
}
} catch(e) {
clearInterval(check);
reject(e);
}
}, 50)
})
})
}
getBothURLs().then(url => console.log(url))
Although, the above mentioned solution works, I would still recommend that you store new window's URL in a variable and then call open with that variable.
Update 1:
This example snippet uses the non-promise callback version that could return both the urls. Although, it's possible to modify the promise version to return two urls as well but since the OP asked for a non-promise based version in the comments so I provided the new snippet.
Have a look:
function getBothURLs(callback){
var current = window.location.href;
var win = window.open("https://stackoverflow.com");
var check = setInterval(() => {
try {
if(win.location.href !== "about:blank"){
clearInterval(check);
callback(null, current, win.location.href)
}
} catch(e) {
clearInterval(check);
callback(e);
}
}, 50);
}
getBothURLs((err, _cur, _new) => {
if(err){
// some error occurred, probably a cross-origin access
console.error("Failed to get URL", err);
return;
}
console.log(`Current: ${_cur} New: ${_new}`);
})

Callback is not working in ajax request?

I am trying to build a contentEditor with draft js. Exactly the feature is Extract the data from url like Facebook. But I am stuck with this part. Callback is not working.
First I wrapped my state with compositeDecorator Like this
constructor(props) {
super(props);
const compositeDecorator = new CompositeDecorator([
.... {
strategy: linkStrategy,
component: decorateComponentWithProps(linkComp, {
passit
})
}
....
]);
}
// This is my strategy
function linkStrategy(contentBlock, callback, contentState) {
findLinkInText(LINK_REGEX, contentBlock, callback)
}
function findLinkInText(regex, contentBlock, callback) {
const text = contentBlock.getText();
let matchArr, start;
if ((matchArr = regex.exec(text)) !== null) {
start = matchArr.index;
let URL = matchArr[0];
console.log(URL);
axios.post('/url', {
url: URL
}).then(response => {
passit = response.data
//not working
callback(start, start + URL.length)
})
//working
callback(start, start + URL.length)
}
}
If the callback won't work, the component will not render..
I don't know this is a basic javascript problem. But the thing is I want to fetch the url data from my server and I have to pass the data via props to my component and render it.
UPDATE FOR THE ANSWER
function findLinkInText(regex, contentBlock, callback) {
const text = contentBlock.getText();
let matchArr, start;
if ((matchArr = regex.exec(text)) !== null) {
start = matchArr.index;
let url = matchArr[0];
axios.post('/url', {
url: URL
}).then(response => {
passit = response.data
handleWithAxiosCallBack(start, start + matchArr[0].length, callback)
}).catch(err => console.log(err))
}
}
function handleWithAxiosCallBack(start, startLength, callback) {
console.log(callback); //Spits out the function But Not working
callback(start, startLength)
}
The below described technique would help you to achieve the behaviour you are expecting.
Why your solution does not works: The reason the desired action that needs to be performed by the callback is not performing is because, draft expects the callback to be called synchronous. Since you are using an async function(the axios api call) and asynchronously calling the callback has no effect.
Solution: This may not be a efficient solution, but could get the job done. In simple words, all you have to do is to store the results from the axios call in a variable(temporarily) and then trigger the re-render for your editor, retrieve the result store earlier and use it to call the callback.
I'm following based on this example here. Assuming that you store the editor state in the component's state. Below is a pseudo-code which you might need to implement as per your needs.
Let's assume your component state is like below which holds the Editor's state.
constructor(props){
super(props);
// .. your composite decorators
// this is your component state holding editors state
this.state = {
editorState: EditorState.createWithContent(..)
}
// use this to temporarily store the results from axios.
this.tempResults = {};
}
Assuming you are rendering your Editor to something like below. Notice the ref. Here the editors reference is stored in the component's editor variable which you can access later. Using a string as ref would work, but this is the recommended way to store refs.
<Editor
ref={ (editor) => this.editor }
editorState={this.state.editorState}
onChange={this.onChange}
// ...your props
/>
In your component, write a function to update the editor with currentState which would force the re-render. Make sure this function is bound to your component so that we get the correct this(context).
forceRenderEditor = () => {
this.editor.update(this.state.editorState);
}
In your findLinkInText function do the below.
First make sure it(findLinkInText) is bound to your component so we get the correct this. You can use arrow function to do this or bind it in the component constructor.
Second, check if we have the result for the url already in tempResults
which we declared in the component's constructor. If we have one, then call the callback immediately with appropriate arguments.
If we don't have a result already, the make the call and store the result in the tempResults. After storing, call the already defined this.forceRenderEditor method which would invoke draft to re-check and this time, since we have stored the results in the tempResults, the callback will be called and appropriate changes would reflect.
function findLinkInText(regex, contentBlock, callback) {
const text = contentBlock.getText();
let matchArr, start;
if ((matchArr = regex.exec(text)) !== null) {
start = matchArr.index;
let URL = matchArr[0];
console.log(URL);
// do we have the result already,??
// call the callback based on the result.
if(this.tempResults[url]) {
// make the computations and call the callback() with necessary args
} else {
// if we don't have a result, call the api
axios.post('/url', {
url: URL
}).then(response => {
this.tempResults[url] = response.data;
this.forceRenderEditor();
// store the result in this.tempResults and
// then call the this.forceRenderEditor
// You might need to debounce the forceRenderEditor function
})
}
}
}
Note:
You have to determine if you need to clear the tempResults. If so you need to implement the logic for it at the appropriate place.
To store the tempResults you can make use of technique called memoization. The above described one is a simple one.
Since your results are memozied, this might be an advantage for you if the axios api call results are not gonna be changing for the same input. You might not have to hit the api again for the same query.
The data you store in your tempResults should be the response from the api call or something from which you can determine the arguments that you need to pass to callback.
I think, you might need to debounce the forceRenderEditormethod to avoid repeated update if there are many api being called for each render.
Finally, i could not find a place where draft uses or suggests for an async callback. You might have to check with the library's team if they support/need such a feature. (If needed make the changes and raise a PR if their team is ok with one.)
Update
To bind you can move the functions within the component and write it the below manner.
linkStrategy = (contentBlock, callback, contentState) => {
this.findLinkInText(LINK_REGEX, contentBlock, callback)
}
findLinkInText = (...args) => {
}
And in your constructor you can call it like this
const compositeDecorator = new CompositeDecorator([
.... {
strategy: this.linkStrategy,
component: decorateComponentWithProps(linkComp, {
passit
})
}
....
]);
}
Or if you want to reuse the function across multiple components then you can bind it in the below manner. But make sure to use the same state in all of the sharing components(or use callback to define custom states)
Your constructor will be like
const compositeDecorator = new CompositeDecorator([
.... {
strategy: linkStrategy.bind(this),
component: decorateComponentWithProps(linkComp, {
passit
})
}
....
]);
}
Your link strategy will be like this
function linkStrategy(contentBlock, callback, contentState) {
findLinkInText.call(this,LINK_REGEX, contentBlock, callback);
}
You can use either of the above method to bind your functions.
Assuming everything else is working, I would expect something more what is below. I am not set up to use Axios so I can't actually test this.
// I made these global variables because you are trying to use them
// in both the .post and the .then
var start; // global variable
var URL // global variable
function processResponse (aStart, aStartURL, passit) {
// do something with the reponse data
}
function findLinkInText(regex, contentBlock) {
const text = contentBlock.getText();
let matchArr;
if((matchArr = regex.exec(text)) !== null){
start = matchArr.index;
// renamed because it is not the url, its data passed to the server
URL = matchArr[0];
console.log(URL);
axios.post('/url',{url:URL}).then(response => {
passit = response.data;
processResponse (start, start + URL.length, passit)
}).catch(function (error) {
console.log(error);
});
}
}
Note that "axios depends on a native ES6 Promise implementation to be supported. If your environment doesn't support ES6 Promises, you can polyfill." which means that this does not work in all browser. ( I could not get it to work in IE 11 even after include the recommended include here https://github.com/stefanpenner/es6-promise
Here is my code that I did get working in Edge:
axios(
{
method: 'post',
url: '/wsService.asmx/GetDTDataSerializedList',
data: { parameters: 'one' },
callback: function () { alert() }
})
.then(response =>{
response.config.callback();
console.log(response);
})
.catch(function (error) {
console.log(error);
});
});
With that I changed your code accordingly as shown below
function findLinkInText(regex, contentBlock, callback) {
const text = contentBlock.getText();
let matchArr, start;
if ((matchArr = regex.exec(text)) !== null) {
start = matchArr.index;
var URL = matchArr[0];
console.log(URL);
// I am putting the parameters used to make the call in a JSON object to be used in the "then"
var myparameters = { myCallback: callback, URL: URL, start: start };
axios({
method:post,
url:'/url',
data: { url: URL },
// note that i am attaching the callback to be carrired through
myParameters: myparameters
}).then(response => {
// This section is the callback generated by the post request.
// it cannot see anything unless they declared globally or attached to the request and passed through
// which is what i did.
passit = response.data
var s = response.config.myParameters.start;
var u = response.config.myParameters.URL
response.config.myParameters.myCallback(s, s + u.length)
})
}
}

Categories

Resources