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

Cross build fs2.io for Scala.js #2453

Merged
merged 44 commits into from
Jul 11, 2021
Merged

Cross build fs2.io for Scala.js #2453

merged 44 commits into from
Jul 11, 2021

Conversation

armanbilge
Copy link
Member

@armanbilge armanbilge commented Jun 28, 2021

This PR lays the groundwork for cross-building fs2.io to Scala.js. My goal is that fs2.io on Scala.js will enable http4s ember on Scala.js in http4s/http4s#4938. I also hope this will unlock other interesting use-cases, such as command-line utilities that can take advantage of JS's fast startup times.

Specifically, I've separated out the typeclasses from the JVM-specific implementations (without compromising binary compatibility). This enables the typeclasses to be cross-built for Scala.js. A subset of the IoSuite is already passing tests on Scala.js.

At this point no JS-specific implementations are provided (besides some stubs). It remains an open question how best to simultaneously support different JS-platforms (primarily, browsers and Node.js). This work can be done piece-by-piece (net is higher priority than file at least for http4s) in follow-up PRs, or I'm also happy to continue here if so desired.

Thanks in advance for your reviews and feedback!

@armanbilge
Copy link
Member Author

armanbilge commented Jun 28, 2021

Alas, I was being naïve thinking wishfully that Websockets === Sockets. Seeing as they're not, implementing fs2.io for browsers is a no-go, and therefore Node.js would be the canonical platform, so I think it's okay to implement specifically for Node.js. Forthcoming ...

@armanbilge
Copy link
Member Author

Scala 3 builds are failing for now pending ScalablyTyped/Converter#202

@armanbilge armanbilge marked this pull request as draft July 5, 2021 15:17
@@ -104,13 +104,33 @@ ThisBuild / mimaBinaryIssueFilters ++= Seq(
ProblemFilters.exclude[ReversedMissingMethodProblem](
"fs2.io.net.tls.TLSContext.dtlsServerBuilder"
),
ProblemFilters.exclude[Problem]("fs2.io.net.tls.TLSEngine*")
ProblemFilters.exclude[Problem]("fs2.io.net.tls.TLSEngine*"),
ProblemFilters.exclude[MissingClassProblem]("fs2.io.net.Socket$IntCallbackHandler"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are all of these exclusions safe?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They should be. Before we merge I will go through one more time and double-check, I can add comment justifications as well for peace-of-mind.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

build.sbt Outdated
scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)),
Compile / npmDependencies += "@types/node" -> "16.0.0",
useYarn := true,
stOutputPackage := "fs2.js",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be fs2.io.js? Or fs2.io.jsdeps?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whatever you want, really! Happy to make it jsdeps.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, the reason I didn't put it in io ... the JS bindings could be useful outside of io?

Copy link
Member Author

@armanbilge armanbilge Jul 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After pondering a bit: I think the package should be fs2.internal.jsdeps and we should not expose these bindings in public APIs. There are lots of them, and because they are auto-generated by Scalably Typed which is technically still beta and they are defined by the Typescript community, I'm just scared whether we will be able to control their binary evolution. The primary purpose for adding these bindings was to help me build the internal implementations safely, not for public consumption.

For the couple places we do expose public APIs of JS-native objects, we can either:

  1. provide our own bindings (that we control bincompat of), or
  2. just accept js.Any (or a newtype wrapper around that) in the spirit of BYOB (bring-your-own-bindings)

@mpilquist thoughts?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to use ST for this the way forward would be to use https://scalablytyped.org/docs/library-developer to generate a library which could be published independently of fs2 releases. If you use very little I'd just copy/paste the necessary parts to be completely in control.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oyvindberg thanks for chiming in! I did find that guide very helpful when setting up the current build.

I'm still inclined to use the ST facades as internal API (it would be awesome if there's a way to automatically mark them as package-private?). I do appreciate the (continued) automation ST provides and would prefer not to chase down the copy/paste necessary to take control. Furthermore, the public APIs we provide in their place can be much more idiomatic: Chunk[Byte] instead of Buffer, callbacks A => F[B] instead of (A, B => Unit) => Unit, etc.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha, I just jumped in and assumed you used the default application mode - apologies.
In that case if it's fast enough to not upset anyone and marked as internal it's probably a good idea.

By package private, do you mean private[fs2] for every generated thing? I'd say that would be easy enough to add

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oyvindberg No reason to apologize for docs so good that I don't need your help 😉

Glad you agree! Yes, that's exactly what I mean by package private :)

def reads: Stream[F, Byte]

/** Indicates that this channel will not read more data. Causes `End-Of-Stream` be signalled to `available`. */
private[net] trait SocketPlatform[F[_]] {
def endOfInput: F[Unit]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Node doesn't support endOfInput but does support endOfOutput?

Copy link
Member Author

@armanbilge armanbilge Jul 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I stared at Node's Socket API for a very long time and couldn't find it.

https://nodejs.org/api/net.html#net_class_net_socket

This raises an interesting issue: for unsupported methods like this, should we

  1. make them platform-specific
  2. raise an exception
  3. do nothing

(1) Causes downstreams to experience the most pain (i.e. full cross) for what could otherwise be pure crosses. But, it makes authors aware of the missing method at compile-time. (2) and (3) still allow for a pure cross, but then there may be unhappy surprises at runtime.

As a motivating example, consider this seemingly innocuous ember-client code:
https://github.com/http4s/http4s/blob/main/ember-client/src/main/scala/org/http4s/ember/client/EmberConnection.scala#L30

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I think I'm all for (1). Still a bit painful, but downstreams can mitigate (1) by adding implicit syntax extensions to fill in the gaps however they like. Then, their code can purely cross-build.

@armanbilge
Copy link
Member Author

@mpilquist thank you very much for the review! Does the approval mean you are open to merging if I address your comments? I can follow up with the missing stuff (Files API and Unix sockets) in subsequent PRs.

@mpilquist
Copy link
Member

Generally okay with merging as long as you think we're in a releasable state -- I don't want to get stuck in a situation where the main branch is not releasable until some other PRs come in. Keep in mind binary compatibility too -- e.g., if we merge and then need to release a 3.0.next, then we're stuck with these APIs for a couple of years. Flip side is bincompat is easier to support on JS so maybe that's not a concern.

@armanbilge
Copy link
Member Author

armanbilge commented Jul 9, 2021

Makes sense. I think it's definitely releasable—there's no ??? gotchas, the missing features are just not there. But, happy to finish it up too. We're close!

Flip side is bincompat is easier to support on JS

I've been wondering about this ... what is bincompat for JS? People don't just drop-in JS files like Jars, they do some bundling stuff right so I assume it's always recompiling? UPDATE: ok, it looks like we check bincompat via the sjsir files.

I think the only new APIs I've introduced for JS are the TLSParameters. All other APIs were already public.

@armanbilge armanbilge marked this pull request as ready for review July 10, 2021 22:28
@armanbilge
Copy link
Member Author

I'm marking this PR as ready-for-review, although it is missing a few things I'd say it's in a "releasable" state.

Specifically, by "releasable" I mean I feel confident about the public APIs introduced. I'd be shocked if the internal implementations don't need some fine-tuning and/or re-working so in that sense I would say this is still experimental/beta. All the test suites are happy though.

What's missing:

  • a file API. As discussed in Discord I will probably introduce a new NodeFiles/PosixFiles typeclass which I think warrants its own PR/discussion.
  • Unix sockets. This is straightforward to implement but would be helped along by a nice file API.
  • DTLS. Not natively supported on Node.js.

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

Successfully merging this pull request may close these issues.

4 participants