in our react native app we are using web sockets, sometimes when we have an abnormal closure (websocket: close 1006 (abnormal closure): unexpected EOF)
the event onclose/onerror is not called right away, but after 20-60 minutes later (on ios only). Please note, this issue appears only in background.
this cause our app to not knowing the connection is closed, and it is not initiating new connection
we have tried listening to the network with wireshark but the problem only occurs sometimes and we did not manage catching it for a last 24 hours.
How can we detect such event?
our code:
initiateConnection = (url) => {
this.conn = new WebSocket(url);
this.conn.onopen = this._handleConnectionOpen;
this.conn.onerror = this._handleConnectionError;
this.conn.onclose = this._handleConnectionClose;
this.conn.onmessage = this._handleMessage;
};
_handleConnectionClose = data => {
Log.info('WS - connection closed', data.reason);
};
_handleConnectionError = data => {
Log.info('WS - connection closed', data.reason);
};
Related
As the title suggests, im trying to listen for click-event from my connected BLE peripheral device even after my react-native app is killed/background mode.
While connected i have a notification subscription on my BLE peripheral device and everytime i press button on device, my app gets notified. I want this subscription to last even if the user kills the application.
The app works fine in foreground and inactive, but when i kill the app on iOS it stops responding to button click.
On android i found a library react-native-background-actions which helped solve this. Here is the background code that is currently working on android.
import BackgroundJob from "react-native-background-actions";
playing = BackgroundJob.isRunning();
const sleep = (time) =>
new Promise((resolve) => setTimeout(() => resolve(), time));
BackgroundJob.on("expiration", () => {
console.log("iOS: I am being closed!");
});
const taskRandom = async (taskData) => {
if (Platform.OS === "ios") {
console.warn(
"This task will not keep your app alive in the background by itself, use other library like react-native-track-player that use audio,",
"geolocalization, etc. to keep your app alive in the background while you excute the JS from this library."
);
}
await new Promise(async (resolve) => {
// For loop with a delay
const { delay } = taskData;
console.log(BackgroundJob.isRunning(), delay);
for (let i = 0; BackgroundJob.isRunning(); i++) {
console.log("Ran -> ", i);
// await BackgroundJob.updateNotification({
// taskDesc: "Emergency -> " + i,
// });
await sleep(delay);
}
});
};
const options = {
taskName: "Example",
taskTitle: "ExampleTask title",
taskDesc: "ExampleTask desc",
taskIcon: {
name: "ic_launcher",
type: "mipmap",
},
color: "#ff00ff",
linkingURI: "exampleScheme://chat/jane",
parameters: {
delay: 30000,
},
};
/**
* Toggles the background task
*/
export const toggleBackground = async () => {
playing = !playing;
if (playing) {
try {
console.log("Trying to start background service");
await BackgroundJob.start(taskRandom, options);
console.log("Successful start!");
} catch (e) {
console.log("Error", e);
}
} else {
console.log("Stop background service");
await BackgroundJob.stop();
}
};
I tried reading the core bluetooth background processing for iOS apps and added a restoration identifier on my start method like this:
BleManager.start({
showAlert: true,
restoreIdentifierKey: "IDENTIFIER",
queueIdentifierKey: "IDENTIFIER",
}).then(() => {
console.log("Module initialized");
});
Does anyone have a suggestion on how to keep the subscription while app is in background? react-native-background-actions suggests audio or geolocation libraries, but these are not relevant for my application.
Any help would be greatly appreciated.
You have two very different states you are talking about; Background and killed.
If your app is not onscreen but is still in memory then it is "suspended". It remains suspended until
It is brought back into the foreground by the user
A supported background event occurs and it executes briefly while before becoming suspended again.
It is removed from memory because iOS needs resources
It is removed from memory by the user "Swiping it away"
If an iOS app is suspended and you have added Bluetooth background mode to your app then it will just work; This is scenario 2.
You need to consider what you want to do if the peripheral goes out of range; Do you want to try and reconnect or do you want to give up.
If you want to try and reconnect then you simply call connect in response to a disconnection. If the peripheral comes into range then Core Bluetooth will provide a call-back to your app (even if it is suspended). This applies in both scenario 1 & 2.
An app can be in two different "killed" states; terminated by the system and terminated by the user. These are scenarios 3 & 4
If the app is terminated by the system, scenario 3, and you have set up Core Bluetooth state restoration when you initialised Core Bluetooth then iOS will relaunch your app. When relaunched you need to set up Core Bluetooth again and then the event will be delivered to your app as usual.
If the app is terminated by the user swiping up, scenario 4, then generally speaking your app is dead until the user relaunches it.
You haven't shown how you are using Core Bluetooth, so I can't offer any concrete suggestions but I can say the approach you have shown, trying to keep your app running in the background, is not the right approach and will definitely not work on iOS. There may even be a better approach on Android, but I am not familiar with that platform. Generally keeping an app around, in memory, performing useless work is just wasting memory and battery resources.
For the IOS App the connection works , but after the phone is idle for a while , the connection is lost, as such the RPC calls hangs, without any response.
I had the same issue in JAVA, there I added a DeadLine and rebuilt the channel when the Deadline Exceeded, something like below.
stub.withDeadlineAfter(timeout, TimeUnit.MILLISECONDS).execute(input, new StreamObserver<AgentGuardStringResponse>() { ... }
Then on error (deadline exceeded)
mChannel.shutdown();
This works fine.
For objective-c/ios
I set a time out for the RPC calls as so
[call setTimeout:timeout];
call.requestHeaders[#"sessionId"] = sessionId;
[call start];
And Try to rebuild ,
_serviceClient = [[AgentGuardService alloc] initWithHost:GRPCMetadata.shared.uri];
This does not seem to work as the app stays idle for a while after starting and eventually the Requests start flowing through.
Any pointers/guides will be highly helpful.
Thanks
I'm building a ReactJs PWA but I'm having trouble detecting updates on iOS.
On Android everything is working great so I'm wondering if all of this is related to iOS support for PWAs or if my implementation of the service worker is not good.
Here's what I've done so far:
Build process and hosting
My app is built using webpack and hosted on AWS. Most of the files (js/css) are built with some hash in their name, generated from their content. For those which aren't (app manifest, index.html, sw.js), I made sure that AWS serves them with some Cache-Control headers preventing any cache. Everything is served over https.
Service Worker
I kept this one as simple as possible : I didn't add any cache rules except precache for my app-shell:
workbox.precaching.precacheAndRoute(self.__precacheManifest || []);
Service-worker registration
Registration of the service worker occurs in the main ReactJs App component, in the componentDidMount() lifecycle hook:
componentDidMount() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then((reg) => {
reg.onupdatefound = () => {
this.newWorker = reg.installing;
this.newWorker.onstatechange = () => {
if (this.newWorker.state === 'installed') {
if (reg.active) {
// a version of the SW is already up and running
/*
code omitted: displays a snackbar to the user to manually trigger
activation of the new SW. This will be done by calling skipWaiting()
then reloading the page
*/
} else {
// first service worker registration, do nothing
}
}
};
};
});
}
}
Service worker lifecycle management
According to the Google documentation about service workers, a new version of the service worker should be detected when navigating to an in-scope page. But as a single-page application, there is no hard navigation happening once the app has been loaded.
The workaround I found for this is to hook into react-router and listen for route changes, then manually ask the registered service worker to update itself :
const history = createBrowserHistory(); // from 'history' node package
history.listen(() => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.getRegistration()
.then((reg) => {
if (!reg) {
return null;
}
reg.update();
});
}
});
Actual behavior
Throwing a bunch of alert() everywhere in the code showed above, this is what I observe :
When opening the pwa for the first time after adding it to the homescreen, the service worker is registered as expected, on Android and iOS
While keeping the app opened, I deploy a new version on AWS. Navigating in the app triggers the manual update thanks to my history listener. The new version is found, installed in the background. Then my snackbar is displayed and I can trigger the switch to the new SW.
Now I close the app and deploy a new version on AWS. When opening the app again :
On Android the update is found immediately as Android reloads the page
iOS does not, so I need to navigate within the app for my history listener to trigger the search for an update. When doing so, the update is found
After this, for both OS, my snackbar is displayed and I can trigger the switch to the new SW
Now I close the app and turn off the phones. After deploying a new version, I start them again and open the app :
On Android, just like before, the page is reloaded which detects the update, then the snackbar is displayed, etc..
On iOS, I navigate within the app and my listener triggers the search for an update. But this time, the new version is never found and my onupdatefound event handler is never triggered
Reading this post on Medium from Maximiliano Firtman, it seems that iOS 12.2 has brought a new lifecycle for PWAs. According to him, when the app stays idle for a long time or during a reboot of the device, the app state is killed, as well as the page.
I'm wondering if this could be the root cause of my problem here, but I was not able to find anyone having the same trouble so far.
So after a lot of digging and investigation, I finally found out what was my problem.
From what I was able to observe, I think there is a little difference in the way Android and iOS handle PWAs lifecycle, as well as service workers.
On Android, when starting the app after a reboot, it looks like starting the app and searching an update of the service worker (thanks to the hard navigation occuring when reloading the page) are 2 tasks done in parallel. By doing that, the app have enough time to subscribe to the already existing service worker and define a onupdatefound() handler before the new version of the service worker is found.
On the other hand with iOS, it seems that when you start the app after a reboot of the device (or after not using it for a long period, see Medium article linked in the main topic), iOS triggers the search for an update before starting your app. And if an update is found, it will be installed and and enter its 'waiting' status before the app is actually started. This is probably what happens when the splashscreen is displayed...
So in the end, when your app finally starts and you subscribe to the already existing service worker to define your onupdatefound() handler, the update has already been installed and is waiting to take control of the clients.
So here is my final code to register the service worker :
componentDidMount() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then((reg) => {
if (reg.waiting) {
// a new version is already waiting to take control
this.newWorker = reg.waiting;
/*
code omitted: displays a snackbar to the user to manually trigger
activation of the new SW. This will be done by calling skipWaiting()
then reloading the page
*/
}
// handler for updates occuring while the app is running, either actively or in the background
reg.onupdatefound = () => {
this.newWorker = reg.installing;
this.newWorker.onstatechange = () => {
if (this.newWorker.state === 'installed') {
if (reg.active) {
// a version of the SW already has control over the app
/*
same code omitted
*/
} else {
// very first service worker registration, do nothing
}
}
};
};
});
}
}
Note :
I also got rid of my listener on history that I used to trigger the search for an update on every route change, as it seemed overkill.
Now I rely on the Page Visibility API to trigger this search every time the app gets the focus :
// this function is called in the service worker registration promise, providing the ServiceWorkerRegistration instance
const registerPwaOpeningHandler = (reg) => {
let hidden;
let visibilityChange;
if (typeof document.hidden !== 'undefined') { // Opera 12.10 and Firefox 18 and later support
hidden = 'hidden';
visibilityChange = 'visibilitychange';
} else if (typeof document.msHidden !== 'undefined') {
hidden = 'msHidden';
visibilityChange = 'msvisibilitychange';
} else if (typeof document.webkitHidden !== 'undefined') {
hidden = 'webkitHidden';
visibilityChange = 'webkitvisibilitychange';
}
window.document.addEventListener(visibilityChange, () => {
if (!document[hidden]) {
// manually force detection of a potential update when the pwa is opened
reg.update();
}
});
return reg;
};
As noted by Speckles (thanks for saving me the headache), iOS installs the new SW before launching the app. So the SW doesn't get a chance to catch the 'installing' state.
Work-around: check if the registration is in the waiting state then handle it.
I've made an (untested) example of handling this. - a mod to the default CRA SW.
I'm running an app, created using IBM MobileFirst 7, on iOS 8.4. At first it works fine: I can write to jsonstore and make other native function calls.
Then the application starts syncing and writing to the JSONStore (a lot of data) and it stops after a while (no errors in console). It usually happens while trying to call the WL.Client.invokeProcedure function.
I also noticed a strange behavior: if I double tap the home button it makes the next WL.Client.invokeProcedure call in the loop and I receive the results from the adapter but then it stops again (everytime I double tap the home button this happens). This also happens with other native calls (e.g. I tried to make a call to other native function in the JS console by using cordova and it only gets called after I double tap the home button).
Does anyone know what may be causing this behavior?~
EDIT:
Regarding the native calls: Since I'm using cordova, I can use a plugin to access some native functionality, example:
//this is what I meant by "native function calls"
cordova.exec(successCallback, errorCallback, 'SFRPowerSave', 'enable', []);
I'm not sure about WL.Client.invokeProcedure and WL.JSONStore functions, but I think they also use native code.
Here's what I'm doing:
//I get the first 50 dirty documents
function push(){
var numberOfDocumentsToPush = 50;
var dirtyDocuments = currentSyncStore.dirtyDocuments.splice(0, numberOfDocumentsToPush);
//then I call the adapter add method and return a promise
return when(WL.Client.invokeProcedure({
adapter : adapter.name,
procedure : adapter.push.procedure,
parameters : [ JSON.stringify(dirtyDocuments),
JSON.stringify({add: addParams}) ],
compressResponse : false
}, {
timeout: adapter.timeout,
invocationContext: {context: this, document: dirtyDocuments}
});
}
//after the promise is returned, I get the next 50 dirty documents and call the push function again, I usually have a lot of dirtyDocuments (lets say 10000).
//After a while it just stops, the WL.Client.invokeProcedure doesn't reject or resolve the promise and no timeout occurs.
//I can interact with the interface of the application but if I try to call some native function, it will not work (but it gets called immediatly after entering the multitask mode - double tap home button in ipad/iphone)
//In the adapter I call the stored procedure from the DB2 database one time for each document:
function pushLogs(logs, params, addFunction){
var parsedParams = JSON.parse(params);
var addParsedParams = parsedParams.add;
var addConfig = getConfig("logs", addFunction, JSON.stringify(addParsedParams));
var parsedLogs = JSON.parse(logs);
var globalResult = {
isSuccessful: true,
responses: []
};
for (var i = 0; i < parsedLogs.length; i++) {
var options = {
values : JSON.stringify(parsedLogs[i].document),
spConfig: addConfig
};
var result = invokeSQLStoredProcedure(options);
globalResult.isSuccessful = globalResult.isSuccessful && result.isSuccessful;
globalResult.responses.push(result);
}
return globalResult;
}
A PMR was opened to handle this question. It has turned out to be a Cordova defect and is now being handled via APAR PI47657: Application hangs when trying to call JSONStore asynchronously to sync data.
The fix will appear in a future iFix release, available at IBM Fix Central (as well as via the PMR opened).
Getting a strange error from QBFC. This code fails:
var qbRequest = sessionManager.CreateMsgSetRequest("US", 7, 0);
qbRequest.Attributes.OnError = ENRqOnError.roeStop;
var qbQuery = qbRequest.AppendCustomerQueryRq();
// Don't get all fields (would take forever) - just get these...
qbQuery.IncludeRetElementList.Add("ListID");
qbQuery.IncludeRetElementList.Add("Phone");
qbQuery.IncludeRetElementList.Add("AltPhone");
qbQuery.IncludeRetElementList.Add("Fax");
var qbResponses = sessionManager.DoRequests(qbRequest);// <<- EXCEPTION: INVALID TICKET PARAMETER !!!
However - if I just put a delay in there it works fine. e.g.
System.Threading.Thread.Sleep(1000);
var qbResponses = sessionManager.DoRequests(qbRequest);// <<- WORKS FINE!!
I found this out because anytime I would set a breakpoint in the code to debug the problem - the problem would go away. So then I learned that I could just put a 1 second Sleep in there and simulate the same behavior. (btw - half second delay doesn't help - still throws exception)
This has got me scratching my head. I initialize the sessionManager at the start of the app and reuse it throughout my code. And it works everywhere else in this app but here. I have looked at the raw XML (for both request and response) and don't see anything wrong in there. The response just has an error: "The data file is no longer open. Cannot continue." but nothing to indicate why. (and the data file is open, after this exception I can use it for any number of OTHER things)
I suspect it has something to do with WHEN this code is called. I have a listener that listens for messages from the XDMessaging add-in (used for inter-process communication). When the listener receives a message the event calls this code. But this code is called in the same app (and same thread) as tons of OTHER QBFC code I have that does very similar stuff without a problem. And if it was a threading issue I would think the error would happen regardless of if I Sleep() for a second or not.
Anybody have any ideas?
What version of QBSDK are you using and what version of QuickBooks? I tested using QBSDK 13 (though I specified version 7 when creating the message request), with version 14.0 Enterprise R5P. I experienced no problems or exceptions without having a delay. Perhaps there's something going on with your SessionManager since it appears that you've already opened the connection and began the session elsewhere?
Here's my code that had no problem:
QBSessionManager SessionMananger = new QBSessionManager();
SessionMananger.OpenConnection2("Sample", "Sample", ENConnectionType.ctLocalQBD);
SessionMananger.BeginSession("", ENOpenMode.omDontCare);
IMsgSetRequest MsgRequest = SessionMananger.CreateMsgSetRequest("US", 7, 0);
MsgRequest.Attributes.OnError = ENRqOnError.roeStop;
var qbQuery = MsgRequest.AppendCustomerQueryRq();
qbQuery.IncludeRetElementList.Add("ListID");
qbQuery.IncludeRetElementList.Add("Phone");
qbQuery.IncludeRetElementList.Add("AltPhone");
qbQuery.IncludeRetElementList.Add("Fax");
IMsgSetResponse MsgResponse = SessionMananger.DoRequests(MsgRequest);
for (int index = 0; index < MsgResponse.ResponseList.Count; index++)
{
IResponse response = MsgResponse.ResponseList.GetAt(index);
if (response.StatusCode != 0)
{
MessageBox.Show(response.StatusMessage);
}
}