Xstate: Cleanup non-cancellable promise on user cancel

Created on 14 Mar 2020  路  5Comments  路  Source: davidkpiano/xstate

My use case is around getting an audio stream (microphone) in a browser and let the user cancel anytime. There is a basic call to navigator.mediaDevices.getUserMedia() which returns a promise that cannot be canceled. Once it resolves, the browser shows an indication of the active microphone. To deactivate arbitrary code has to be called on the resolved stream object.

The getUserMedia() is fast in theory, but for cases, it isn't and a user wants to cancel sooner, it should be allowed. You can compare it to doing a mistake dial on the phone and wanting to end it sooner than it starts connecting.

https://codesandbox.io/s/hopeful-wu-6cuty

It works correctly, but it feels overly complex given how simple tasks it does. Maybe it's just my laziness talking and this is totally cool approach 馃し鈥嶁檪

Edit: Now I realize I shouldn't probably create it as a bug 馃槄

documentation question

All 5 comments

You should rather create a machine (or an actor, maybe a callback one) wrapping that interface and "cancel" it by sending an event to it - rather than trying to call imperatively a stop method on it.

Um, that feels even weirder to me. This machine is already pretty specific and narrow in what it does. Splitting it to extra machine doesn't feel to have much of a benefit.

With an actor, you mean like https://xstate.js.org/docs/guides/communication.html#invoking-callbacks? I thought about that, but either I don't understand it correctly or it will call cleanup whenever I leave that state. That's not desirable in cases where user did not cancel.

And now I am realizing another weakness of this pattern. I cannot use services config this way which would useful for mocking in tests.

entry: assign(() => ({
  streamPromise: getAudioStream(),
})) as any,
invoke: {
  id: 'stream',
  src: ctx => ctx.streamPromise!,
}

I understand that it's quite specific, but you have a machine (business logic~) integrating with some non-machine entity here (which doesn't need to know about any business logic). The split makes sense because you just have to wrap that non-machine entity into a common interface (an actor) which should be able to handle its internal state based on received commands and you wouldn't have to worry about promise/stream/whatever stuff in your business unit. It's arguably more boilerplate but IMHO it makes complete sense.

With an actor, you mean like https://xstate.js.org/docs/guides/communication.html#invoking-callbacks?

Yes, but as with any other actor you can use spawn instead of the invoke and those won't be cleaned up automatically. Only upon receiving an event. With spawn you control the lifetime of an actor.

Thank you @Andarist, I did rewrite it with actor and spawning and it does feel less convoluted. And it's also nice to be able to have spawning action in the config so I can override it in tests.

https://codesandbox.io/s/charming-firefly-txu85

There is some TypeScript issue on line 85, I don't consider it a big problem, but if you want, I can open a new issue with it.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

hnordt picture hnordt  路  3Comments

mattiamanzati picture mattiamanzati  路  3Comments

bradwoods picture bradwoods  路  3Comments

suku-h picture suku-h  路  3Comments

doup picture doup  路  3Comments