Webxr: Should we use promises for async APIs in WebXR?

Created on 9 Oct 2018  路  13Comments  路  Source: immersive-web/webxr

@toji has stated that the very critical timing of async APIs like hit-test and anchors - where the resolution must happen prior to a RAF for which the result is valid - means we should not use promises. This is because promises support "late-binding" - basically if I don't attach a .then() immediately and/or I store the promise for later use, a .then() attached to that promise may end up resolving at a time where the results are invalid. Furthermore, the promise API doesn't support rejecting a promise after it has already been resolved, so we don't have the ability to invalidate the promise after the critical time period for which it is valid has elapsed.

Two questions:
1) Is the above analysis correct? Must we use callbacks in our async APIs that have extremely specific timing for resolution.
2) Assuming the answer to (1) is "yes", should we avoid using promises entirely in the WebXR API to avoid confusion about what is and is not a promises / enforce consistency in the style of our asynchronous APIs.

Most helpful comment

Making the API "all promises" or "no promises" seems unwise. Break it down by call, and base it on how many times that call might want to return over the life of the application.

This is a pretty good summation of my thinking, actually. But I'll break it down even further:

  1. Don't be async when you don't have to be: We should be better about this, IMO. For example: Nell and I have discussed whether or not the Frame of Reference creation really needs to be async. I suspect that if we can identify a good "this is where the UA injects room config if it needs it" spot then they can indeed be synchronous, which is always the cleanest route IMO. (It would be nice if Layers and Frame of Reference had the same creation pattern. let xrThing = new XRThing(xrSession, etc);)

    1. Use callbacks or events for timing-critical data: Anything where a particular piece of data is only really useful for a frame or so really should be a callback or an event. Which of those to choose gets a little bit more fiddly, but I'd say that in general we should treat recurring time-sensitive data as events and time-sensitive data requests as callbacks (when it can't be synchronous, that is.) Frames are a special case because while it's valid to see them as recurring and thus events, we have received guidance from the Web Platform gurus at large that we should follow existing conventions for script-driven animation loops, which means requestAnimationFrame.

    2. Promises everywhere else: If there's no danger to seeing the data late but we still need to be asynchronous (most frequently to allow for UA intervention if needed) then promises should always be our default solution. I do view this as primarily a developer ergonomics matter, as well as simply trying to mesh with the direction of the web platform as a whole.

All 13 comments

1) This is in line with my understanding of the requirements and promises
2) IMO no: promises do enforce consistency in callback; their callbacks are executed as microtasks following the Task that resolve it, such that if the WebXR Device API can ensure outstanding requests are resolved during the rAF Task, then their callbacks will be executed after the rAF ends.

This is because promises support "late-binding" - basically if I don't attach a .then() immediately and/or I store the promise for later use, a .then() attached to that promise may end up resolving at a time where the results are invalid.

Is this in reference to the potential footgunning of having stale data? Maybe pedantic, but late binding a thennable doesn't change when the promise is resolved -- it is not resolved at a time where the results are invalid (it was resolved whether anyone was listening or not, although the thought of quantum promises are terrifying), but a callback was requested after the period in which the promise was valid. (Test: Promise.resolve() -> Promise聽{<resolved>: undefined}).

Not draining the promised hit results when requested seems to go directly against the spec and the validity -- is this an issue with communication (solvable via docs, spec-clarity?), or a technical constraint/path to guide a developer to a working solution (e.g. maybe accessing the hitMatrix on XRHitTestResult throws with a helpful error message, "XRHitTestResult only valid in origin render frame"?) Changing Promise/microtask semantics seems untractable, and the web platform has been avoiding callbacks for single request/response functions, would communication or more technical constraints alleviate hesitations?

These are good points, if I may summarize your opinion:

"We should use promises anyway because of the code ergonomics and rely on documentation and examples to guard against late binding."

I think that is a reasonable point of view. It is a question of whether avoiding promises is going too far out of our way to enforce a pit-of-success, especially since standardizing on promises may avoid a lot of confusion with our APIs.

As to your point of guarding against late binding with some sort of self-destructing data, I am not sure how viable that approach would be but it could be explored further.

Maybe related to #403 as well, but what are the scenarios for storing the promises in the first place, if not even their resolved values? For other prior art, synthetic events in React are only valid during their callback lifetime:

The SyntheticEvent is pooled. This means that the SyntheticEvent object will be reused and all properties will be nullified after the event callback has been invoked. This is for performance reasons. As such, you cannot access the event in an asynchronous way.

FWIW, I've been in favor of ditching promises for some things since we were talking about WebVR2. So you won't get arguments from me against ditching some promises. There are some async methods I can imagine (such as a "createAnchor" or "initializeImageTarget(image,....)") that might be best done with promises, so I'm not really in favor of getting rid of all promises though.

For example, I'm very much in favor of an API surface that allows the UA to enter/exit WebXR modes, vs those requests being only initiated by the UA. I would prefer if we had "onSessionCreation" as an event, for example, and have "requestSession" return immediately (or however long it takes for it to figure out if the request can be satisfied). Then, requestSession is more of a "this is what I'd prefer" and the UA can send in sessionCreation events when the session is initialized (perhaps via a link-transition, perhaps because the user initiated "immersive mode" via some other means, etc).

I'm also not really swayed by nitpicky programmer egonomics arguments, at the expense of significant functionality, I admit. I doubt that many programmers will talk to WebXR directly, and those that do (middleware authors) can deal with small changes in ergonomics.

Regarding self-destructing data: Having data that mutates seems markedly worse to me than just having stale data. I'm in favor of avoiding the "pit of success" thinking here and just taking the leap of faith that if devs request a hit test, then choose not the look the results of that hit test until later, they'll discover the cause of the issue pretty intuitively.

