diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSPromise.java b/jso/apis/src/main/java/org/teavm/jso/core/JSPromise.java new file mode 100644 index 000000000..5a3d9890a --- /dev/null +++ b/jso/apis/src/main/java/org/teavm/jso/core/JSPromise.java @@ -0,0 +1,124 @@ +/* + * Copyright 2023 Bernd Busse. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.jso.core; + +import org.teavm.interop.NoSideEffects; +import org.teavm.jso.JSBody; +import org.teavm.jso.JSFunctor; +import org.teavm.jso.JSMethod; +import org.teavm.jso.JSObject; +import org.teavm.jso.JSProperty; +import org.teavm.jso.util.function.JSConsumer; +import org.teavm.jso.util.function.JSFunction; +import org.teavm.jso.util.function.JSSupplier; + +/** + * Interface for interacting with JavaScript + * + * {@code Promise}s. + * + * @param The type this promise returns when resolving successfully. + */ +public abstract class JSPromise implements JSObject { + private JSPromise() { + } + + /** Interface for a function wrapped by a promise. */ + @FunctionalInterface + @JSFunctor + public interface Executor extends JSObject { + /** + * @param resolveFunc Call this function to resolve with success value. + * @param rejectFunc Call this function to reject with error value. + */ + void onExecute(JSConsumer resolveFunc, JSConsumer rejectFunc); + } + + @JSBody(params = "executor", script = "return new Promise(executor);") + @NoSideEffects + public static native JSPromise create(Executor executor); + + @JSBody(params = "promises", script = "return Promise.any(promises);") + @NoSideEffects + public static native JSPromise any(JSArrayReader> promises); + + // TODO: Allow passing differently typed promises via a JSTuple interface + @JSBody(params = "promises", script = "return Promise.all(promises);") + @NoSideEffects + public static native JSPromise> all(JSArrayReader> promises); + + // TODO: Allow passing differently typed promises via a JSTuple interface + @JSBody(params = "promises", script = "return Promise.allSettled(promises);") + @NoSideEffects + public static native JSPromise>> + allSettled(JSArrayReader> promises); + + @JSBody(params = "promises", script = "return Promise.race(promises);") + @NoSideEffects + public static native JSPromise race(JSArrayReader> promises); + + @JSBody(params = "value", script = "return Promise.resolve(value);") + @NoSideEffects + public static native JSPromise resolve(V value); + + @JSBody(params = "reason", script = "return Promise.reject(reason);") + @NoSideEffects + public static native JSPromise reject(Object reason); + + /** Call {@code onFulfilled} with the success value, resolving with its return value. */ + public abstract JSPromise then(JSFunction onFulfilled); + + /** Call {@code onFulfilled} with the success value or {@code onRejected} with the reject reason, + * resolving with its return value. */ + public abstract JSPromise then(JSFunction onFulfilled, JSFunction onRejected); + + /** Call {@code onFulfilled} with the success value, returning a new promise. */ + @JSMethod("then") + public abstract JSPromise flatThen(JSFunction> onFulfilled); + + /** Call {@code onFulfilled} with the success value or {@code onRejected} with the reject reason, + * returning a new promise. */ + @JSMethod("then") + public abstract JSPromise flatThen(JSFunction> onFulfilled, + JSFunction> onRejected); + + /** Call {@code onRejected} with the reject reason, resolving with its return value. */ + @JSMethod("catch") + public abstract JSPromise catchError(JSFunction onRejected); + + /** Call {@code onRejected} with the reject reason, returning a new promise. */ + @JSMethod("catch") + public abstract JSPromise flatCatchError(JSFunction> onRejected); + + /** Call {@code onFinally} after settling, ignoring the return value. */ + @JSMethod("finally") + public abstract JSPromise onSettled(JSSupplier onFinally); + + /** Interface for the return values of {@ref #allSettled()}. */ + public interface FulfillmentValue extends JSObject { + @JSProperty + @NoSideEffects + JSString getStatus(); + + @JSProperty + @NoSideEffects + T getValue(); + + @JSProperty + @NoSideEffects + Object getReason(); + } +} diff --git a/jso/apis/src/main/java/org/teavm/jso/util/function/JSConsumer.java b/jso/apis/src/main/java/org/teavm/jso/util/function/JSConsumer.java new file mode 100644 index 000000000..b4b2b1b1f --- /dev/null +++ b/jso/apis/src/main/java/org/teavm/jso/util/function/JSConsumer.java @@ -0,0 +1,35 @@ +/* + * Copyright 2023 Bernd Busse. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.jso.util.function; + +import java.util.Objects; +import org.teavm.jso.JSFunctor; +import org.teavm.jso.JSObject; + +@FunctionalInterface +@JSFunctor +public interface JSConsumer extends JSObject { + void accept(T t); + + default JSConsumer andThen(JSConsumer after) { + Objects.requireNonNull(after); + + return (T t) -> { + this.accept(t); + after.accept(t); + }; + } +} diff --git a/jso/apis/src/main/java/org/teavm/jso/util/function/JSFunction.java b/jso/apis/src/main/java/org/teavm/jso/util/function/JSFunction.java new file mode 100644 index 000000000..8e9c410ec --- /dev/null +++ b/jso/apis/src/main/java/org/teavm/jso/util/function/JSFunction.java @@ -0,0 +1,50 @@ +/* + * Copyright 2023 Bernd Busse. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.jso.util.function; + +import java.util.Objects; +import org.teavm.jso.JSFunctor; +import org.teavm.jso.JSObject; + +@FunctionalInterface +@JSFunctor +public interface JSFunction extends JSObject { + R apply(T t); + + default JSFunction andThen(JSFunction after) { + Objects.requireNonNull(after); + + return (T t) -> { + R result = this.apply(t); + return after.apply(result); + }; + } + + default JSFunction compose(JSFunction before) { + Objects.requireNonNull(before); + + return (V v) -> { + T result = before.apply(v); + return this.apply(result); + }; + } + + static JSFunction identity() { + return (T t) -> { + return t; + }; + } +} diff --git a/jso/apis/src/main/java/org/teavm/jso/util/function/JSSupplier.java b/jso/apis/src/main/java/org/teavm/jso/util/function/JSSupplier.java new file mode 100644 index 000000000..31caef3d3 --- /dev/null +++ b/jso/apis/src/main/java/org/teavm/jso/util/function/JSSupplier.java @@ -0,0 +1,25 @@ +/* + * Copyright 2023 Bernd Busse. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.jso.util.function; + +import org.teavm.jso.JSFunctor; +import org.teavm.jso.JSObject; + +@FunctionalInterface +@JSFunctor +public interface JSSupplier extends JSObject { + R get(); +} diff --git a/samples/promise/build.gradle.kts b/samples/promise/build.gradle.kts new file mode 100644 index 000000000..bf82c2e26 --- /dev/null +++ b/samples/promise/build.gradle.kts @@ -0,0 +1,41 @@ +/* + * Copyright 2023 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + java + war + id("org.teavm") +} + +configurations { + create("war") +} + +dependencies { + teavm(teavm.libs.jsoApis) + "war"(project(":stdout-helper", "war")) +} + +teavm.js { + addedToWebApp = true + obfuscated = false + mainClass = "org.teavm.samples.promise.PromiseExample" +} + +tasks.war { + dependsOn(configurations["war"]) + from(provider { configurations["war"].map { zipTree(it) } }) +} diff --git a/samples/promise/src/main/java/org/teavm/samples/promise/PromiseExample.java b/samples/promise/src/main/java/org/teavm/samples/promise/PromiseExample.java new file mode 100644 index 000000000..5b08d5049 --- /dev/null +++ b/samples/promise/src/main/java/org/teavm/samples/promise/PromiseExample.java @@ -0,0 +1,423 @@ +/* + * Copyright 2024 Bernd Busse. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.samples.promise; + +import java.util.Arrays; +import org.teavm.jso.JSBody; +import org.teavm.jso.browser.Window; +import org.teavm.jso.core.JSArray; +import org.teavm.jso.core.JSPromise; +import org.teavm.jso.core.JSString; +import org.teavm.jso.util.function.JSConsumer; +import org.teavm.jso.util.function.JSFunction; +import org.teavm.jso.util.function.JSSupplier; + +public final class PromiseExample { + private static long start = System.currentTimeMillis(); + + private PromiseExample() { + } + + public static void main(String[] args) throws InterruptedException { + report(Arrays.toString(args)); + report(""); + + checkFunctionalInterface(); + + runSimplePromise(); + + runComplexPromise(); + + final var lock = new Object(); + runLongRunningPromise(lock); + synchronized (lock) { + report("Lock acquired"); + lock.wait(20000); + } + + combinePromises(lock); + synchronized (lock) { + report("Lock acquired"); + lock.wait(20000); + } + + testNativePromises(lock); + + report("Finished main thread"); + } + + private static void report(String message) { + var current = System.currentTimeMillis() - start; + System.out.println("[" + Thread.currentThread().getName() + "]/" + current + ": " + message); + } + + private static void checkFunctionalInterface() { + JSSupplier supplier = () -> 23; + + JSFunction addTwenty = value -> value + 20; + JSFunction subTwenty = value -> value - 20; + JSFunction isPositive = value -> value >= 0; + + JSConsumer print = value -> report("My value: " + value.toString()); + JSConsumer print2 = value -> report("My value plus 10: " + Integer.valueOf(value + 10).toString()); + + var value = supplier.get(); + report("Supplied value: " + value.toString()); + + value = addTwenty.apply(value); + report("Value plus 20: " + value.toString()); + + value = subTwenty.apply(value); + report("Value minus 20: " + value.toString()); + + var value2 = isPositive.apply(value); + report("Value is positive: " + value2.toString()); + + var subFourty = subTwenty.andThen(subTwenty); + value = subFourty.apply(value); + report("Value minus 40: " + value.toString()); + + var plusFourty = addTwenty.compose(addTwenty).andThen(addTwenty.compose(subTwenty)); + value = plusFourty.apply(value); + report("Value plus 40: " + value.toString()); + + value2 = subFourty.andThen(isPositive).apply(value); + report("Value minus 40 is positive: " + value2.toString()); + + print.accept(value); + var printExtended = print.andThen(print2); + printExtended.accept(value); + } + + private static void runSimplePromise() { + var promise = JSPromise.create((resolve, reject) -> { + report("Simple promise execution"); + + report("Resolving with 'success'"); + resolve.accept("success"); + }); + } + + private static void runComplexPromise() { + var promise = JSPromise.create((resolve, reject) -> { + report("Complex promise execution"); + + report("Resolving with 'step1'"); + resolve.accept("step1"); + }) + .then(value -> { + report("Resolved with '" + value + "'"); + report("... and resolve with 'step2'"); + return "step2"; + }) + .then(value -> { + report("Resolved with '" + value + "'"); + report("... and throw exception"); + throw new RuntimeException("Exception in promise handler"); + }, reason -> { + report("Failed unexpectedly with reason: " + reason.toString()); + return reason.toString(); + }) + .then(value -> { + report("Resolved unexpectedly with '" + value + "'"); + return value; + }, reason -> { + report("Failed expectedly with reason: " + reason.toString()); + return reason.toString(); + }) + .flatThen(value -> { + report("Resolved with '" + value + "'"); + report("... and resolve with resolved promise"); + return JSPromise.resolve("step3"); + }) + .flatThen(value -> { + report("Resolved with '" + value + "'"); + report("... and resolve with rejected promise"); + return JSPromise.reject("step4"); + }) + .catchError(reason -> { + report("Catched reject reason '" + reason.toString() + "'"); + return reason.toString(); + }) + .flatThen(value -> { + report("Resolved with '" + value + "'"); + report("... and resolve with new promise"); + return JSPromise.create((resolve, reject) -> { + report("Inner promise"); + report("Reject with 'step from inner'"); + reject.accept("step from inner"); + }); + }) + .then(value -> { + report("Resolved unexpectedly with '" + value + "'"); + return value; + }) + .catchError(reason -> { + report("Catched reject reason '" + reason.toString() + "'"); + return reason.toString(); + }) + .onSettled(() -> { + report("Promise has finally settled"); + return null; + }); + } + + private static void runLongRunningPromise(Object lock) { + var promise = JSPromise.create((resolve, reject) -> { + report("Long promise exection"); + report("Wait for a while..."); + Window.setTimeout(() -> { + report("... and resolve with 'done'"); + resolve.accept("done"); + }, 2000); + }).then(value -> { + report("Resolved with '" + value + "'"); + synchronized (lock) { + lock.notify(); + } + return value; + }); + } + + private static void combinePromises(Object lock) throws InterruptedException { + JSArray> promises = JSArray.create(3); + + report("Start 3 successful promises"); + promises.set(0, JSPromise.resolve("success1")); + promises.set(1, JSPromise.resolve("success2")); + promises.set(2, JSPromise.resolve("success3")); + + var allPromises = JSPromise.all(promises); + allPromises.then(value -> { + report("All promises resolved to: " + value); + return "success"; + }, reason -> { + report("At least one promise rejected with: " + reason.toString()); + return "failure"; + }).onSettled(() -> { + synchronized (lock) { + lock.notify(); + } + return null; + }); + + synchronized (lock) { + report("Lock acquired"); + lock.wait(20000); + } + + report("Start 1 successful and 2 rejected promise"); + promises.set(0, JSPromise.resolve("success1")); + promises.set(1, JSPromise.reject("failure2")); + promises.set(2, JSPromise.reject("failure3")); + + allPromises = JSPromise.all(promises); + allPromises.then(value -> { + report("All promises resolved to: " + value); + return "success"; + }, reason -> { + report("At least one promise rejected with: " + reason.toString()); + return "failure"; + }).onSettled(() -> { + synchronized (lock) { + lock.notify(); + } + return null; + }); + + synchronized (lock) { + report("Lock acquired"); + lock.wait(20000); + } + + var settledPromises = JSPromise.allSettled(promises); + settledPromises.then(value -> { + report(Integer.toString(value.getLength()) + " promises settled to:"); + for (int i = 0; i < value.getLength(); ++i) { + var item = value.get(i); + var msg = "-- Promise " + i + " " + item.getStatus() + " with: "; + if (item.getStatus().stringValue().equals("fulfilled")) { + msg += item.getValue().toString(); + } else if (item.getStatus().stringValue().equals("rejected")) { + msg += item.getReason().toString(); + } + report(msg); + } + return "success"; + }).onSettled(() -> { + synchronized (lock) { + lock.notify(); + } + return null; + }); + + synchronized (lock) { + report("Lock acquired"); + lock.wait(20000); + } + + var anyPromise = JSPromise.any(promises); + anyPromise.then(value -> { + report("At least one promise resolved to: " + value); + return "success"; + }, reason -> { + report("All promises rejected with: " + reason.toString()); + return "failure"; + }).onSettled(() -> { + synchronized (lock) { + lock.notify(); + } + return null; + }); + + synchronized (lock) { + report("Lock acquired"); + lock.wait(20000); + } + + report("Start 3 rejected promises"); + promises.set(0, JSPromise.reject("failure1")); + promises.set(1, JSPromise.reject("failure2")); + promises.set(2, JSPromise.reject("failure3")); + + anyPromise = JSPromise.any(promises); + anyPromise.then(value -> { + report("At least one promise resolved to: " + value); + return "success"; + }, reason -> { + report("All promises rejected with: " + reason.toString()); + return "failure"; + }).onSettled(() -> { + synchronized (lock) { + lock.notify(); + } + return null; + }); + + synchronized (lock) { + report("Lock acquired"); + lock.wait(20000); + } + + report("Start 3 delayed promises"); + promises.set(0, JSPromise.create((resolve, reject) -> Window.setTimeout(() -> resolve.accept("success1"), 200))); + promises.set(1, JSPromise.create((resolve, reject) -> Window.setTimeout(() -> reject.accept("failure1"), 100))); + promises.set(2, JSPromise.create((resolve, reject) -> Window.setTimeout(() -> resolve.accept("success3"), 50))); + + anyPromise = JSPromise.race(promises); + anyPromise.then(value -> { + report("First settled promise resolved to: " + value); + return "success"; + }, reason -> { + report("First settled promise rejected with: " + reason.toString()); + return "failure"; + }).onSettled(() -> { + synchronized (lock) { + lock.notify(); + } + return null; + }); + + synchronized (lock) { + report("Lock acquired"); + lock.wait(20000); + } + + report("Start 3 delayed promises"); + promises.set(0, JSPromise.create((resolve, reject) -> Window.setTimeout(() -> resolve.accept("success1"), 200))); + promises.set(1, JSPromise.create((resolve, reject) -> Window.setTimeout(() -> reject.accept("failure1"), 50))); + promises.set(2, JSPromise.create((resolve, reject) -> Window.setTimeout(() -> resolve.accept("success3"), 100))); + + anyPromise = JSPromise.race(promises); + anyPromise.then(value -> { + report("First settled promise resolved to: " + value); + return "success"; + }, reason -> { + report("First settled promise rejected with: " + reason.toString()); + return "failure"; + }).onSettled(() -> { + synchronized (lock) { + lock.notify(); + } + return null; + }); + } + + private static void testNativePromises(Object lock) throws InterruptedException { + report("Get promise from native method"); + var nativePromise = getNativePromise(JSString.valueOf("success from native")); + nativePromise.then(value -> { + report("Native resolved expectedly with '" + value + "'"); + return value; + }, reason -> { + report("Native rejected unexpectedly with '" + reason.toString() + "'"); + return reason.toString(); + }).onSettled(() -> { + synchronized (lock) { + lock.notify(); + } + return null; + }); + + synchronized (lock) { + report("Lock acquired"); + lock.wait(20000); + } + + nativePromise = getNativeRejectingPromise(JSString.valueOf("failure from native")); + nativePromise.then(value -> { + report("Native resolved unexpectedly with '" + value + "'"); + return value; + }, reason -> { + report("Native rejected expectedly with '" + reason.toString() + "'"); + return reason.toString(); + }).onSettled(() -> { + synchronized (lock) { + lock.notify(); + } + return null; + }); + + synchronized (lock) { + report("Lock acquired"); + lock.wait(20000); + } + + report("Pass promise to native method"); + handlePromise(JSPromise.create((resolve, reject) -> { + resolve.accept(JSString.valueOf("Resolved from Java")); + })); + + handlePromise(JSPromise.create((resolve, reject) -> { + reject.accept(JSString.valueOf("Rejected from Java")); + })); + } + + @JSBody(params = "msg", script = "return new Promise((resolve, reject) => {" + + " setTimeout(() => resolve(msg), 500);" + + "});") + private static native JSPromise getNativePromise(JSString msg); + + @JSBody(params = "msg", script = "return new Promise((resolve, reject) => {" + + " setTimeout(() => reject(msg), 500);" + + "});") + private static native JSPromise getNativeRejectingPromise(JSString msg); + + @JSBody(params = "promise", script = "promise.then(" + + " (value) => console.log('success:', value)," + + " (reason) => console.log('failure:', reason));") + private static native void handlePromise(JSPromise promise); +} diff --git a/samples/promise/src/main/webapp/WEB-INF/web.xml b/samples/promise/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 000000000..6471cd77a --- /dev/null +++ b/samples/promise/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,21 @@ + + + + \ No newline at end of file diff --git a/samples/promise/src/main/webapp/highlight.pack.js b/samples/promise/src/main/webapp/highlight.pack.js new file mode 100644 index 000000000..1225a13e2 --- /dev/null +++ b/samples/promise/src/main/webapp/highlight.pack.js @@ -0,0 +1 @@ +!function(e){"undefined"!=typeof exports?e(exports):(window.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return window.hljs}))}(function(e){function n(e){return e.replace(/&/gm,"&").replace(//gm,">")}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0==t.index}function a(e){var n=(e.className+" "+(e.parentNode?e.parentNode.className:"")).split(/\s+/);return n=n.map(function(e){return e.replace(/^lang(uage)?-/,"")}),n.filter(function(e){return N(e)||/no(-?)highlight/.test(e)})[0]}function o(e,n){var t={};for(var r in e)t[r]=e[r];if(n)for(var r in n)t[r]=n[r];return t}function i(e){var n=[];return function r(e,a){for(var o=e.firstChild;o;o=o.nextSibling)3==o.nodeType?a+=o.nodeValue.length:1==o.nodeType&&(n.push({event:"start",offset:a,node:o}),a=r(o,a),t(o).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:o}));return a}(e,0),n}function c(e,r,a){function o(){return e.length&&r.length?e[0].offset!=r[0].offset?e[0].offset"}function c(e){l+=""}function u(e){("start"==e.event?i:c)(e.node)}for(var s=0,l="",f=[];e.length||r.length;){var g=o();if(l+=n(a.substr(s,g[0].offset-s)),s=g[0].offset,g==e){f.reverse().forEach(c);do u(g.splice(0,1)[0]),g=o();while(g==e&&g.length&&g[0].offset==s);f.reverse().forEach(i)}else"start"==g[0].event?f.push(g[0].node):f.pop(),u(g.splice(0,1)[0])}return l+n(a.substr(s))}function u(e){function n(e){return e&&e.source||e}function t(t,r){return RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var c={},u=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");c[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof a.k?u("keyword",a.k):Object.keys(a.k).forEach(function(e){u(e,a.k[e])}),a.k=c}a.lR=t(a.l||/\b[A-Za-z0-9_]+\b/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=t(a.i)),void 0===a.r&&(a.r=1),a.c||(a.c=[]);var s=[];a.c.forEach(function(e){e.v?e.v.forEach(function(n){s.push(o(e,n))}):s.push("self"==e?a:e)}),a.c=s,a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var l=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=l.length?t(l.join("|"),!0):{exec:function(){return null}}}}r(e)}function s(e,t,a,o){function i(e,n){for(var t=0;t";return o+=e+'">',o+n+i}function d(){if(!w.k)return n(y);var e="",t=0;w.lR.lastIndex=0;for(var r=w.lR.exec(y);r;){e+=n(y.substr(t,r.index-t));var a=g(w,r);a?(B+=a[1],e+=p(a[0],n(r[0]))):e+=n(r[0]),t=w.lR.lastIndex,r=w.lR.exec(y)}return e+n(y.substr(t))}function h(){if(w.sL&&!R[w.sL])return n(y);var e=w.sL?s(w.sL,y,!0,L[w.sL]):l(y);return w.r>0&&(B+=e.r),"continuous"==w.subLanguageMode&&(L[w.sL]=e.top),p(e.language,e.value,!1,!0)}function v(){return void 0!==w.sL?h():d()}function b(e,t){var r=e.cN?p(e.cN,"",!0):"";e.rB?(M+=r,y=""):e.eB?(M+=n(t)+r,y=""):(M+=r,y=t),w=Object.create(e,{parent:{value:w}})}function m(e,t){if(y+=e,void 0===t)return M+=v(),0;var r=i(t,w);if(r)return M+=v(),b(r,t),r.rB?0:t.length;var a=c(w,t);if(a){var o=w;o.rE||o.eE||(y+=t),M+=v();do w.cN&&(M+=""),B+=w.r,w=w.parent;while(w!=a.parent);return o.eE&&(M+=n(t)),y="",a.starts&&b(a.starts,""),o.rE?0:t.length}if(f(t,w))throw new Error('Illegal lexeme "'+t+'" for mode "'+(w.cN||"")+'"');return y+=t,t.length||1}var x=N(e);if(!x)throw new Error('Unknown language: "'+e+'"');u(x);for(var w=o||x,L={},M="",k=w;k!=x;k=k.parent)k.cN&&(M=p(k.cN,"",!0)+M);var y="",B=0;try{for(var C,j,I=0;;){if(w.t.lastIndex=I,C=w.t.exec(t),!C)break;j=m(t.substr(I,C.index-I),C[0]),I=C.index+j}m(t.substr(I));for(var k=w;k.parent;k=k.parent)k.cN&&(M+="");return{r:B,value:M,language:e,top:w}}catch(A){if(-1!=A.message.indexOf("Illegal"))return{r:0,value:n(t)};throw A}}function l(e,t){t=t||E.languages||Object.keys(R);var r={r:0,value:n(e)},a=r;return t.forEach(function(n){if(N(n)){var t=s(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}}),a.language&&(r.second_best=a),r}function f(e){return E.tabReplace&&(e=e.replace(/^((<[^>]+>|\t)+)/gm,function(e,n){return n.replace(/\t/g,E.tabReplace)})),E.useBR&&(e=e.replace(/\n/g,"
")),e}function g(e,n,t){var r=n?x[n]:t,a=[e.trim()];return e.match(/(\s|^)hljs(\s|$)/)||a.push("hljs"),r&&a.push(r),a.join(" ").trim()}function p(e){var n=a(e);if(!/no(-?)highlight/.test(n)){var t;E.useBR?(t=document.createElementNS("http://www.w3.org/1999/xhtml","div"),t.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):t=e;var r=t.textContent,o=n?s(n,r,!0):l(r),u=i(t);if(u.length){var p=document.createElementNS("http://www.w3.org/1999/xhtml","div");p.innerHTML=o.value,o.value=c(u,i(p),r)}o.value=f(o.value),e.innerHTML=o.value,e.className=g(e.className,n,o.language),e.result={language:o.language,re:o.r},o.second_best&&(e.second_best={language:o.second_best.language,re:o.second_best.r})}}function d(e){E=o(E,e)}function h(){if(!h.called){h.called=!0;var e=document.querySelectorAll("pre code");Array.prototype.forEach.call(e,p)}}function v(){addEventListener("DOMContentLoaded",h,!1),addEventListener("load",h,!1)}function b(n,t){var r=R[n]=t(e);r.aliases&&r.aliases.forEach(function(e){x[e]=n})}function m(){return Object.keys(R)}function N(e){return R[e]||R[x[e]]}var E={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},R={},x={};return e.highlight=s,e.highlightAuto=l,e.fixMarkup=f,e.highlightBlock=p,e.configure=d,e.initHighlighting=h,e.initHighlightingOnLoad=v,e.registerLanguage=b,e.listLanguages=m,e.getLanguage=N,e.inherit=o,e.IR="[a-zA-Z][a-zA-Z0-9_]*",e.UIR="[a-zA-Z_][a-zA-Z0-9_]*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such)\b/},e.CLCM={cN:"comment",b:"//",e:"$",c:[e.PWM]},e.CBCM={cN:"comment",b:"/\\*",e:"\\*/",c:[e.PWM]},e.HCM={cN:"comment",b:"#",e:"$",c:[e.PWM]},e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e});hljs.registerLanguage("java",function(e){var a=e.UIR+"(<"+e.UIR+">)?",t="false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private",c="(\\b(0b[01_]+)|\\b0[xX][a-fA-F0-9_]+|(\\b[\\d_]+(\\.[\\d_]*)?|\\.[\\d_]+)([eE][-+]?\\d+)?)[lLfF]?",r={cN:"number",b:c,r:0};return{aliases:["jsp"],k:t,i:/<\//,c:[{cN:"javadoc",b:"/\\*\\*",e:"\\*/",r:0,c:[{cN:"javadoctag",b:"(^|\\s)@[A-Za-z]+"}]},e.CLCM,e.CBCM,e.ASM,e.QSM,{cN:"class",bK:"class interface",e:/[{;=]/,eE:!0,k:"class interface",i:/[:"\[\]]/,c:[{bK:"extends implements"},e.UTM]},{bK:"new throw return",r:0},{cN:"function",b:"("+a+"\\s+)+"+e.UIR+"\\s*\\(",rB:!0,e:/[{;=]/,eE:!0,k:t,c:[{b:e.UIR+"\\s*\\(",rB:!0,r:0,c:[e.UTM]},{cN:"params",b:/\(/,e:/\)/,k:t,r:0,c:[e.ASM,e.QSM,e.CNM,e.CBCM]},e.CLCM,e.CBCM]},r,{cN:"annotation",b:"@[A-Za-z]+"}]}}); \ No newline at end of file diff --git a/samples/promise/src/main/webapp/index.html b/samples/promise/src/main/webapp/index.html new file mode 100644 index 000000000..a30cbc44b --- /dev/null +++ b/samples/promise/src/main/webapp/index.html @@ -0,0 +1,459 @@ + + + + + Continuation-passing style demo + + + + + + + + + +
This application shows how TeaVM can interact with JavaScript + Promises + (see source code on GitHub).
+ +
+
+
stdout
+
+
+ +
+
Source code
+
+import java.util.Arrays;
+import org.teavm.jso.JSBody;
+import org.teavm.jso.browser.Window;
+import org.teavm.jso.core.JSArray;
+import org.teavm.jso.core.JSPromise;
+import org.teavm.jso.core.JSString;
+import org.teavm.jso.util.function.JSConsumer;
+import org.teavm.jso.util.function.JSFunction;
+import org.teavm.jso.util.function.JSSupplier;
+
+public final class PromiseExample {
+    private static long start = System.currentTimeMillis();
+
+    private PromiseExample() {
+    }
+
+    public static void main(String[] args) throws InterruptedException {
+        report(Arrays.toString(args));
+        report("");
+
+        checkFunctionalInterface();
+
+        runSimplePromise();
+
+        runComplexPromise();
+
+        final var lock = new Object();
+        runLongRunningPromise(lock);
+        synchronized (lock) {
+            report("Lock acquired");
+            lock.wait(20000);
+        }
+
+        combinePromises(lock);
+        synchronized (lock) {
+            report("Lock acquired");
+            lock.wait(20000);
+        }
+
+        testNativePromises(lock);
+
+        report("Finished main thread");
+    }
+
+    private static void report(String message) {
+        var current = System.currentTimeMillis() - start;
+        System.out.println("[" + Thread.currentThread().getName() + "]/" + current + ": " + message);
+    }
+
+    private static void checkFunctionalInterface() {
+        JSSupplier supplier = () -> 23;
+
+        JSFunction addTwenty = value -> value + 20;
+        JSFunction subTwenty = value -> value - 20;
+        JSFunction isPositive = value -> value >= 0;
+
+        JSConsumer print = value -> report("My value: " + value.toString());
+        JSConsumer print2 = value -> report("My value plus 10: " + Integer.valueOf(value + 10).toString());
+
+        var value = supplier.get();
+        report("Supplied value: " + value.toString());
+
+        value = addTwenty.apply(value);
+        report("Value plus 20: " + value.toString());
+
+        value = subTwenty.apply(value);
+        report("Value minus 20: " + value.toString());
+
+        var value2 = isPositive.apply(value);
+        report("Value is positive: " + value2.toString());
+
+        var subFourty = subTwenty.andThen(subTwenty);
+        value = subFourty.apply(value);
+        report("Value minus 40: " + value.toString());
+
+        var plusFourty = addTwenty.compose(addTwenty).andThen(addTwenty.compose(subTwenty));
+        value = plusFourty.apply(value);
+        report("Value plus 40: " + value.toString());
+
+        value2 = subFourty.andThen(isPositive).apply(value);
+        report("Value minus 40 is positive: " + value2.toString());
+
+        print.accept(value);
+        var printExtended = print.andThen(print2);
+        printExtended.accept(value);
+    }
+
+    private static void runSimplePromise() {
+        var promise = JSPromise.create((resolve, reject) -> {
+            report("Simple promise execution");
+
+            report("Resolving with 'success'");
+            resolve.accept("success");
+        });
+    }
+
+    private static void runComplexPromise() {
+        var promise = JSPromise.create((resolve, reject) -> {
+            report("Complex promise execution");
+
+            report("Resolving with 'step1'");
+            resolve.accept("step1");
+        })
+        .then(value -> {
+            report("Resolved with '" + value + "'");
+            report("... and resolve with 'step2'");
+            return "step2";
+        })
+        .then(value -> {
+            report("Resolved with '" + value + "'");
+            report("... and throw exception");
+            throw new RuntimeException("Exception in promise handler");
+        }, reason -> {
+            report("Failed unexpectedly with reason: " + reason.toString());
+            return reason.toString();
+        })
+        .then(value -> {
+            report("Resolved unexpectedly with '" + value + "'");
+            return value;
+        }, reason -> {
+            report("Failed expectedly with reason: " + reason.toString());
+            return reason.toString();
+        })
+        .flatThen(value -> {
+            report("Resolved with '" + value + "'");
+            report("... and resolve with resolved promise");
+            return JSPromise.resolve("step3");
+        })
+        .flatThen(value -> {
+            report("Resolved with '" + value + "'");
+            report("... and resolve with rejected promise");
+            return JSPromise.reject("step4");
+        })
+        .catchError(reason -> {
+            report("Catched reject reason '" + reason.toString() + "'");
+            return reason.toString();
+        })
+        .flatThen(value -> {
+            report("Resolved with '" + value + "'");
+            report("... and resolve with new promise");
+            return JSPromise.create((resolve, reject) -> {
+                report("Inner promise");
+                report("Reject with 'step from inner'");
+                reject.accept("step from inner");
+            });
+        })
+        .then(value -> {
+            report("Resolved unexpectedly with '" + value + "'");
+            return value;
+        })
+        .catchError(reason -> {
+            report("Catched reject reason '" + reason.toString() + "'");
+            return reason.toString();
+        })
+        .onSettled(() -> {
+            report("Promise has finally settled");
+            return null;
+        });
+    }
+
+    private static void runLongRunningPromise(Object lock) {
+        var promise = JSPromise.create((resolve, reject) -> {
+            report("Long promise exection");
+            report("Wait for a while...");
+            Window.setTimeout(() -> {
+                report("... and resolve with 'done'");
+                resolve.accept("done");
+            }, 2000);
+        }).then(value -> {
+            report("Resolved with '" + value + "'");
+            synchronized (lock) {
+                lock.notify();
+            }
+            return value;
+        });
+    }
+
+    private static void combinePromises(Object lock) throws InterruptedException {
+        JSArray> promises = JSArray.create(3);
+
+        report("Start 3 successful promises");
+        promises.set(0, JSPromise.resolve("success1"));
+        promises.set(1, JSPromise.resolve("success2"));
+        promises.set(2, JSPromise.resolve("success3"));
+
+        var allPromises = JSPromise.all(promises);
+        allPromises.then(value -> {
+            report("All promises resolved to: " + value);
+            return "success";
+        }, reason -> {
+            report("At least one promise rejected with: " + reason.toString());
+            return "failure";
+        }).onSettled(() -> {
+            synchronized (lock) {
+                lock.notify();
+            }
+            return null;
+        });
+
+        synchronized (lock) {
+            report("Lock acquired");
+            lock.wait(20000);
+        }
+
+        report("Start 1 successful and 2 rejected promise");
+        promises.set(0, JSPromise.resolve("success1"));
+        promises.set(1, JSPromise.reject("failure2"));
+        promises.set(2, JSPromise.reject("failure3"));
+
+        allPromises = JSPromise.all(promises);
+        allPromises.then(value -> {
+            report("All promises resolved to: " + value);
+            return "success";
+        }, reason -> {
+            report("At least one promise rejected with: " + reason.toString());
+            return "failure";
+        }).onSettled(() -> {
+            synchronized (lock) {
+                lock.notify();
+            }
+            return null;
+        });
+
+        synchronized (lock) {
+            report("Lock acquired");
+            lock.wait(20000);
+        }
+
+        var settledPromises = JSPromise.allSettled(promises);
+        settledPromises.then(value -> {
+            report(Integer.toString(value.getLength()) + " promises settled to:");
+            for (int i = 0; i < value.getLength(); ++i) {
+                var item = value.get(i);
+                var msg = "-- Promise " + i + " " + item.getStatus() + " with: ";
+                if (item.getStatus().stringValue().equals("fulfilled")) {
+                    msg += item.getValue().toString();
+                } else if (item.getStatus().stringValue().equals("rejected")) {
+                    msg += item.getReason().toString();
+                }
+                report(msg);
+            }
+            return "success";
+        }).onSettled(() -> {
+            synchronized (lock) {
+                lock.notify();
+            }
+            return null;
+        });
+
+        synchronized (lock) {
+            report("Lock acquired");
+            lock.wait(20000);
+        }
+
+        var anyPromise = JSPromise.any(promises);
+        anyPromise.then(value -> {
+            report("At least one promise resolved to: " + value);
+            return "success";
+        }, reason -> {
+            report("All promises rejected with: " + reason.toString());
+            return "failure";
+        }).onSettled(() -> {
+            synchronized (lock) {
+                lock.notify();
+            }
+            return null;
+        });
+
+        synchronized (lock) {
+            report("Lock acquired");
+            lock.wait(20000);
+        }
+
+        report("Start 3 rejected promises");
+        promises.set(0, JSPromise.reject("failure1"));
+        promises.set(1, JSPromise.reject("failure2"));
+        promises.set(2, JSPromise.reject("failure3"));
+
+        anyPromise = JSPromise.any(promises);
+        anyPromise.then(value -> {
+            report("At least one promise resolved to: " + value);
+            return "success";
+        }, reason -> {
+            report("All promises rejected with: " + reason.toString());
+            return "failure";
+        }).onSettled(() -> {
+            synchronized (lock) {
+                lock.notify();
+            }
+            return null;
+        });
+
+        synchronized (lock) {
+            report("Lock acquired");
+            lock.wait(20000);
+        }
+
+        report("Start 3 delayed promises");
+        promises.set(0, JSPromise.create((resolve, reject) -> Window.setTimeout(() -> resolve.accept("success1"), 200)));
+        promises.set(1, JSPromise.create((resolve, reject) -> Window.setTimeout(() -> reject.accept("failure1"), 100)));
+        promises.set(2, JSPromise.create((resolve, reject) -> Window.setTimeout(() -> resolve.accept("success3"), 50)));
+
+        anyPromise = JSPromise.race(promises);
+        anyPromise.then(value -> {
+            report("First settled promise resolved to: " + value);
+            return "success";
+        }, reason -> {
+            report("First settled promise rejected with: " + reason.toString());
+            return "failure";
+        }).onSettled(() -> {
+            synchronized (lock) {
+                lock.notify();
+            }
+            return null;
+        });
+
+        synchronized (lock) {
+            report("Lock acquired");
+            lock.wait(20000);
+        }
+
+        report("Start 3 delayed promises");
+        promises.set(0, JSPromise.create((resolve, reject) -> Window.setTimeout(() -> resolve.accept("success1"), 200)));
+        promises.set(1, JSPromise.create((resolve, reject) -> Window.setTimeout(() -> reject.accept("failure1"), 50)));
+        promises.set(2, JSPromise.create((resolve, reject) -> Window.setTimeout(() -> resolve.accept("success3"), 100)));
+
+        anyPromise = JSPromise.race(promises);
+        anyPromise.then(value -> {
+            report("First settled promise resolved to: " + value);
+            return "success";
+        }, reason -> {
+            report("First settled promise rejected with: " + reason.toString());
+            return "failure";
+        }).onSettled(() -> {
+            synchronized (lock) {
+                lock.notify();
+            }
+            return null;
+        });
+    }
+
+    private static void testNativePromises(Object lock) throws InterruptedException {
+        report("Get promise from native method");
+        var nativePromise = getNativePromise(JSString.valueOf("success from native"));
+        nativePromise.then(value -> {
+            report("Native resolved expectedly with '" + value + "'");
+            return value;
+        }, reason -> {
+            report("Native rejected unexpectedly with '" + reason.toString() + "'");
+            return reason.toString();
+        }).onSettled(() -> {
+            synchronized (lock) {
+                lock.notify();
+            }
+            return null;
+        });
+
+        synchronized (lock) {
+            report("Lock acquired");
+            lock.wait(20000);
+        }
+
+        nativePromise = getNativeRejectingPromise(JSString.valueOf("failure from native"));
+        nativePromise.then(value -> {
+            report("Native resolved unexpectedly with '" + value + "'");
+            return value;
+        }, reason -> {
+            report("Native rejected expectedly with '" + reason.toString() + "'");
+            return reason.toString();
+        }).onSettled(() -> {
+            synchronized (lock) {
+                lock.notify();
+            }
+            return null;
+        });
+
+        synchronized (lock) {
+            report("Lock acquired");
+            lock.wait(20000);
+        }
+
+        report("Pass promise to native method");
+        handlePromise(JSPromise.create((resolve, reject) -> {
+            resolve.accept(JSString.valueOf("Resolved from Java"));
+        }));
+
+        handlePromise(JSPromise.create((resolve, reject) -> {
+            reject.accept(JSString.valueOf("Rejected from Java"));
+        }));
+    }
+
+    @JSBody(params = "msg", script = "return new Promise((resolve, reject) => {"
+        + "    setTimeout(() => resolve(msg), 500);"
+        + "});")
+    private static native JSPromise getNativePromise(JSString msg);
+
+    @JSBody(params = "msg", script = "return new Promise((resolve, reject) => {"
+        + "    setTimeout(() => reject(msg), 500);"
+        + "});")
+    private static native JSPromise getNativeRejectingPromise(JSString msg);
+
+    @JSBody(params = "promise", script = "promise.then("
+        + "    (value) => console.log('success:', value),"
+        + "    (reason) => console.log('failure:', reason));")
+    private static native void handlePromise(JSPromise promise);
+}
+
+ +
+
+ + + diff --git a/samples/promise/src/main/webapp/style.css b/samples/promise/src/main/webapp/style.css new file mode 100644 index 000000000..96249c14c --- /dev/null +++ b/samples/promise/src/main/webapp/style.css @@ -0,0 +1,62 @@ +html, body { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + margin: 0; + padding: 0; +} + +.block { + position: absolute; + left: 0; + right: 0; +} +.block-title { + position: absolute; + top: 10px; + height: 20px; + left: 10px; + right: 10px; + font-weight: bold; +} +.block-content { + position: absolute; + top: 30px; + bottom: 5px; + left: 10px; + right: 10px; + background-color: rgb(240,240,240); + border-width: 1px; + border-style: solid; + border-color: rgb(210,210,210); + overflow: auto; + padding: 3px; + margin: 0; + border-radius: 3px; +} + +#description { + position: absolute; + top: 10px; + height: 40px; + left: 10px; + right: 10px; +} +#blocks { + position: absolute; + top: 50px; + bottom: 0; + left: 0; + right: 0; +} +#stdout-wrapper { + top: 0; + height: 50%; +} +#source-wrapper { + top: 50%; + bottom: 0; +} + diff --git a/samples/promise/src/main/webapp/syntax.css b/samples/promise/src/main/webapp/syntax.css new file mode 100644 index 000000000..e4d47b0a7 --- /dev/null +++ b/samples/promise/src/main/webapp/syntax.css @@ -0,0 +1,12 @@ +.hljs-keyword { + font-weight: bold; +} +.hljs-class .hljs-title { + color: #9BB01D; +} +.hljs-annotation { + color: #FFA500; +} +.hljs-string, .hljs-number { + color: red; +} \ No newline at end of file diff --git a/samples/settings.gradle.kts b/samples/settings.gradle.kts index db11e7ea6..d98625e2c 100644 --- a/samples/settings.gradle.kts +++ b/samples/settings.gradle.kts @@ -56,6 +56,7 @@ include("hello") include("async") include("benchmark") include("pi") +include("promise") include("kotlin") include("scala") include("web-apis")