Using redux-saga with setInterval - how and when to yield - generator

Having just moved from thunks to sagas I'm trying to find the best way to call setTimeout and then from within that function call another function (in this case corewar.step()). This was my original code which works as I'd expect.
runner = window.setInterval(() => {
for(let i = 0; i < processRate; i++) {
corewar.step()
}
operations += processRate;
}, 1000/60)
This code is inside a saga and I believe that I should be able to wrap function calls within call as I've done in other areas in the application.
I've tried wrapping the setInterval call in a call and leaving everything else as it is, which results in step() never being called.
runner = yield call(window.setInterval, () => {
for(let i = 0; i < processRate; i++) {
corewar.step()
}
operations += processRate;
}, 1000/60)
I've tried, leaving the setInterval as it is and wrapping the step() function in a call and changing the anonymous function signature to function* which also results in step() never being called.
runner = window.setInterval(function*() {
for(let i = 0; i < processRate; i++) {
yield call([corewar, corewar.step])
}
operations += processRate;
}, 1000/60)
Finally, I've tried wrapping both, which again results in step() never being called.
runner = yield call(window.setInterval, function*() {
for(let i = 0; i < processRate; i++) {
yield call([corewar, corewar.step])
}
operations += processRate;
}, 1000/60)
It feels like I'm missing something here so my question is, should I need to wrap these functions up in call at all or is this wrong?
The follow on question if I am supposed to wrap the outer setInterval in a call would be how should I be defining a function as a parameter to call which also wants to yield either a put or call itself?

There is a section in the saga-redux docs called "Using the eventChannel factory to connect to external events", that suggests using channels.
This section is also providing an example for a setInterval implementation:
import { eventChannel, END } from 'redux-saga'
function countdown(secs) {
return eventChannel(emitter => {
const iv = setInterval(() => {
secs -= 1
if (secs > 0) {
emitter(secs)
} else {
// this causes the channel to close
emitter(END)
}
}, 1000);
// The subscriber must return an unsubscribe function
return () => {
clearInterval(iv)
}
}
)
}
You would then use yield call and yield takeEvery to set it up:
const channel = yield call(countdown, 10);
yield takeEvery(channel, function* (secs) {
// Do your magic..
});

const anotherSaga = function * () {
const runner = yield call(setInterval, () => {
console.log('yes');
}, 1000);
console.log(runner);
}
This works pretty fine for me. In your second snippet there is a double ) at the end where should be only one.

A little late to the party here but this is the top search result for the question of setting a timer in a saga. There's an alternate solution due to the nature of sagas. From here.
I adapted this so:
function* callSelfOnTimer({ value }) {
// Do your work here
...
// If still true call yourself in 2 seconds
if (value) {
yield delay(2000);
yield call(callSelfOnTimer, { value });
}
}

For this to work you also need to add this:
const delay = (ms) => new Promise(res => setTimeout(res, ms))
function* callSelfOnTimer({ value }) {
// Do your work here
...
// If still true call yourself in 2 seconds
if (value) {
yield delay(2000);
yield call(callSelfOnTimer, { value });
}
}

Related

Why does the ToolController's getPriority return 0 for my tool?