Regarding session creation, @blairmacintyre can you clarify a bit more what you mean by this?

an API surface that allows the UA to enter/exit WebXR modes, vs those requests being only initiated by the UA.

@tencircles sorry for lack of clarity.

The design of WebVR, and current WebXR, requires the page to initiate session creation and control when the switch between 2D and AR/VR happens. If a browser (UA) wanted to have a UI to allow users to trigger the switch between immersive and 2D, there is no current way to support that.

If the session creation flow used events instead of promises, we could have pages set things up like this:

  • create an event hander for session creation (onSessionCreate or something)
  • create a UI (if desired) for triggering session creation
  • requestSession doesn't return a promise; it just returns. At the point the browser would currently resolve the promise, it instead fires an event

The flow would now support both the promise-based page-initiated flow we have now, but ALSO support a UA triggered flow. Which gives more flexibility for the future, IMHO.

(FWIW, I've brought this up repeatedly since 2016, during the initial WebVR -> WebVR2 discussions, to little avail, but I'll continue to mention it, because I think it's important).

No worries at all, I just wanted to be sure I understood you correctly before replying. Thanks for clarifying @blairmacintyre, very much appreciated.

I'm in favor of the model described above. However it seems like what's happening in this case is not really requesting a session, but rather defining what type of session you'd like once the user initiates it. This makes me wonder if this wouldn't be better done in a more declarative manner. A good analogue here would be providing multiple source elements to an HTMLMediaElement. Not suggesting this model, but it bears considering how this might look on the API level.

It seems to me like if it's just going to return undefined, then perhaps we'd want to simply allow users to call XRSession as a constructor and add a readyState property. Attempts to call methods or access properties only available within a session prior to the user entering immersive would throw. This approach also conforms to your suggestion to add an enterimmersive event (or whatever it's named). The primary difference here is that you'd enable users to pass options to a constructor rather than to a "side-effecty" requestSession method.

I realize we're derailing a bit here from the question of "should we use promises or not?" question, so I'll just include my answer here to clarify.

  1. Promises should be used whenever possible, but I'd say individual cases need individual evaluations.
  2. Callbacks should be avoided. I haven't seen here an explanation or argument for how these "solve" the issue of stale data. In any case, I think it's preferable to have stale data than an inconsistent API surface.
  3. As Blair points out, not everything needs to return a promise. However, I'd prefer to lean more in the direction of values being returned, and less in the direction of side-effecty methods which don't clearly indicate to the developer what's actually being done. The benefit of the current approach is that it's very intuitive to have a requestSession method which returns a promise which resolves with a session. A requestSession that returns undefined, seems less preferable because it's hard to make a case that what you're doing there is actually requesting a session.

I'm not in favor of mixing this API with declarative concepts right now.

I agree that what I describe does, in fact, change the semantics from "requesting a session" to "requesting the features of a session". The options passed in are just that, the features you want.

For me, the difference between promises and not promises is essentially the answer to the question "Can this request EVER need to return more than once?".

For session creation, the answer to that is "Yes". That happens, for example, if a user enters immersive mode and then exits and then wants to go back in. Adding UA-initiated "enter immersive mode" is on top of this.

For other commands, like creating an anchor or doing a hit test, the answer is "No." these are one-shot commands that don't return immediately, so promises are perfect.

Making the API "all promises" or "no promises" seems unwise. Break it down by call, and base it on how many times that call might want to return over the life of the application.

Making the API "all promises" or "no promises" seems unwise. Break it down by call, and base it on how many times that call might want to return over the life of the application.

Sounds good. Just want to clarify one point (sorry for not clarifying earlier), does the "no promises" solution here involve extending the Event interface or are we talking about some other callback-based solution? e.g. Early WebAudio/Geolocation style callbacks which have the signature (success, error, [options])

That's my assumption, yes. But, @toji should clarify what he was thinking.

Making the API "all promises" or "no promises" seems unwise. Break it down by call, and base it on how many times that call might want to return over the life of the application.

This is a pretty good summation of my thinking, actually. But I'll break it down even further:

  1. Don't be async when you don't have to be: We should be better about this, IMO. For example: Nell and I have discussed whether or not the Frame of Reference creation really needs to be async. I suspect that if we can identify a good "this is where the UA injects room config if it needs it" spot then they can indeed be synchronous, which is always the cleanest route IMO. (It would be nice if Layers and Frame of Reference had the same creation pattern. let xrThing = new XRThing(xrSession, etc);)

    1. Use callbacks or events for timing-critical data: Anything where a particular piece of data is only really useful for a frame or so really should be a callback or an event. Which of those to choose gets a little bit more fiddly, but I'd say that in general we should treat recurring time-sensitive data as events and time-sensitive data requests as callbacks (when it can't be synchronous, that is.) Frames are a special case because while it's valid to see them as recurring and thus events, we have received guidance from the Web Platform gurus at large that we should follow existing conventions for script-driven animation loops, which means requestAnimationFrame.

    2. Promises everywhere else: If there's no danger to seeing the data late but we still need to be asynchronous (most frequently to allow for UA intervention if needed) then promises should always be our default solution. I do view this as primarily a developer ergonomics matter, as well as simply trying to mesh with the direction of the web platform as a whole.

@blairmacintyre @lincolnfrog, do you feel like this has adequately addressed the original issue? (@blairmacintyre, if you want to explore enabling a UA to have a button to cause a page to enter immersive XR, can we use a dedicated issue for it?) If so, I'm inclined to close this one.

Sounds like the initial issue is resolved and Blair filed #426 to cover the other issue that was raised.

Was this page helpful?
0 / 5 - 0 ratings