Completely remove RxJS dependency from the RxPlayer's source code. #1193
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This is one of the final (our demo pages still contains some RxJS code) instalments of our recurring PR to remove RxJS from the RxPlayer code (after #916, #962, #1042, #1127, #1091, #1135, #1158 and #1167).
With that PR, RxJS stops being a dependency of the RxPlayer code (but stays a "devDependency" for our demo pages for now, though we may want to switch to the more popular React-Redux webapp architecture in the future for popularity sake, as copiability is also one of its goals).
Just to give a last reminder of why we're doing this:
To improve the approachability and readability of the RxPlayer code.
The lazyness, non-shared by default, multiple emissions (which isn't linked to completion), complex combination operators, schedulers configuration and implicit cancellation nature of RxJS Observables are all very interesting features yet hard to get into and very easy to mess up.
Forgetting to share may lead to multiple side-effects being performed when only one is needed (it happened more than once), looking why an Observable has been cancelled can be a very had task, handling Observables which may send events synchronously on subscription lead to a lot of special considerations (using specific schedulers if multiple subscriber exists, for one), lazyness make it harder to understand the performance of our application, and so on.
Evoked in the previous point: to make the cancellation logic explicit.
The RxPlayer performs a lot of asynchronous tasks (which is why we relied a lot on RxJS in the first place) which may have to be cancelled at some point (e.g. a request for a previously chosen quality, DRM negotiation when the content is changed buffer operations when the current track is changed etc.).
With RxJS, the idiomatic way of handling cancellation is by stop listening to the Observable, which is a very interesting concept theoretically but in our usecases was too implicit, leading sometimes to issues (task cancelled too soon or not cancelled soon enough, necessity to play with RxJS subjects or create new Observables to be able to get the right timing etc.).
Cancellation is now mostly handled by a
TaskCanceller
concept, which almost exactly copies theAbortController
browser object (we may use anAbortController
instead in the future? For now the main - and subtle - difference between the two is that theTaskCanceller
also automatically generates a niceCancellationError
on cancellation which can be very useful to communicate about cancellation through rejected promises).TaskCanceller
-linked tasks are manually cancelled through acancel
call, and cancelling operations (such as clean-up) is done through an event listener on the task-side, this is both explicit and idiomatic for a JavaScript developper.To improve debuggability, notably by making call stacks great again.
Due to lazyness and the operator-rich nature of RxJS, stack traces have never been exploitable. Improving those have been a subject for RxJS for a long time but by removing that library from the code, we can now obtain very inspectable call stacks
An important chunks of our bugs are related to our handling of RxJS, some even linked to RxJS issues (we had for example 2 memory leaks from RxJS bugs, a long-lived issue this year - now-fixed - which led to many migraines and RxJS versions rollbacks, and multiple other issues those last years).
Even if RxJS turns out with no bug in the future, its complexity is overkill for most of our use cases and in itself can lead to issues. Replacements in our situations can range from Promise to event emitters (or event-emitter likes, such as callbacks,
TaskCanceller
andSharedReference
), which are generally more understood and led for now to less issues.The closest replacement of RxJS Observables we have now in our code is what we called
SharedReference
which can be compared toBehaviorSubject
s: non-lazy structures wrapping some data, with the possibility to add an event listener to run some logic when that data changes. This is now the more advanced concept to grasp but it is still order of magnitudes simpler than RxJS Observables to get into.Observables was the default goto way of handling asynchronous data. Now, in most cases where there's only one emission, Promise are used instead.
Promises are easier to combine to many browser API already relying on Promises, profit from async/await syntax, and is much more understood by JavaScript developpers than RxJS Observables.
The main issue we could have with Promises is the obligation to have micro-tasks scheduled when awaiting them, but this is minor and generally lead to a more predictable flow.
The Stream part of the code now make heavy usage of callbacks given as parameters, used as event emitters.
This is the paradigm used instead of the more idiomatic EventTarget-like concept (with
addEventListener
andremoveEventListener
methods) to both allow synchronous events to be triggered right on call (without introducing astart
/stop
concept) and to better ensure - through TypeScript - that all events are either explicitely considered or skipped as all callbacks have to be defined on function call (which is a VERY nice feature). It also conveniently greatly facilitates bubbling up events, by just passing on to the corresponding callback, the reference to the parent callback to bubble-up too.