Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updating onConnect() to a new API #293

Open
kylebakerio opened this issue Sep 9, 2021 · 19 comments
Open

Updating onConnect() to a new API #293

kylebakerio opened this issue Sep 9, 2021 · 19 comments
Assignees

Comments

@kylebakerio
Copy link
Member

kylebakerio commented Sep 9, 2021

I also feel like having onConnect be called if the global is present is just a terrible API and that should definitely be moved to an event or an exposed promise on the NAF global... I cringe every time I see that.

Ah ah I also find it weird that a global function named onConnect would be called like that out of the blue. :)
We have this API like forever so we can't really change that without breaking code used in the wild.

This is done here

if (this.hasOnConnectFunction()) {
this.callOnConnect();
}

callOnConnect: function() {
NAF.connection.onConnect(window[this.data.onConnect]);
},

The global onConnect is the callback function called on the "connected" event you have on document.body, see

onConnect(callback) {
this.onConnectCallback = callback;
if (this.isConnected()) {
callback();
} else {
document.body.addEventListener('connected', callback, false);
}
}

So you could register your own listener instead of relying on the global onConnect, it's just not documented. And you need to be sure you call your function if you are already connected like it's done above.

I'd propose
1a) Update the schema to not just take a function name, but an actual function (either a string that we convert into a function to eval or, if set by JS, can just be an actual function)
1b) Alternatively, update the schema to just not accept this at all
1c) Either way, include a deprecation warning if we detect user trying to use old API, a la THREE
2) Change the event to be more tightly scoped--other libraries could easily use "connected" as an event as well. Perhaps "naf-connected" should be the new event?
2a) We can continue to leave in the old "connected" event for now, along with a deprecation warning when we emit it.
2b) When we call the built-in global, if it exists, we should also emit a deprecation warning.
3) Update docs and examples to use this new pattern.
4) Update version to e.g. 0.8.4 reflect the new API we are encouraging, and update the version to 0.9.x whenever we later remove the old API to reflect the potentially breaking API update. Relying on a generically named global like this just seems like bad practice that we should actively go out of our way to remove

also, because of this

And you need to be sure you call your function if you are already connected like it's done above.

The listener should be set before NAF script is declared, yeah?

Thoughts?

@vincentfretin
Copy link
Member

Sorry if I don't reply to all issues and PR right away in the coming weeks. Don't worry, I'll reply, probably the weekend. :)

@kylebakerio
Copy link
Member Author

@vincentfretin no worries, I'm also pretty busy. It's fine having it async and just moving along when it moves. I'll just keep doing my thing, get to it when you can.

@vincentfretin
Copy link
Member

1a) Update the schema to not just take a function name, but an actual function (either a string that we convert into a function to eval or, if set by JS, can just be an actual function)
1b) Alternatively, update the schema to just not accept this at all
1c) Either way, include a deprecation warning if we detect user trying to use old API, a la THREE

eval is not a good idea, and there are security implications of using eval on an unknown string. This is worse than the global onConnect. :)
I don't think the aframe component schema accepts functions, it only accept primitive types like number, vec3, selector as far as I'm aware. Now that I'm thinking about it, this is probably why we have such weird api where you can change the function name to be executed with networked-scene="onConnect: myOwnFunction" and in networked-scene it will execute window[this.data.onConnect]() that is calls window.myOwnFunction().

I think we already have enough deprecation warnings and breakage in THREE. If we can avoid breakage in aframe libraries that are not related to THREE, I think we developers using those libs would be happier. :D
Also per my previous paragraph, there doesn't seem to be a good alternative to what we have now.

  1. Change the event to be more tightly scoped--other libraries could easily use "connected" as an event as well. Perhaps "naf-connected" should be the new event?
    2a) We can continue to leave in the old "connected" event for now, along with a deprecation warning when we emit it.
    2b) When we call the built-in global, if it exists, we should also emit a deprecation warning.

From reading the code, I think you can just use today

document.body.addEventListener('connected', myOwnFunction)

It should work properly even with networked-scene="connectOnLoad:true"
In NetworkConnection.js onConnect the

  if (this.isConnected()) { 
    callback(); 
  } else...