According to a prior SO answer, you can implement getPriority for a forge viewer Tool. And according to another SO answer extending the ToolInterface does not work. Hence, me not extending the ToolInterface implementing my Tool like so:
class MyCustomExtension extends Autodesk.Viewing.Extension {
constructor(viewer, options) {
super(viewer, options);
this.theiaUtil = new TheiaUtil(this);
}
getPriority() {
console.log("Theia#getPriority called! ", (this.getPriority && this.getPriority() || 0));
return 100000;
}
...
}
My tool's priority is returned as 0 in the ToolController, although it shouldn't:
function getPriority(tool)
{
return tool.getPriority instanceof Function && tool.getPriority() || 0;
}
I don't know why this function returns 0 as tool.getPriority instanceof Function returns true if I call MyCustomExtension.getPriority myself.
Note that ToolInterface is implemented like so:
function ToolInterface()
{
this.names = [ "unnamed" ];
this.getNames = function() { return this.names; };
this.getName = function() { return this.names[0]; };
this.getPriority = function() { return 0; };
this.register = function() {};
this.deregister = function() {};
this.activate = function(name, viewerApi) {};
this.deactivate = function(name) {};
this.update = function(highResTimestamp) { return false; };
this.handleSingleClick = function( event, button ) { return false; };
this.handleDoubleClick = function( event, button ) { return false; };
this.handleSingleTap = function( event ) { return false; };
this.handleDoubleTap = function( event ) { return false; };
// ...
}
Because of that, simply extending the ToolInterface class won't work because all these properties and functions added to the instance in the constructor will take precedence over your actual class methods. This is also likely the reason why you're seeing the priority value returned as zero - when you call myTool.getPriority(), you are not actually calling your getPriority method, but rather the default function which was assigned to this.getPriority in ToolInterface's constructor.
To work around this issue I would recommend explicitly deleting the corresponding fields in your class' constructor (something I explain in my blog post on implementing custom Forge Viewer tools):
class DrawTool extends Autodesk.Viewing.ToolInterface {
constructor() {
super();
this.names = ['box-drawing-tool', 'sphere-drawing-tool'];
// Hack: delete functions defined *on the instance* of the tool.
// We want the tool controller to call our class methods instead.
delete this.register;
delete this.deregister;
delete this.activate;
delete this.deactivate;
delete this.getPriority;
delete this.handleMouseMove;
delete this.handleButtonDown;
delete this.handleButtonUp;
delete this.handleSingleClick;
}
register() {
console.log('DrawTool registered.');
}
deregister() {
console.log('DrawTool unregistered.');
}
activate(name, viewer) {
console.log('DrawTool activated.');
}
deactivate(name) {
console.log('DrawTool deactivated.');
}
getPriority() {
return 42; // Or feel free to use any number higher than 0 (which is the priority of all the default viewer tools)
}
// ...
}
TL;DR: Activate the tool in button click event from a toolbar button instead of the extension's load method.
class MyExtension extends Autodesk.Viewing.Extension {
...
onToolbarCreated(toolbar) {
const MyToolName = 'My.Tool.Name'
let button = new Autodesk.Viewing.UI.Button('my-tool-button');
button.onClick = (e) => {
const controller = this.viewer.toolController;
if (controller.isToolActivated(MyToolName)) {
controller.deactivateTool(MyToolName);
button.setState(Autodesk.Viewing.UI.Button.State.INACTIVE);
} else {
controller.activateTool(MyToolName);
button.setState(Autodesk.Viewing.UI.Button.State.ACTIVE);
}
};
}
...
}
I activated the tool instantly after registering it in the Extension's load method. Petr Broz's github repo from his blog post loads the tool from a button in the toolbar. So I moved the activation of the tool to a button click in the toolbar which worked for me.

redux-saga: race between action and event channel

I want to race between a redux action and an event channel using redux-saga.
export function * createEventChannel () {
return eventChannel(emit => {
MyModule.addEventListener(MyModule.MY_EVENT, emit)
return () => { MyModule.removeEventListner(MyModule.MY_EVENT, emit)}
})
}
....
function * raceWithActionAndEvent() {
const channel = yield call(createEventChannel)
// I want to race between Redux Action: 'MY_ACTION' and channel here
}
This should do it:
export function* raceWithActionAndEvent() {
const channel = yield call(createEventChannel);
const winner = yield race({
channel: take(channel),
action: take(MY_ACTION),
});
if (winner.action) {
// then the action was dispatched first
}
if (winner.channel) {
// then the channel emitted first
}
}
In my opinion the code is quite readable. You set up a race between two takes and act on whichever wins.
Please note,createEventChannel doesn't need to be a generator function (like you have in the original question)

ES6 for of - is iterable evaluated only once?

