I'm trying to find a simple and easy way to cancel all running sagas within a "page" when the user decides to navigate to another "page" within the app... We are not using routing, but instead each "page" is its own widget within a larger host application that is responsible for creating and loading each page when the user navigates...
Currently, we are using redux-saga and have setup logic like so (simplified for brevity) when a page widget is created and loaded...
// page-sagas
export function* rootSaga() {
const allSagas = [
// ... all sagas used by page (example) ...
// function* watchFoo() {
// yield takeEvery(FooAction, foo);
// }
];
yield all(allSagas.map((saga) => call(saga)));
}
// page-widget
onLoad = () => {
const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducer, initState, applyMiddlware(sagaMiddleware));
sagaMiddleware.run(rootSaga);
}
Ideally, I'd prefer to avoid having to add forking logic to every single saga in every single page-widget, and looking at the Redux-Saga Task API, it says you can cancel a task returned by the call to middleware.run, but I'm wondering if this propagates down to all nested / child sagas that are currently in progress, or if there are any issues / gotcha's I should be aware of:
Example:
// page-widget
onLoad = () => {
...
this.task = sagaMiddlware.run(rootSaga);
}
destroy = () => {
this.task.cancel();
}
Related
I'm using Rails 5 to make a simple turn based game tracker for an in-person social game (via phones/tablets/etc..)
I want to have all the 'players' in the game (list of sessions/users/...) to reload their browsers automatically once a player has taken an action.
I know that there are live update capabilities such as AJAX and websockets, but they all seem far too weighty for what seems to be a simple problem. Furthermore, I want to update other clients pages, not just the client initiating the action.
Is there a simple solution to send a reload? Or do I need to code something up in one of the more complicated APIs?
For the simple trouble, you still can use AJAX to reload user client by making interval request for each XX seconds. The server can return the last action time which can be used for client to determine that it should reload itself or not.
For example, on the controller
# SomeController
def get_last_action_time
# Return the timestamp of the last action
render json: {last_action_time: "2017-12-29 10:00:42 UTC"}
end
on the client
function getLocalLastAction () {
/* return timestamp of the last action on local */
}
function setLocalLastAction (time) {
/* Store the `time` to somewhere, ex: localStorage */
}
function checkLastAction () {
$.getJSON("/get_last_action_time", function (data) {
if (getLocalLastAction() < data.last_action_time) {
/* destroy the interval */
setLocalLastAction(data.last_action_time)
/* do the reload page */
} else {
/* do nothing */
}
})
}
// Check every 1 second, shouldn't be too short due to performance
var checking = setInterval(checkLastAction, 1000)
Then when user A do an action, the server last_action_time will change, hence client of other users will be reloaded at most after 1 second.
This way is old but quite easy to do in some simple case, and when you implement together with actions caching, the performance of app still acceptable. In the more complicated cases, I suggest using WebSocket solution for
Full control
Low latency
Better performance for app
Thanks to #yeuem1vannam's answer, here is the final code I used that helps avoid the race condition of a page loading old information while the time is being updated and then the javascript updating the time and getting the new time, and hence missing the reload.
The javascript code:
var actionChecker;
function doneChecking () {
clearInterval(actionChecker);
}
function checkLastAction () {
// Get the game ID from the html access span
var dataId = document.getElementById('javascript_data_access');
if (!dataId) return doneChecking();
var initActionTime = dataId.getAttribute('init_last_action_time');
if (!initActionTime) return doneChecking();
dataId = dataId.getAttribute('game_number');
if (!dataId) return doneChecking();
// Get the last action time
var ret = $.getJSON("/get_last_action_time/"+dataId, function (data) {
var lastActionTime = data.last_action_time;
if (!lastActionTime) return doneChecking();
if (lastActionTime>initActionTime) {
location.reload();
}
})
}
window.onload = function() {
// Check every 1 second, shouldn't be too short due to performance
actionChecker = setInterval(checkLastAction, 1000);
}
The controller's action:
def get_last_action_time
last_time = nil
begin
#game = Game.find_by_id(params[:id])
# Return the timestamp of the last action
last_time = (#game && !#game.endTime) ? #game.reloadTime.to_i : 0
rescue ActiveRecord::RecordNotFound
last_time = 0
end
# Stop bugging us after 30m, we should have moved on from this page
last_time==0 if (last_time!=0 && (milliseconds - last_time)>30*60*1000)
render json: {last_action_time: last_time}
end
And then in the html.erb:
<span id='javascript_data_access' game_number=<%= params[:id] %> init_last_action_time=<%= #game.reloadTime %>></span>
Obviously you need to add reloadTime to your model and also endTime if there's a time you no longer want to check for reloads anymore.
Seems to be working fine so far, you have to make sure that you're careful about who is in charge of setting reloadTime. If two pages set reloadTime everytime they reload, you'll be stuck in a reload loop battle between the two pages.
So I'm learning dart and web development in general. Right now I'm experimenting with the history API. I have:
import 'dart:html';
void main() {
ParagraphElement paragraph = querySelector('.parag');
ButtonElement buttonOne = querySelector('.testaja');
buttonOne.onClick.listen((_) {
window.history.pushState(null, 'test title', '/testdata/');
window.history.forward();
});
ButtonElement buttonTwo = querySelector('.testlagi');
buttonTwo.onClick.listen((_) {
window.history.back();
});
window.onPopState.listen((_) {
window.alert(window.location.pathname);
});
}
My conclusion is that onPopState only triggers when we click on browser's back or forward button, or using window.history.forward() or window.history.back(). So this is like, we render a template, then change its url using pushState, not update template based on url changes. Is this true or not?
Edit:
So maybe I'm not clear enough. Let's say I have something like this:
void main() {
InputElement input = querySelector('.input')
ButtonElement changeUrl = querySelector('.change-url');
changeUrl.onClick.listen((event) {
window.history.pushState(null, 'test tile', input.value);
});
Map urls = {
'/' : showRoot,
'/user/:id' : showUserProfile
};
window.onPopState.listen((_) {
var location = window.location.pathname;
urls[location]();
});
}
I can get input's value by clicking on changeUrl, and then by adding a listener to changeUrl, I can use pushState to update url on browser. What I'm expecting is, when I do pushState, the window.onPopState will triggered and invoke the callback when in reality it doesn't.
tldr, what I'm trying to achieve is:
listen on url changes -> get current url -> use current url to invoke a handler stored in a map. Using onHashChange also doesn't work when updating url using pushState prefixed by #.
edit
set the hash using
window.location.hash = input.value;
this triggers the PopState and HashChange event
as does a click on such a link
abc
original
I don't have time to take a close look what you'r trying to achive..
But I think you should add an event handler for 'window.onHashChange' this way ordinary links work too for navigation, not only buttons with onclick-handlers modifying browser history.
I work on a Windows 8 app, and from a page that I use link hystory for running back and forward through the app, I also have 3 or 4 links to external websites(eg: facebook or my site). I tried to run them in iframe, or also to make them open in the default browser like simple links. Both method resulted in an error in base.js that says it can't handle my error (!?) I searched a lot before asking here. I watched msdn sample that works just fine, but if i copy what I need in my app results in the same error. I I use it from another page where I dont have forward history, it works, but i really need it on the front page. Any ideeas? Thank you very much.
LE:
This is my items.js code: ( for the items.html page )
(function () {
"use strict";
var appViewState = Windows.UI.ViewManagement.ApplicationViewState;
var ui = WinJS.UI;
ui.Pages.define("/pages/items/items.html", {
// This function is called whenever a user navigates to this page. It
// populates the page elements with the app's data.
ready: function (element, options) {
var listView = element.querySelector(".itemslist").winControl;
listView.itemDataSource = Data.groups.dataSource;
listView.itemTemplate = element.querySelector(".itemtemplate");
listView.oniteminvoked = this._itemInvoked.bind(this);
this._initializeLayout(listView, Windows.UI.ViewManagement.ApplicationView.value);
listView.element.focus();
WinJS.Utilities.query("a").listen("click", this.linkClickEventHandler, false);
},
// This function updates the page layout in response to viewState changes.
updateLayout: function (element, viewState, lastViewState) {
/// <param name="element" domElement="true" />
var listView = element.querySelector(".itemslist").winControl;
if (lastViewState !== viewState) {
if (lastViewState === appViewState.snapped || viewState === appViewState.snapped) {
var handler = function (e) {
listView.removeEventListener("contentanimating", handler, false);
e.preventDefault();
}
listView.addEventListener("contentanimating", handler, false);
var firstVisible = listView.indexOfFirstVisible;
this._initializeLayout(listView, viewState);
if (firstVisible >= 0 && listView.itemDataSource.list.length > 0) {
listView.indexOfFirstVisible = firstVisible;
}
}
}
},
linkClickEventHandler: function (eventInfo) {
eventInfo.preventDefault();
var link = eventInfo.target;
WinJS.Navigation.navigate(link.href);
},
// This function updates the ListView with new layouts
_initializeLayout: function (listView, viewState) {
/// <param name="listView" value="WinJS.UI.ListView.prototype" />
if (viewState === appViewState.snapped) {
listView.layout = new ui.ListLayout();
} else {
listView.layout = new ui.GridLayout();
}
},
_itemInvoked: function (args) {
var groupKey = Data.groups.getAt(args.detail.itemIndex).key;
WinJS.Navigation.navigate("/pages/split/split.html", { groupKey: groupKey });
}
});
})();
And from items.html I have different types of links: some of them links to other application pages, from where I can return with history buttons back/forward and some of them are links to external page. Simple link.These links crashes my app with the error that I mentioned below. If I erase the next line:
WinJS.Utilities.query("a").listen("click", this.linkClickEventHandler, false);
from my js script, external links works, but I dont have anymore history buttons in my others's app pages.
You are trying to use the navigation framework to navigate to an external URI. It's usually meant to be used within the application's local context and pages that can contain 'fragments' to load up into your main nav control.
I wouldn't hook anchor tags with your function call, instead in your linkClickEventHandler I would do the following to only hook your internal links
WinJS.Utilities.query(".nav").listen("click", linkClickEventHandler, false);
in turn your internal links would be
click me
This approach only hooks the navigation framework into your internal links. Another approach is to inspect the 'this.href' in your handler and if it contains http:// or https:// then call window.open instead
What I am looking to do is:
1) From an MVC View, Start a long running Process. In my case, this process is a seperate Console Application being executed. The Console Application runs for potentially 30 minutes and regurlarily Console.Write's its current actions.
2) Back on the MVC View, periodically poll the server to retrieve the latest Standard Out which I have redirected to a Stream (or anywhere I can get access to it for that matter). I'll append newly retieved standard output to a log textbox or something equivalent.
Sounds relativly easy. My client side programming is a bit rusty though and I'm having issues with the actual streaming. I would assume this is not an uncommon task. Anyone got a decent solution for it in ASP.NET MVC?
Biggest issue seems to be that I cant get the StandardOutput until the end of execution, but I was able to get it with an event handler. Of course, using the event handler seems to lose focus of my output.
This is what I was working with so far...
public ActionResult ProcessImport()
{
// Get the file path of your Application (exe)
var importApplicationFilePath = ConfigurationManager.AppSettings["ImportApplicationFilePath"];
var info = new ProcessStartInfo
{
FileName = importApplicationFilePath,
RedirectStandardError = true,
RedirectStandardInput = true,
RedirectStandardOutput = true,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = false
};
_process = Process.Start(info);
_process.BeginOutputReadLine();
_process.OutputDataReceived += new DataReceivedEventHandler(_process_OutputDataReceived);
_process.WaitForExit(1);
Session["pid"] = _process.Id;
return Json(new { success = true }, JsonRequestBehavior.AllowGet);
}
void _process_OutputDataReceived(object sender, DataReceivedEventArgs e)
{
_importStandardOutputBuilder.Insert(0, e.Data);
}
public ActionResult Update()
{
//var pid = (int)Session["pid"];
//_process = Process.GetProcessById(pid);
var newOutput = _importStandardOutputBuilder.ToString();
_importStandardOutputBuilder.Clear();
//return View("Index", new { Text = _process.StandardOutput.ReadToEnd() });
return Json(new { output = newOutput }, "text/html");
}
I haven't written the client code yet as I am just hitting the URL to test the Actions, but I'm also interested how you would approach polling for this text. If you could provide the actual code for this too, it would be great. I would assume you'd have a js loop running after kicking off the process that would use ajax calls to the server which returns JSON results... but again, its not my forte so would love to see how its done.
Thanks!
Right, so from the couple of suggestions I received and a lot of trial and error I have come up with a work in progress solution and thought I should share with you all. There are definitely potential issues with it at the moment, as it relies on static variables shared across the website, but for my requirement it does the job well. Here goes!
Let's start off with my view. We start off by binding the click event of my button with some jquery which does a post to /Upload/ProcessImport (Upload being my MVC Controller and ProcessImport being my MVC Action). Process Import kicks off my process which I will detail below. The js then waits a short time (using setTimeout) before calling the js function getMessages.
So getMessages gets called after the button is clicked and it does a post to /Upload/Update (my Update action). The Update action basically retrieves the status of the Process and returns it as well as the StandardOutput since last time Update was called. getMessages will then parse the JSON result and append the StandardOutput to a list in my view. I also try to scroll to the bottom of the list, but that doesn't work perfectly. Finally, getMessages checks whether the process has finished, and if it hasn't it will recursivly call itself every second until it has.
<script type="text/javascript">
function getMessages() {
$.post("/Upload/Update", null, function (data, s) {
if (data) {
var obj = jQuery.parseJSON(data);
$("#processOutputList").append('<li>' + obj.message + '</li>');
$('#processOutputList').animate({
scrollTop: $('#processOutputList').get(0).scrollHeight
}, 500);
}
// Recurivly call itself until process finishes
if (!obj.processExited) {
setTimeout(function () {
getMessages();
}, 1000)
}
});
}
$(document).ready(function () {
// bind importButton click to run import and then poll for messages
$('#importButton').bind('click', function () {
// Call ProcessImport
$.post("/Upload/ProcessImport", {}, function () { });
// TODO: disable inputs
// Run's getMessages after waiting the specified time
setTimeout(function () {
getMessages();
}, 500)
});
});
</script>
<h2>Upload</h2>
<p style="padding: 20px;">
Description of the upload process and any warnings or important information here.
</p>
<div style="padding: 20px;">
<div id="importButton" class="qq-upload-button">Process files</div>
<div id="processOutput">
<ul id="processOutputList"
style="list-style-type: none; margin: 20px 0px 10px 0px; max-height: 500px; min-height: 500px; overflow: auto;">
</ul>
</div>
</div>
The Controller. I chose not to go with an AsyncController, mainly because I found I didn't need to. My original issue was piping the StdOut of my Console application to the view. I found couldn't ReadToEnd of the standard out, so instead hooked the event handler ProcessOutputDataReceived up which gets fired when standard out data is recieved and then using a StringBuilder, append the output to previously received output. The issue with this approach was that the Controller gets reinstantiated every post and to overcome this I decided to make the Process and the StringBuilder static for the application. This allows me to then receive a call to the Update Action, grab the static StringBuilder and effectivly flush its contents back to my view. I also send back to the view a boolean indicating whether the process has exited or not, so that the view can stop polling when it knows this. Also, being static I tried to ensure that if an import in in progress, don't allow other's to begin.
public class UploadController : Controller
{
private static Process _process;
private static StringBuilder _importStandardOutputBuilder;
public UploadController()
{
if(_importStandardOutputBuilder == null)
_importStandardOutputBuilder = new StringBuilder();
}
public ActionResult Index()
{
ViewData["Title"] = "Upload";
return View("UploadView");
}
//[HttpPost]
public ActionResult ProcessImport()
{
// Validate that process is not running
if (_process != null && !_process.HasExited)
return Json(new { success = false, message = "An Import Process is already in progress. Only one Import can occur at any one time." }, "text/html");
// Get the file path of your Application (exe)
var importApplicationFilePath = ConfigurationManager.AppSettings["ImportApplicationFilePath"];
var info = new ProcessStartInfo
{
FileName = importApplicationFilePath,
RedirectStandardError = true,
RedirectStandardInput = true,
RedirectStandardOutput = true,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = false
};
_process = Process.Start(info);
_process.BeginOutputReadLine();
_process.OutputDataReceived += ProcessOutputDataReceived;
_process.WaitForExit(1);
return Json(new { success = true }, JsonRequestBehavior.AllowGet);
}
static void ProcessOutputDataReceived(object sender, DataReceivedEventArgs e)
{
_importStandardOutputBuilder.Append(String.Format("{0}{1}", e.Data, "</br>"));
}
public ActionResult Update()
{
var newOutput = _importStandardOutputBuilder.ToString();
_importStandardOutputBuilder.Clear();
return Json(new { message = newOutput, processExited = _process.HasExited }, "text/html");
}
}
Well, that's it so far. It works. It still needs work, so hopefully I'll update this solution when I perfect mine. What are your thoughts on the static approach (assuming the business rule is that only one import can occur at any one time)?
Look into long poll. Basically you can open an ajax request and then hold onto it inside the controller.
Sample of long poll
This is something that you will want to do Async or you will possibly have issues with thread starvation.
Consider writing a service that runs on a server somewhere and pipes its output to a file/db accessible by your web server. Then you can just load the generated data in your website and returning them to your caller.
Understand that tying up your web server's threads for extended periods of time can result in thread starvation and make it look like your website has crashed (even though it's acutally just busy waiting for your console app to run).
I'm developing a web app with asp.net mvc 2. This app, has a controller with some asynchronous operations that return json or ajax... I call it by jquery and works fine!
My script is on the MasterPage, so this operations can be called by any View that inherits from this MasterPage.
My question is, How could I know ... what is the controller and action that are requesting the asynchronous operation?
I tried this:
if (this.RouteData.Values["controller"] == "Product" && this.RouteData.Values["action"] == "Index") {
}
but this get the current action (my assync action... or... "THIS" action!), I want the request.
I saw it because, if the request came from Home/Index or Home/Contact or Customer/Index or Product/Index my result of json can be diferent, so, I'd like to test what's the controller and action.
thanks!
---- Edited
It's a system of job monitoring of my customer. I do something like this:
//every second I get info in my assync action:
$(document).ready(function () {
var interval = window.setInterval(GetJobs, 1000);
});
function GetJobs() {
$.getJSON('<%=Url.Action("Index", "AssyncJob", new { area = "Admin"}) %>', function (r) {
/// ----------- Info in MasterPage (All views need it) ------------ //
// setup the time of server...
$("#time").html(r.time);
// setup the jobs are running... (
$("#running").html("");
if (r.jobcount == 1)
$("#running").html("There is one job running!");
else if(r.jobcount > 1)
$("#running").html(r.jobcount + " jobs running!");
/// ----------- Info in Home/Index ------------ //
if ($("#jobstoped")) { $("#jobstoped").html(r.jobstoped); }
// get a list of jobs... (in my action this info is in Cache)
if (r.jobs != null) {
$(r.jobs).each(function () {
if ($("#job" + this.id)) {
if (this.IsRunning) {
if (!$("#job" + this.id).hasClass("running")) {
$("#job" + this.id).addClass("running");
}
}
else if (this.IsStoped) {
if (!$("#job" + this.id).hasClass("stoped")) {
$("#job" + this.id).addClass("stoped");
}
}
else if (this.IsEnding) {
if (!$("#job" + this.id).hasClass("finished")) {
$("#job" + this.id).addClass("finished");
}
}
// --- there is a lot of info and rules that I fill for each job in list
}
});
}
});
}
I return some infos and works fine but I need to return the list of jobs only on Index action at Home controller, because this... I need to know what's the route are requesting the assync action ... to improve performace and avoid unnecessary information!
Well if you can help my... I would greatly appreciate it! =D
Thanks again!
If your JSON is going to be different depending on which route you have why not separate out the different routes into different actions, then you wouldn't have to do the check you are asking. It would make the code a lot cleaner and easier to read than having a bunch of if-else blocks in one action to determine which ActionResult to return to the view.