is executed only if you try to connect a second time I think?
You can do some tests to be sure, put a console.log there and emit('connect') several times in an example.

  1. Update docs and examples to use this new pattern.

Well, in the end, having

function myOnConnect () { ...}
document.body.addEventListener('connected', myOnConnect);

(You absolutely need to have something other that onConnect otherwise it will be executed twice, if we don't change the networked-scene API)

instead of

function onConnect () { ... }

I don't see much value to change the examples.

  1. Update version to e.g. 0.8.4 reflect the new API we are encouraging, and update the version to 0.9.x whenever we later remove the old API to reflect the potentially breaking API update. Relying on a generically named global like this just seems like bad practice that we should actively go out of our way to remove

For me it's not a good reason to introduce a breaking change. The current API is no so bad. :)

@vincentfretin
Copy link
Member

In the documentation we currently have
| onConnect | Function to be called when client has successfully connected to the server. | onConnect |
We should at least modify it to say it's actually a string with the function name that should be available globally. PR welcome.

@kylebakerio
Copy link
Member Author

eval is not a good idea, and there are security implications of using eval on an unknown string.

We are not running eval on some other user's text input... There's no reason to be paranoid about eval(). If a would-be hacker has the ability to set onconnect attribute, they (1) have the ability to call whatever JS they want, and (2) could just as (if not more) easily set a global onConnect() function.

This is worse than the global onConnect. :)

I disagree. Non-namspaced globals are a terrible idea, and a global named onConnect is sufficiently generically named that it has a reasonably high chance of collision with no protection from some other given library, or even the user accidentally shadowing it themselves.

That said, I get why it's mildly unpleasant, but it's a result of the A-Frame decision to allow declaration of everything within HTML attributes, which is inherently awkward. If you take a second to think about it, this is actually more or less identical to the existing onclick HTML attribute API, which also has to be run via eval.

So, to my point: this is consistent with both existing HTML and A-Frame precedent.

I don't think the aframe component schema accepts functions, it only accept primitive types like number, vec3, selector as far as I'm aware.

There are many more types than that, and you can declare custom component schema inputs 1. I've used this custom component schema to accept JSON in another component I wrote, and the same principle could be used to eval function inputs instead.

I think we already have enough deprecation warnings and breakage in THREE. If we can avoid breakage in aframe libraries that are not related to THREE, I think we developers using those libs would be happier. :D

The fact that THREE abuses the logs is not an argument for us to not use them reasonably. Until THREE fixes their log issue, which they've finally acknowledged they should, I recommend all users silence THREE logs using a snippet similar to this:

// TO TURN OFF THREEJS LOGS
vrgc.console = {
  // log: console.log,
  info: console.info,
  warn: console.warn,
  error: console.error,
  counters: {

  }
};

Object.keys(vrgc.console)
.forEach(
  key => {
    console.warn(`hiding console.${key} calls from THREE`)
    vrgc.console.counters[key] = 0;

    console[key] = function() {
      if ( (typeof arguments[ 0 ] === 'string') && (arguments[ 0 ].substr( 0, 5 ) === 'THREE') ) {
          // ignore noisy THREE, but check here if something breaks.
          vrgc.console.counters[key]++
          return
      } 
      else {
        vrgc.console[key].apply( console, arguments );
      }
    }
  }
);

Meanwhile, we're pre 1.0, and this is a very easy 'breaking change' to detect, notify, and for devs to fix. There's no reason for this to be painful at all.

All this said, if you don't see why this needs changing, I'm not sure what to say. To me it's clear, and the first time I saw this, I was pretty shocked and it made it looked very half-baked and like a very undeveloped library. Again:

Change the event to be more tightly scoped--other libraries, or projects themselves, could easily defined and use (and collide with) a "connected" as an event as well. Perhaps "naf-connected" should be the new event?

@vincentfretin
Copy link
Member

I agree I may have been in the "eval is evil" position. After reading your reply and some comments with the link you gave, I think it's ok. You pointed out we could do a custom field type for aframe schema, I didn't do the search, I didn't know that.