Consider a simple for of:
for (const elem of document.getElementsByTagName('*') {
// do something with elem
}
does getElementsByTagName evaluated only once or on each iteration ?
thx!
In this case, it's evaluated once to obtain an iterable, which it then uses to obtain an iterator. It reuses that iterator to grab all the values and pass them to your for block. It's very similar to doing the following with a generator function:
function* getIntegers(max) {
for (let i = 0; i <= max; i++) {
yield i;
}
}
const iterator = getIntegers(15);
while (true) {
const { done, value } = iterator.next();
if (done) {
break;
}
console.log(value);
}
As noted by loganfsmyth, generator functions return an iterator directly. Note: generator functions can also be used with the for..of construct.
See this article on MDN for more info.

Aurelia update value of bound item in another class

I guess the question boils down how to i pass the instance of a property to another class.
I have something like this:
import timerClass from "./timer";
export class App {
constructor() {
this.timeLeft = 6; //<--- I want to update this
new timerClass(this.timeLeft);
}
activate() {
}
}
and
export default class {
constructor(time) {
this.initialTime = time;
setInterval(function () {
if (--time < 0) {
time = this.initialTime; //<--- From here
}
}, 1000);
}
}
Time is passed in but not reflected in the view when updated.
In knockout this was easy as all observables are functions an I could pass it round all over the place. How would i do the same here, should I wrap it in a function too?
When you call
new timerClass(this.timeLeft);
you pass your variable by value, i.e. the timer just gets 6 and there is no way to modify it there. The easiest way to fix this is indeed pass the callback function. I made it work with the following code.
timer.js:
export default class {
constructor(time, callback) {
this.initialTime = time;
this.currentTime = time;
setInterval(() => {
if (--this.currentTime < 0) {
this.currentTime = this.initialTime;
}
callback(this.currentTime);
}, 1000);
}
}
app.js:
constructor(){
this.timeLeft = 6;
var timer = new timerClass(this.timeLeft, v => this.timeLeft = v);
}
So I did some more reading and came across the aurelia-event-aggregator
http://aurelia.io/docs#the-event-aggregator
This allowed me to try a different angle. As my timer is eventually going to become a game loop this pub/sub way of doing it will work quite nicely.
Im still quite green with the syntax so I imagine its doing some things not entirely "best practice" but hope it helps someone.
main.js
import {inject} from 'aurelia-framework';
import {EventAggregator} from 'aurelia-event-aggregator';
import TimerClass from "./timer";
#inject(EventAggregator)
export class Main {
constructor(eventAggregator) {
this.eventAggregator = eventAggregator;
this.timer = new TimerClass(this.eventAggregator);
this.eventAggregator.subscribe('gameLoop', currentTime => {
this.timeLeft = currentTime
});
}
activate() {
this.timer.start();
}
}
timer.js
export default class Timer {
constructor(eventAggregator) {
this.eventAggregator = eventAggregator;
}
start(){
var initalTime = 5;
var currentTime = initalTime;
setInterval(() => {
if (--currentTime < 0) {
currentTime = initalTime;
}
this.eventAggregator.publish('gameLoop', currentTime);
}, 500);
}
}
main.html
<template>
<div>
<h2>Time Left:</h2>
<div>${timeLeft}</div>
</div>
</template>

How to obtain arguments.callee.caller?

I am trying to find out the name of the function that called my Google Apps Script function, by using arguments.callee.caller as in How do you find out the caller function in JavaScript?, but it seems there's no such property exported. (However, arguments.callee exists.)
How can I get that calling function's name in Google Apps Script?
As a secondary question, why isn't arguments.callee.caller there?
I made this function:
function getCaller()
{
var stack;
var ret = "";
try
{
throw new Error("Whoops!");
}
catch (e)
{
stack = e.stack;
}
finally
{
var matchArr = stack.match(/\(.*\)/g);
if (matchArr.length > 2)
{
tmp = matchArr[2];
ret = tmp.slice(1, tmp.length - 1) + "()";
}
return ret;
}
}
It throws as Error() and then gets the function name from the stack trace.
Try vary the '2' in matchArr[2] when using wrappers.
caller is a non-standard extension to JavaScript (that is, many browsers have it but it's not part of the EcmaScript standard) and not implemented in Apps Script.
I made a function to get the call stack based on jgrotius's answer:
function getCallStack()
{
var returnValue = "";
var framePattern = /\sat (.+?):(\d+) \((.+?)\)/;
try
{
throw new Error('');
}
catch (e)
{
returnValue = e.stack
.split('\n')
.filter(function(frame, index) {
return !frame.isBlank() && index > 0;
})
// at app/lib/debug:21 (getCaller)
.map(function(frame) {
var parts = frame.match(framePattern);
return {
file: parts[1],
line: parseInt(parts[2]),
func: parts[3]
};
});
}
return returnValue;
}
This is my updated version of the other two proposed solutions:
const getStacktrace = () => {
try {
throw new Error('')
} catch (exception) {
// example: at getStacktrace (helper:6:11)
const regex = /\sat (.+?) \((.+?):(\d+):(\d+)\)/
return exception
.stack
.split('\n')
.slice(1, -1)
.filter((frame, index) => {
return frame && index > 0
})
.map((frame) => {
const parts = frame.match(regex)
return {
function: parts[1],
file: parts[2],
line: parseInt(parts[3]),
column: parseInt(parts[4])
}
})
}
}
P.S.: please not that the regex has changed and also we are ignoring the first element of the stacktrace, since it is the getStacktrace function itself.