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

Support "Service Loader" pattern/api in compiled JS #244

Open
niloc132 opened this issue Nov 21, 2023 · 1 comment
Open

Support "Service Loader" pattern/api in compiled JS #244

niloc132 opened this issue Nov 21, 2023 · 1 comment

Comments

@niloc132
Copy link
Member

This feature request is mainly in service of an excellent i18n experience, but there will be many other use cases it can support as well. The intended setup allows a project to have a (potentially transitive) dependency on some API that defines an interface for a service/resource, and then dependencies that offer implementations for that interface can be provided. Consumers of the interface need not know about which implementations will be available, and can rely on other features like a runtime lookup or compile-time "define" to let the underlying service loader mechanism select the appropriate one.

For internationalization, this separation and complete compilation is especially important:

  • A widget library might define an interface that each locale will provide, with only a default built-in locale present
  • Each widget will call some methods on the locale interface, to be satisfied at compile time
  • Other libraries will provide locale implementations
  • A project will depend on the widget library, and on one or more specific locale implementations. The project's build will produce an output for each --defined locale, and each localized output will omit strings from other locales (including default), except where appropriate.
  • A project might wish to only set one locale for development, and add the rest of the dependencies in a non-dev profile (as BUNDLE and BUNDLE_JAR will not remove unused locales)

Logging APIs might also be interested in using this.

It might also be important to support JVM use cases, as well as have a story for how to use this in GWT2 projects (likely a Generator that checks the value of a property at compile time, and emits an appropriate implementation).

Rough components to this:

Common pattern/implementation for the "service loader" itself

Our initial experiments suggested this would simply be a JsPropertyMap<T> and a static method to let each implementation self-register on startup, plus an api to call with a string to get the desired implementation. A wrapper type around this will append a known string prefix/suffix to these strings to ensure they are matched as part of this feature. Additionally, a runtime check should be performed to validate that the entire key is a valid JS identifier.

Closure-compiler

Better optimized output

Run a modified ConvertToDottedProperties compiler pass earlier than it runs today, only on reading and writing properties by string literal, and only if that string matches the required prefix/suffix.
See https://groups.google.com/g/closure-compiler-discuss/c/3Gsd73xdt1U

Multi-stage build

Running the "finalization" stage multiple times suggests that we should cache the output of the earlier stages, so that only the new --define-supplied constants need to be propagated through the program.

Additional support when defining a project

The j2cl-maven-plugin and related tools could provide some "permutation-like" features, computing the matrix of possible outputs.

@niloc132
Copy link
Member Author

The closure custom compiler pass was fairly easy to implement, but the optimization process in closure is quite different from GWT, and it is taking time to find the right way to incorporate the new pass into the existing set of passes - if the pass runs too early, we can't rely on "constantKeyName" + "$some$key$suffix" already being optimized to "constantKeyName$some$key$suffix" and inlining method calls to direct map access. On the other hand if it runs too late, we can't rely on the unused keys being optimized out. Even if we enforce a rule that all keys (i.e. locale names) must end with some specific key suffix, the earliest pass at inlining method params is still later than the last pass at optimizing out unused functions.

As with the original discussion, running the generated output through the compiler a second time is enough to finish optimizations, so it isn't a question of having unoptimizable code, just the particular order of the passes.

This could either require that we produce more tightly optimized JS/Java to begin with, or make bigger changes to the default pass config. Or, this is a red herring, and some other change to the type system needs to take place, to signal that the "map" actually only has static properties after this new pass is complete.

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

1 participant