I absolutely agree with you on the global namespace pollution and potential collision.

I'm not convinced on writing all your connect function code as a string. It depends on what you want to do in this callback, but it may be several 10 or 50 lines of code. If instead of all those lines of code, you just use your function name, you come back of using a function in the global namespace. Writing code in a string usually means you don't have your IDE recognize that as code and so you don't have the syntax errors, linting etc.

I would be much in favor of something like this:

function myOnConnect () { ...}
const scene = document.querySelector('a-scene');
scene.components["networked-scene"].onConnectCallback = myConnectFunc;

or

document.body.addEventListener('naf-connected', myOnConnect);

For the "connected" event emitted on document.body, I didn't consider the potential collision issue with other libs. I agree we could rename it to "naf-connected".

I know we are on pre 1.0.0 version and with semantic versioning we can introduce breaking changes in 0.x.0.
A renaming such as "connected" to "naf-connected" seems trivial but some libraries may depend on this "connected" event. All libraries maintainers do not update their lib right away and as a developer you may end up blocked to update naf version because of other libraries didn't update their code yet, or you need to fork those libraries to fix the issue yourself.

@vincentfretin
Copy link
Member

If we're going to break compatibility with aframe < 1.3.0 because of #280 then let's do all those breaking changes, properly document those in the release notes and add a warning in the README we don't support aframe < 1.3.0 and you need to use naf 0.8.x for older aframe versions.

@kylebakerio
Copy link
Member Author

kylebakerio commented Oct 7, 2021

I'm not convinced on writing all your connect function code as a string. It depends on what you want to do in this callback, but it may be several 10 or 50 lines of code. If instead of all those lines of code, you just use your function name, you come back of using a function in the global namespace. Writing code in a string usually means you don't have your IDE recognize that as code and so you don't have the syntax errors, linting etc.

So, to be clear, I'm not suggesting writing the function as a string should be the primary way of doing this, just that we enable that for consistency. What I mean is that you can declare an onclick within HTML, as well as within JS:

<div id="mydiv" onclick="doSomething()"></div>

vs.

// assuming doSomething = () => { /* stuff */ }
const div = document.querySelector('#mydiv')
// sadly, this HTML interface also requires adding a string, for consistency:
div.setAttribute('onclick','doSomething()')
// this is nicer, though:
div.onclick = doSomething

The normal way would be to do the function declaration in JS, just as I believe that while one can declare networked-scene in HTML, one probably usually wants to do so in JS. As a result, what I'm saying is that while you should be able to do this:

<a-scene networked-scene="onconnect: alert('connected'); room: default; audio:true"></a-scene>
// ^this matches the HTML onclick pattern, but we could also do this:
<a-scene networked-scene="onconnect: () => {alert('connected')}; room: default; audio:true"></a-scene>

ideally, you would do this:

const scene = document.querySelector('a-scene')
scene.setAttribute('networked-scene', { 
  onconnect: function() { alert('connected') },
  room: 'default',
  audio: true,
}

// in theory, I would also propose the custom attribute should allow this:
scene.setAttribute('networked-scene','onconnect',() => { alert('connected') })
// but that would frankly not make much sense in this specific context, since you would specify onconnect's behavior at the time you add networked-scene to a-scene, and it wouldn't generally safely be expected fire if you added it later for some reason.

A-Frame has this pattern of being able to specify stuff in HTML with a bunch of stuff that gets parsed as strings. I'm just saying we match that A-Frame pattern. If you want to do a 50-line onConnect function, no problem, just specify it in a component and assign it whenever you're ready to add the networked-scene component and do it all in JS--I assure you, that's what I would do. The string pattern will still be useful for some quick debug stuff, though.

@vincentfretin
Copy link
Member

Ok I understand now the consistency with the onclick attribute you want to achieve.
I also somehow forgot we could have a onConnect property that can take a real function or a string, similar to vec3 where you can give a string or object for example.

Note that because onConnect is not an html attribute but a component property, it will be quite limited because of the A-Frame parsing of the whole component string value and usage of semicolon as a separator for properties, see examples below.

If we want consistency, first please note that

<button onclick="myFunction">Click me</button>

does nothing.

<button onclick="myFunction()">Click me</button>

does something. The attribute value is evaluated when the user click the button.

Similarly

<button onclick="() => {alert('connected')}">Click me</button>

does nothing.

<button onclick="(() => {alert('connected')})()">Click me</button>

shows the alert dialog.

You can play with the onclick attribute here: https://www.w3schools.com/jsref/event_onclick.asp

The syntax

<a-scene networked-scene="onConnect: () => {alert('connected')}; room: default; audio:true"></a-scene>

is quite limited because you can't use semicolon in onConnect, so you're limited to only one instruction in your function (and without putting the semicolon).
And also it should do nothing because the function is not called. If we want to have the same semantic as onclick, it should really be

<a-scene networked-scene="onConnect: (() => {alert('connected')})(); room: default; audio:true"></a-scene>

or simply like your first example

<a-scene networked-scene="onConnect: alert('connected'); room: default; audio:true"></a-scene>

eval("(() => {alert('connected')})()") or eval("alert('connected')") works.
If you configured CSP in your page, be aware that you may need to tweak your CSP rules to allow execution of eval otherwise you get:

VM43:1 Uncaught EvalError: Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src chrome://resources chrome://test 'self' 'unsafe-inline' https:".

As a developer I would maybe want

<a-scene networked-scene="onConnect: (() => {alert('connected'); console.log('connected')})(); room: default; audio:true"></a-scene>

I tested that now, I thought it would give me an error, but it just fail to parse silently without errors.

<a-scene networked-scene="room: default; audio:true"></a-scene>

gives

document.querySelector('a-scene').components['networked-scene'].data
{room: 'basic-audio', debug: true, adapter: 'easyrtc', audio: true, serverURL: '/', …}
document.querySelector('a-scene').components['networked-scene'].data
{room: 'basic-audio', onConnect: "(() => {alert('connected')})()", debug: true, adapter: 'easyrtc', audio: true, …}

gives

{room: 'basic-audio', onConnect: "(() => {alert('connected')})()", debug: true, adapter: 'easyrtc', audio: true, …}

Adding semicolon after the alert instruction:

<a-scene networked-scene="onConnect: (() => {alert('connected');})(); room: default; audio:true"></a-scene>

gives

document.querySelector('a-scene').components['networked-scene'].data
"\n      room: basic-audio;\n      onConnect: (() => {alert('connected');})();\n      debug: true;\n      adapter: easyrtc;\n      audio: true;\n    "

If you want more that one instruction in your function, you end up using:

<script>
const doSomething = () => { alert('connected'); console.log("really connected!"); }
</script>
<a-scene networked-scene="onConnect: doSomething(); room: default; audio:true"></a-scene>

In comparison we have this today without parentheses:

<a-scene networked-scene="onConnect: doSomething; room: default; audio:true"></a-scene>

The following should work

const scene = document.querySelector('a-scene')
scene.setAttribute('networked-scene', { 
  onConnect: "alert('connected'); console.log('connected')",
  room: 'default',
  audio: true,
}

but we agree that it's meaningless to write it like that, you would better write:

const scene = document.querySelector('a-scene')
scene.setAttribute('networked-scene', { 
  onConnect: () => { alert('connected'); console.log('connected'); },
  room: 'default',
  audio: true,
}

So, with all the above said, I don't see much value changing the behavior of onConnect with string value to be a string evaluated instead of a global function with the specified name that get executed if found. I only see all potential issues the developer could have and bug reports created. :)

I'm in favor of keeping the onConnect behavior like it is today and add support for setting a function with the js syntax in the last code snippet.

@vincentfretin
Copy link
Member

About the other proposal

<a-scene networked-scene="connectOnLoad:true; room: default; audio:true"></a-scene>
<script>
document.body.addEventListener('naf-connected', doSomething);
</script>

I'm afraid we could have a race condition similar to #267 because the connection logic is called at networked-scene component initialization, and the 'naf-connected' listener may not have been registered yet.

@kylebakerio
Copy link
Member Author

I'm aware of how the native html onclick works (that it executes the string, etc.). I hope that's clear from my examples and proposal.... My point was that we don't have to have a perfect replica of onclick, though we can.

Good point about the semicolons, which esssentially become overloaded here. (Too bad aframe didn't require something more clear for separating properties in strings). So, yes, to properly match the native onclick style, we'd need to actually register a primitive--then we wouldn't have the semicolon limitation. That's a significant change to our API, though, and I do think that's overkill and unnecessary for just this case (though it would clearly be the cleanest option--maybe something to consider in the future if we look to 1.0, but really not a priority).

As a result, yeah, I would understand ditching the string eval option. More opportunity to be confused by the semicolon parsing limitation than it would help if we include that in the docs.

I'm in favor of keeping the onConnect behavior like it is today and add support for setting a function with the js syntax in the last code snippet.

I am still in favor of removing the call to global method option--if we do leave it until we switch to 1.0 at some point, then at least we should unpublish it as a recommended option and make it silent only for backwards compatibility. My take:

  • referring to a naked global like that is messy / bad practice
  • global option has a reasonable chance of collision
  • you could just declare the naf-connected listener before you add networked-scene just as easily as you can declare a global
  • use connectOnLoad:false if you must declare it after adding networked-scene
  • ideally, add support for adding with JS syntax, for the clearest option imo

(And yes, to avoid a race condition, you should absolutely declare your listener before networked-scene is added to your a-scene)

@vincentfretin
Copy link
Member

We can set onConnect property to empty string by default here

onConnect: {default: 'onConnect'},
I'm ok with that. You will need to set it explicitly if you want to use the global onConnect function, yes.

About the race condition, if you declare your listener and after set networked-scene with javascript, you will have no issue, of course.
Here I'm talking about setting your listener before <a-scene networked-scene> in html, networked-scene init can still be executed before registering the listener I think. This is the race I had with NAF.schemas.add, although it was declared in the head, sometimes it was executed too late, after the networked init included in the html in the body. I tested with

diff --git a/examples/basic-audio.html b/examples/basic-audio.html
index 2444e2d..fde6d98 100644
--- a/examples/basic-audio.html
+++ b/examples/basic-audio.html
@@ -15,7 +15,15 @@
     <script src="/js/spawn-in-circle.component.js"></script>
   </head>
   <body>
+    <script>
+      function doSomething () {
+        console.log("onConnect", "hello");
+      }
+      document.body.addEventListener('connected', doSomething);
+    </script>
     <a-scene networked-scene="
+      connectOnLoad: true;
+      onConnect: '';
       room: basic-audio;
       debug: true;
       adapter: easyrtc;

I couldn't produce an issue though, but it doesn't mean it can't happen.

Anyway, I don't think we want to promote this particular usage.
I think we should document this way:

diff --git a/examples/basic-audio.html b/examples/basic-audio.html
index 2444e2d..2b24ea4 100644
--- a/examples/basic-audio.html
+++ b/examples/basic-audio.html
@@ -16,6 +16,8 @@
   </head>
   <body>
     <a-scene networked-scene="
+      connectOnLoad: true;
+      onConnect: doSomething;
       room: basic-audio;
       debug: true;
       adapter: easyrtc;
@@ -141,5 +143,10 @@
         });
       }
     </script>
+    <script>
+      function doSomething () {
+        console.log("onConnect", "hello");
+      }
+    </script>
   </body>
 </html>

or fully in javascript

const scene = document.querySelector('a-scene')
scene.setAttribute('networked-scene', { 
  onConnect: () => { console.log("onConnect", "hello"); },
  room: 'default',
  audio: true,
}

and not promote a third way of doing things. Do you agree?

@kylebakerio
Copy link
Member Author

kylebakerio commented Oct 9, 2021

So, I'm still pretty strongly against the idea of declaring and calling globals, like this:

diff --git a/examples/basic-audio.html b/examples/basic-audio.html
index 2444e2d..2b24ea4 100644
--- a/examples/basic-audio.html
+++ b/examples/basic-audio.html
@@ -16,6 +16,8 @@
   </head>
   <body>
     <a-scene networked-scene="
+      connectOnLoad: true;
+      onConnect: doSomething;
       room: basic-audio;
       debug: true;
       adapter: easyrtc;
@@ -141,5 +143,10 @@
         });
       }
     </script>
+    <script>
+      function doSomething () {
+        console.log("onConnect", "hello");
+      }
+    </script>
   </body>
 </html>

But if declaring it in JS like we've agreed upon above isn't enough, how about this as an option: we expose a promise on the NAF global, e.g., NAF.isConnected. Then devs can write code like this:

NAF.connection.active.then(doSomething)
// or
(async () => {
  await NAF.connection.active
  doSomething()
})()

Behind the scenes, this is very simple to implement in NAF. This:

  • enables having multiple code blocks awaiting the promise without overlapping
  • prevents race conditions, because if the promise has already completed, then the code will just fire immediately
  • doesn't use any unnecessary globals

how do you feel about that?

@kylebakerio
Copy link
Member Author

kylebakerio commented Oct 10, 2021

So, I read through some of the NAF source code on this, and realized I hadn't noticed the full power of NAF.connection.onConnect() before. I still don't think it's ideal, but I think it's better than the current advice to add a global function.

in the newest draft of the pull request, I show a monkey patch on NAF to add a new promise called NAF.connection.active:

NAF.connection.active = new Promise((rs, rj) => { NAF.connection.onConnect(rs)})

With that patch in place, we can do this:

NAF.connection.active.then(() => {console.log('NAF connected at', new Date())})

In theory, we could instead just use this function right now, no monkey patch needed, directly:

NAF.connection.onConnect(() => {console.log('2. NAF connected at', new Date())})

This already enables adding multiple functions that get called upon connection, and eliminates race conditions... with an important caveat: you must call NAF.connection.onConnect() after document.body exists.

So, my real recommendation is we add this to NAF itself, and make this promise style the new standard:

NAF.connection.active.then(() => {console.log('1. NAF connected at', new Date())})
// or, in async function
await NAF.connection.active
console.log('NAF connected at', new Date())

The benefit is that the existing method

  1. cannot be called before the body is declared; promise would not have this limitation
  2. only removes the last method added from the connected event listener on the body
  3. is a somewhat atypical api for a JS lib these days
  4. promises feature better error handling and clearer stacks than events

And besides that... await NAF.connection.active is just super clean, no? ;)

@vincentfretin
Copy link
Member

I think beginner developers may like to use a global onConnect function, it's just simple. No need to learn about closure and Promise and async/await to start tinkering with NAF. If someone doesn't like global, they can use one of the other syntaxes that doesn't need to use a global.

You can indeed give a callback to NAF.connection.onConnect today, I'm actually using this in my own code although not documented, I used this with easyrtc adapter because it wasn't possible for me to use a global onConnect function.

NAF.connection.onConnect(() => {                                              
 console.log("connected');
});

For a Promise based API, I like better the API used by the janus adapter.

scene.components["networked-scene"].connect().then(() => {                      
        console.log("connected');                                         
      })                                                                              
      .catch((connectError) => {
        console.log(connectError);
      });
    }

or the equivalent with async/await. No change needed in networked-scene component, it's the adapter connect function that needs to return a Promise.

@kylebakerio
Copy link
Member Author

kylebakerio commented Oct 10, 2021

I think beginner developers may like to use a global onConnect function, it's just simple. No need to learn about closure and Promise and async/await to start tinkering with NAF.

This is a networking library, in JS, for VR. I think it's reasonable to expect a dev to have the most basic understanding of how a promise works. There is no uniquely special need to understand scope or closure here. It's just:

init: async function() {
    await NAF.connection.active
    doSomething()
}

That's it. It's just a safer, more consistent, more reliable, more powerful option. No race conditions, no name collisions, no fancy features. You don't even have to understand how it works, you just declare your function as async, and copy/paste await NAF.connection.active, and everything after that works, or if you prefer promise syntax, you just use NAF.connection.active.then(), which is honestly more readable, predictable, and understandable than having some random magic-name global... and setting global functions is considered harmful.12345

It's ok if you're quickly debugging to throw a global around, but the idea that a library reaches for some random global function and calls it is kind of crazy, and I just don't understand how I have to justify this. I don't know of any other reputable library that has this anti-pattern baked in and listed on the front page readme. That's the kind of thing you do when you're throwing something together, not something you do when you're thinking it out and doing it right.

For a Promise based API, I like better the API used by the janus adapter.

I mean, that's great and all, but is just a different scenario, as well as looking much messier. For one, the user shouldn't call connect() repeatedly, so they would have to store this promise somewhere if they wanted to have more than one thing depending on the connection to initiate. Second, they have to initiate the connection in order to wait on it... What if they don't want to initiate it? Then they have to write their own workaround to encapsulate the promise. Finally, they have to go through scene.components["networked-scene"].connect, instead NAF.connection.active, which would be better organization.

I think the Janus adapter's API there is great, and it should exist, but it's not a solution or an alternative to what I'm talking about here.

And NAF.connection.onConnect() is nice, but it's a weird standard, doesn't remove its event handlers correctly if multiple callbacks are passed in, and throws an error if you try to call it before document.body is initiated (which is a huge caveat, since one is going to want to declare connection listening code in A-Frame components in init(), and components need to be declared before inits....).

@vincentfretin
Copy link
Member

I'm not against a Promise API like you proposed, but this would mean a fourth way of doing things if we keep the others we talked about.
We discussed a long time about the eval string where it allowed usage of globals functions, this is why I don't understand you're strongly against the global function, even if it's the choice of the developer.
Are you suggesting to completely remove the two current ways we currently have with onConnect (as a string property and callback) and also not implementing the third way with setting onConnect with a function via setAttribute('networked-scene', ... ?
And doing only your promise based API?
I'm ok with that too, I already agreed we can have breaking changes.

@kylebakerio
Copy link
Member Author

We discussed a long time about the eval string where it allowed usage of globals functions, this is why I don't understand you're strongly against the global function

If you go back and read, I never actually supported the use of a string pointing to a global function, though you did respond suggesting that. That would indeed be just allowing the same global function we have now, just with allowing custom names. If we must keep a global function, I'd suggest it be within the NAF namespace; e.g. NAF.connection.callOnConnect = function(){} (or whatever name).

I didn't comment on this suggestion of the string pointing to a global function's name because we decided to use the string because of the semicolon parsing issue--but to be clear, yes, I'm not a fan of the global function name string eval option either. Naked globals just not a good choice across the board imo, I'm fully consistent on this issue.

Are you suggesting to completely remove the two current ways we currently have with onConnect (as a string property and callback) and also not implementing the third way with setting onConnect with a function via setAttribute('networked-scene', ... ?

My suggestions are this:

  1. remove the existing onConnect() global (alt: at least changing that to be within the NAF global)
  2. add this promise API, for advanced usage and for internal usage
  3. update NAF.connection.onConnect() to use that promise internally, so that it doesn't throw an error if added before document.body. we can leave this method there, but I recommend we use it internally. we also should then remove the event-listener based code.
  4. remove the connect event. it's more error-prone for a dev to make sure the listener is called at the right time, better to just have the promise. (alt: at least change it to naf-connect.)
  5. add the JS setAttribute('networked-scene' method, and recommend that as the primary developer interface. (The promise API exists if they want to have multiple dependencies in multiple places, want to declare networked-scene in the HTML and not in JS, and also for NAF usage internally).

@vincentfretin
Copy link
Member

Ok then, please proceed to the changes in a separate PR. Other tasks that needs to be done we also discussed here:

  • update README.md to point to the new API
  • update docs/RELEASE_NOTES.md to explain how to upgrade your project from old to new API (I will copy the content to github release textarea when I'll release the new version)
  • adapt and fix the tests, add new ones if necessary
  • update examples to use the new API

@kylebakerio kylebakerio self-assigned this Oct 13, 2021
vincentfretin added a commit that referenced this issue Oct 31, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants