diff --git a/.github/workflows/release-js.yml b/.github/workflows/release-js.yml new file mode 100644 index 000000000..3bc1cfa02 --- /dev/null +++ b/.github/workflows/release-js.yml @@ -0,0 +1,69 @@ +name: JS automated product release + +on: + pull_request: + branches: [master] + types: [closed] + paths: + - 'js-chat/.pubnub.yml' + +jobs: + check-release: + name: Check release required + if: github.event.pull_request.merged && endsWith(github.repository, '-private') != true + runs-on: + group: macos-gh + outputs: + release: ${{ steps.check.outputs.ready }} + steps: + - name: Checkout actions + uses: actions/checkout@v4 + with: + repository: pubnub/client-engineering-deployment-tools + ref: v1 + token: ${{ secrets.GH_TOKEN }} + path: .github/.release/actions + - id: check + name: Check pre-release completed + uses: ./.github/.release/actions/actions/checks/release + with: + token: ${{ secrets.GH_TOKEN }} + publish: + name: Publish package + needs: check-release + if: needs.check-release.outputs.release == 'true' + runs-on: + group: macos-gh + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + # This should be the same as the one specified for on.pull_request.branches + ref: master + submodules: recursive + - name: Checkout actions + uses: actions/checkout@v4 + with: + repository: pubnub/client-engineering-deployment-tools + ref: v1 + token: ${{ secrets.GH_TOKEN }} + path: .github/.release/actions + - name: Install JDK + uses: actions/setup-java@v4 + with: + distribution: 'zulu' # See 'Supported distributions' for available options + java-version: '21' + - uses: actions/setup-node@v4 + with: + node-version: 18 + - name: Publish to NPM + uses: ./.github/.release/actions/actions/services/npm + with: + token: ${{ secrets.GH_TOKEN }} + npm-token: ${{ secrets.NPM_TOKEN }} + check-ownership: false + package-path: package.json + actions: "build,publish" + build-command: npm run build + build-path: dist + last-service: true diff --git a/.github/workflows/release/js-pre-npm-build.sh b/.github/workflows/release/js-pre-npm-build.sh new file mode 100755 index 000000000..03e00f5b4 --- /dev/null +++ b/.github/workflows/release/js-pre-npm-build.sh @@ -0,0 +1,8 @@ +set -e +echo "Build JS Matchmaking SDK module artifacts" +./gradlew pubnub-matchmaking-kotlin:jsNodeProductionLibraryDistribution #build JS locally, command: ./gradlew pubnub-matchmaking-kotlin:jsNodeProductionLibraryDistribution +./gradlew pubnub-matchmaking-kotlin:packJsPackage #this is configured by npmPublish in pubnub-matchmaking-kotlin/build.gradle.kts +mkdir -p pubnub-matchmaking-kotlin/js-matchmaking/dist +cp pubnub-matchmaking-kotlin/build/packages/js/package.json pubnub-matchmaking-kotlin/js-matchmaking/package.json +cp pubnub-matchmaking-kotlin/build/dist/js/productionLibrary/pubnub-pubnub-matchmaking-kotlin.d.ts pubnub-matchmaking-kotlin/js-matchmaking/dist/ #todo zmienic nazwe pliku index.d.ts +cp pubnub-matchmaking-kotlin/build/dist/js/productionLibrary/pubnub-pubnub-matchmaking-kotlin.d.ts pubnub-matchmaking-kotlin/js-matchmaking/dist/pubnub-pubnub-matchmaking-kotlin.es.d.ts #todo zmienic nazwe pliku index.d.ts diff --git a/.github/workflows/release/products.json b/.github/workflows/release/products.json index 00a198434..b37910eed 100644 --- a/.github/workflows/release/products.json +++ b/.github/workflows/release/products.json @@ -4,5 +4,11 @@ "name": "Java & Kotlin SDK", "main": true, "overrideRelease": true + }, + "js": { + "path": "./pubnub-matchmaking-kotlin/js-matchmaking", + "name": "PubNub JS Matchmaking SDK", + "main": false, + "overrideRelease": true } } diff --git a/.pubnub.yml b/.pubnub.yml index c9a83d454..db9b86ce8 100644 --- a/.pubnub.yml +++ b/.pubnub.yml @@ -73,8 +73,8 @@ sdks: requires: - name: kotlin-stdlib - min-version: 1.8.0 - location: https://repo1.maven.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/2.0.21/kotlin-stdlib-2.0.21.jar + min-version: 2.1.0 + location: https://repo1.maven.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/2.1.0/kotlin-stdlib-2.1.0.jar license: Apache License, Version 2.0 license-url: https://www.apache.org/licenses/LICENSE-2.0.txt is-required: Required diff --git a/build-logic/gradle-plugins/src/main/kotlin/com/pubnub/gradle/PubNubKotlinMultiplatformPlugin.kt b/build-logic/gradle-plugins/src/main/kotlin/com/pubnub/gradle/PubNubKotlinMultiplatformPlugin.kt index 60bc7819f..73afc047d 100644 --- a/build-logic/gradle-plugins/src/main/kotlin/com/pubnub/gradle/PubNubKotlinMultiplatformPlugin.kt +++ b/build-logic/gradle-plugins/src/main/kotlin/com/pubnub/gradle/PubNubKotlinMultiplatformPlugin.kt @@ -71,9 +71,6 @@ class PubNubBaseKotlinMultiplatformPlugin : Plugin { if (enableJsTarget) { js { -> - project.findProperty("JS_MODULE_NAME")?.toString()?.let { jsModuleName -> - moduleName = jsModuleName - } nodejs { testTask { it.environment("MOCHA_OPTIONS", "--exit") diff --git a/gradle.properties b/gradle.properties index a56303289..1abbaf7f0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -39,10 +39,16 @@ POM_DEVELOPER_NAME=PubNub POM_DEVELOPER_URL=support@pubnub.com IOS_SIMULATOR_ID=iPhone 15 Pro -#SWIFT_PATH=../swift +#SWIFT_PATH=../swift # swift project sourcecode is in the same folder as kotlin source ENABLE_TARGET_JS=true ENABLE_TARGET_IOS_OTHER=false ENABLE_TARGET_IOS_SIMULATOR_ARM64=true # alternatively (e.g. for release): -# ENABLE_TARGET_IOS_ALL=true \ No newline at end of file +# ENABLE_TARGET_IOS_ALL=true + +#the compiler generates a single, large JavaScript output file containing all Kotlin code and its dependencies. +kotlin.js.ir.output.granularity=whole-program + +#we don't want to publish to NPM from here we do it from github scripts +NPM_PUBLISH_REGISTRY_NPMJS_DRY=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 14cb4c779..7ccaacc52 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ logback = "1.2.11" okhttp = "4.12.0" retrofit2 = "2.11.0" nexus = "2.0.0" -kotlin = "2.0.21" +kotlin = "2.1.0" vanniktech = "0.29.0" ktlint = "12.1.0" dokka = "1.9.20" @@ -28,6 +28,7 @@ jetbrains-annotations = { module = "org.jetbrains:annotations", version = "24.1. kotlinx-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version = "0.24.0" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx_datetime"} kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx_coroutines"} +touchlab-kermit = { module = "co.touchlab:kermit", version = "2.0.4" } # tests wiremock = { module = "com.github.tomakehurst:wiremock", version = "2.27.2" } @@ -64,4 +65,6 @@ benmanes-versions = { id = "com.github.ben-manes.versions", version = "0.42.0" } vanniktech-maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktech" } lombok = { id = "io.freefair.lombok", version = "8.6" } gradle-nexus-publish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexus" } -codingfeline-buildkonfig = { id = "com.codingfeline.buildkonfig", version = "0.15.1" } \ No newline at end of file +codingfeline-buildkonfig = { id = "com.codingfeline.buildkonfig", version = "0.15.1" } +mokkery = { id = "dev.mokkery", version = "2.6.0" } +npm-publish = { id = "dev.petuska.npm.publish", version = "3.4.3" } \ No newline at end of file diff --git a/kotlin-js-store/yarn.lock b/kotlin-js-store/yarn.lock index 18b9c34f2..f5bf8f6f0 100644 --- a/kotlin-js-store/yarn.lock +++ b/kotlin-js-store/yarn.lock @@ -490,6 +490,13 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +kotlin-web-helpers@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/kotlin-web-helpers/-/kotlin-web-helpers-2.0.0.tgz#b112096b273c1e733e0b86560998235c09a19286" + integrity sha512-xkVGl60Ygn/zuLkDPx+oHj7jeLR7hCvoNF99nhwXMn8a3ApB4lLiC9pk4ol4NHPjyoCbvQctBqvzUcp8pkqyWw== + dependencies: + format-util "^1.0.5" + lil-uuid@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/lil-uuid/-/lil-uuid-0.1.1.tgz#f9edcf23f00e42bf43f0f843d98d8b53f3341f16" @@ -534,10 +541,10 @@ minimatch@^5.0.1, minimatch@^5.1.6: dependencies: brace-expansion "^2.0.1" -mocha@10.7.0: - version "10.7.0" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.7.0.tgz#9e5cbed8fa9b37537a25bd1f7fb4f6fc45458b9a" - integrity sha512-v8/rBWr2VO5YkspYINnvu81inSz2y3ODJrhO175/Exzor1RcEZZkizgE2A+w/CAXXoESS8Kys5E62dOHGHzULA== +mocha@10.7.3: + version "10.7.3" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.7.3.tgz#ae32003cabbd52b59aece17846056a68eb4b0752" + integrity sha512-uQWxAu44wwiACGqjbPYmjo7Lg8sFrS3dQe7PP2FQI+woptP4vZXSMcfMyFL/e1yFEeEpV4RtyTpZROOKmxis+A== dependencies: ansi-colors "^4.1.3" browser-stdout "^1.3.1" diff --git a/pubnub-matchmaking-kotlin/build.gradle.kts b/pubnub-matchmaking-kotlin/build.gradle.kts new file mode 100644 index 000000000..2fba246ad --- /dev/null +++ b/pubnub-matchmaking-kotlin/build.gradle.kts @@ -0,0 +1,49 @@ +import com.pubnub.gradle.enableJsTarget + +plugins { + alias(libs.plugins.benmanes.versions) + id("pubnub.dokka") + alias(libs.plugins.npm.publish) + id("pubnub.multiplatform") + id("pubnub.shared") +} + +npmPublish { + packages { + getByName("js") { + scope = "pubnub" + packageName = "matchmaking" +// types.set("index.d.ts") // todo not needed if types are generated and defined in package.json + packageJsonTemplateFile = project.layout.projectDirectory.file("js-matchmaking/package_template.json") + } + } +} + +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + api(project(":pubnub-matchmaking-kotlin:pubnub-matchmaking-kotlin-api")) + implementation(project(":pubnub-matchmaking-kotlin:pubnub-matchmaking-kotlin-impl")) + } + } + } + if (enableJsTarget) { + js { +// keep this in here for ad-hoc testing +// browser { +// testTask { +// useMocha { +// timeout = "15s" +// } +// } +// } + + compilerOptions { + target.set("es2015") + } + binaries.library() + generateTypeScriptDefinitions() // generates pubnub-matchmaking-kotlin/build/dist/js/productionLibrary/pubnub-pubnub-matchmaking-kotlin.d.ts + } + } +} diff --git a/pubnub-matchmaking-kotlin/js-matchmaking/.pubnub.yml b/pubnub-matchmaking-kotlin/js-matchmaking/.pubnub.yml new file mode 100644 index 000000000..3c4dba1a4 --- /dev/null +++ b/pubnub-matchmaking-kotlin/js-matchmaking/.pubnub.yml @@ -0,0 +1,12 @@ +name: pubnub-js-matchmaking +version: 0.11.6 +scm: github.com/pubnub/kotlin +schema: 1 +files: + - lib/dist/index.js +changelog: + - date: 2025-03-26 + version: v0.1.0 + changes: + - type: feature + text: "Initial release." diff --git a/pubnub-matchmaking-kotlin/js-matchmaking/LICENSE b/pubnub-matchmaking-kotlin/js-matchmaking/LICENSE new file mode 100644 index 000000000..c08d55bb4 --- /dev/null +++ b/pubnub-matchmaking-kotlin/js-matchmaking/LICENSE @@ -0,0 +1,31 @@ +PubNub Software Development Kit License Agreement +Copyright © 2023 PubNub Inc. All rights reserved. + +Subject to the terms and conditions of the license, you are hereby granted +a non-exclusive, worldwide, royalty-free license to (a) copy and modify +the software in source code or binary form for use with the software services +and interfaces provided by PubNub, and (b) redistribute unmodified copies +of the software to third parties. The software may not be incorporated in +or used to provide any product or service competitive with the products +and services of PubNub. + +The above copyright notice and this license shall be included +in or with all copies or substantial portions of the software. + +This license does not grant you permission to use the trade names, trademarks, +service marks, or product names of PubNub, except as required for reasonable +and customary use in describing the origin of the software and reproducing +the content of this license. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL PUBNUB OR THE AUTHORS OR COPYRIGHT HOLDERS OF THE SOFTWARE BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +https://www.pubnub.com/ +https://www.pubnub.com/terms + + diff --git a/pubnub-matchmaking-kotlin/js-matchmaking/NOTICE b/pubnub-matchmaking-kotlin/js-matchmaking/NOTICE new file mode 100644 index 000000000..27c621dc8 --- /dev/null +++ b/pubnub-matchmaking-kotlin/js-matchmaking/NOTICE @@ -0,0 +1,431 @@ +This software contains other open-source libraries and that require the inclusion of the following notices: + +Touchlab Kermit +https://github.com/touchlab/Kermit/blob/main/LICENSE.txt + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2021 Touchlab + + 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. + + +JetBrains Kotlin +https://github.com/JetBrains/kotlin/blob/master/license/README.md + +/* + * Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors. + * + * 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. + */ + + The following modules contain third-party code and are incorporated into the Kotlin compiler and/or the Kotlin IntelliJ IDEA plugin: + + Path: compiler/backend/src/org/jetbrains/kotlin/codegen/inline/MaxStackFrameSizeAndLocalsCalculator.kt + + License: BSD (license/third_party/asm_license.txt) + Origin: Derived from ASM: a very small and fast Java bytecode manipulation framework, Copyright (c) 2000-2011 INRIA, France Telecom + Path: compiler/backend/src/org/jetbrains/kotlin/codegen/inline/MaxLocalsCalculator.java + + License: BSD (license/third_party/asm_license.txt) + Origin: Derived from ASM: a very small and fast Java bytecode manipulation framework, Copyright (c) 2000-2011 INRIA, France Telecom + Path: compiler/backend/src/org/jetbrains/kotlin/codegen/optimization/common/FastMethodAnalyzer.kt + + License: BSD (license/third_party/asm_license.txt) + Origin: Derived from ASM: a very small and fast Java bytecode manipulation framework, Copyright (c) 2000-2011 INRIA, France Telecom + Path: compiler/backend/src/org/jetbrains/kotlin/codegen/optimization/common/InstructionLivenessAnalyzer.kt + + License: BSD (license/third_party/asm_license.txt) + Origin: Derived from ASM: a very small and fast Java bytecode manipulation framework, Copyright (c) 2000-2011 INRIA, France Telecom + Path: compiler/backend/src/org/jetbrains/kotlin/codegen/optimization/common/ControlFlowGraph.kt + + License: BSD (license/third_party/asm_license.txt) + Origin: Derived from ASM: a very small and fast Java bytecode manipulation framework, Copyright (c) 2000-2011 INRIA, France Telecom + Path: compiler/backend/src/org/jetbrains/kotlin/codegen/optimization/fixStack/FastStackAnalyzer.kt + + License: BSD (license/third_party/asm_license.txt) + Origin: Derived from ASM: a very small and fast Java bytecode manipulation framework, Copyright (c) 2000-2011 INRIA, France Telecom + Path: compiler/backend/src/org/jetbrains/kotlin/codegen/optimization/temporaryVals/FastStoreLoadAnalyzer.kt + + License: BSD (license/third_party/asm_license.txt) + Origin: Derived from ASM: a very small and fast Java bytecode manipulation framework, Copyright (c) 2000-2011 INRIA, France Telecom + Path: eval4j/src/org/jetbrains/eval4j/interpreterLoop.kt + + License: BSD (license/third_party/asm_license.txt) + Origin: Derived from ASM: a very small and fast Java bytecode manipulation framework, Copyright (c) 2000-2011 INRIA, France Telecom + Path: compiler/backend/src/org/jetbrains/kotlin/codegen/optimization/common/OptimizationBasicInterpreter.java + + License: BSD (license/third_party/asm_license.txt) + Origin: Derived from ASM: a very small and fast Java bytecode manipulation framework, Copyright (c) 2000-2011 INRIA, France Telecom + Path: js/js.ast + + License: BSD (license/third_party/dart_LICENSE.txt) + Origin: Originally part of the Dart compiler, (c) 2011 the Dart Project Authors, + Path: js/js.parser/src/com/google + + License: Netscape Public License 1.1 (license/third_party/rhino_LICENSE.txt) + Origin: Originally part of GWT, (C) 2007-08 Google Inc., distributed under the Apache 2 license. The code is derived from Rhino, (C) 1997-1999 Netscape Communications Corporation, distributed under the Netscape Public License. + Path: libraries/stdlib/src/kotlin/collections + + License: Apache 2 (license/third_party/gwt_license.txt) + Origin: Derived from GWT, (C) 2007-08 Google Inc. + Path: libraries/stdlib/js/src/kotlin/UnsignedJs.kt + + License: Apache 2 (license/third_party/guava_license.txt) + Origin: Derived from Guava's UnsignedLongs, (C) 2011 The Guava Authors + Path: libraries/stdlib/jvm/src/kotlin/util/UnsignedJVM.kt + + License: Apache 2 (license/third_party/guava_license.txt) + Origin: Derived from Guava's UnsignedLongs, (C) 2011 The Guava Authors + Path: kotlin-native/runtime/src/main/kotlin/kotlin/Unsigned.kt + + License: Apache 2 (license/third_party/guava_license.txt) + Origin: Derived from Guava's UnsignedLongs, (C) 2011 The Guava Authors + Path: libraries/stdlib/jvm/src/kotlin/util/MathJVM.kt + + License: Boost Software License 1.0 (license/third_party/boost_LICENSE.txt) + Origin: Derived from boost special math functions, Copyright Eric Ford & Hubert Holin 2001. + Path: libraries/stdlib/js/src/kotlin/collections + + License: Apache 2 (license/third_party/gwt_license.txt) + Origin: Derived from GWT, (C) 2007-08 Google Inc. + Path: libraries/stdlib/native-wasm/src/kotlin/collections + + License: Apache 2 (license/third_party/gwt_license.txt) + Origin: Derived from GWT, (C) 2007-08 Google Inc. + Path: libraries/stdlib/js/runtime/longJs.kt + + License: Apache 2 (license/third_party/closure-compiler_LICENSE.txt) + Origin: Google Closure Library, Copyright 2009 The Closure Library Authors + Path: libraries/stdlib/js/src/kotlin/js/math.polyfills.kt + + License: Boost Software License 1.0 (license/third_party/boost_LICENSE.txt) + Origin: Derived from boost special math functions, Copyright Eric Ford & Hubert Holin 2001. + Path: libraries/stdlib/wasm/internal/kotlin/wasm/internal/Number2String.kt + + License: Apache 2 (license/third_party/assemblyscript_license.txt) + Origin: Derived from assemblyscript standard library + Path: libraries/tools/kotlin-power-assert + + License: Apache 2 (license/third_party/power_assert_license.txt) + Origin: Copyright (C) 2020-2023 Brian Norman + Path: plugins/compose + + License: Apache 2 (license/third_party/compose_license.txt) + Origin: Copyright 2019-2024 The Android Open Source Project + Path: plugins/lint/android-annotations + + License: Apache 2 (license/third_party/aosp_license.txt) + Origin: Copyright (C) 2011-15 The Android Open Source Project + Path: plugins/lint/lint-api + + License: Apache 2 (license/third_party/aosp_license.txt) + Origin: Copyright (C) 2011-15 The Android Open Source Project + Path: plugins/lint/lint-checks + + License: Apache 2 (license/third_party/aosp_license.txt) + Origin: Copyright (C) 2011-15 The Android Open Source Project + Path: plugins/lint/lint-idea + + License: Apache 2 (license/third_party/aosp_license.txt) + Origin: Copyright (C) 2011-15 The Android Open Source Project + Path: plugins/power-assert + + License: Apache 2 (license/third_party/power_assert_license.txt) + Origin: Copyright (C) 2020-2023 Brian Norman + Path: wasm/ir/src/org/jetbrains/kotlin/wasm/ir/convertors + + License: MIT (license/third_party/asmble_license.txt) + Origin: Copyright (C) 2018 Chad Retz + Path: compiler/tests-common/tests/org/jetbrains/kotlin/codegen/ir/ComposeLikeGenerationExtension.kt + + License: Apache 2 (license/third_party/aosp_license.txt) + Origin: Derived from JetPack Compose compiler plugin code, Copyright 2019 The Android Open Source Project + Path: libraries/stdlib/wasm/src/kotlin/text/FloatingPointConverter.kt + + License: MIT (license/third_party/asmble_license.txt) + Origin: Copyright (C) 2018 Chad Retz + Path: libraries/stdlib/wasm/src/kotlin/math/fdlibm/ + + License: SUN (license/third_party/sun_license.txt) + Origin: Copyright (C) 1993 by Sun Microsystems, Inc. + Path: kotlin-native/runtime/src/main/cpp/Utils.cpp + + License: Boost Software License 1.0 (license/third_party/boost_LICENSE.txt) + Origin: Derived from boost hash functions, Copyright 2005-2014 Daniel James + +JetBrains Kotlinx.datetime +https://github.com/Kotlin/kotlinx-datetime/blob/master/license/README.md + + Path: core/common/src/internal/dateCalculations.kt + + Origin: implementation of date/time calculations is based on ThreeTen backport project. + License: BSD 3-Clause (license/thirdparty/threetenbp_license.txt) + Path: core/nativeMain/src + + Origin: implementation of date/time entities is based on ThreeTen backport project. + License: BSD 3-Clause (license/thirdparty/threetenbp_license.txt) + Path: core/nativeTest/src + + Origin: Derived from tests of ThreeTen backport project + License: BSD 3-Clause (license/thirdparty/threetenbp_license.txt) + Path: core/commonTest/src + + Origin: Some tests are derived from tests of ThreeTen backport project + License: BSD 3-Clause (license/thirdparty/threetenbp_license.txt) + Path: thirdparty/date + + Origin: https://github.com/HowardHinnant/date library + License: MIT (license/thirdparty/cppdate_license.txt) + Path: core/nativeMain/cinterop/public/windows_zones.hpp + + Origin: time zone name mappings for Windows are generated from https://raw.githubusercontent.com/unicode-org/cldr/master/common/supplemental/windowsZones.xml + License: Unicode (license/thirdparty/unicode_license.txt) + Path: core/androidNative/src + + Origin: implementation is based on the bionic project. + License: BSD (license/thirdparty/bionic_license.txt) + +JetBrains Kotlin Atomicfu +https://github.com/Kotlin/kotlinx-atomicfu/blob/master/LICENSE.txt + +/* + * Copyright 2017 JetBrains s.r.o. + * + * 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. + */ + +JetBrains Kotlinx.serialization +https://github.com/Kotlin/kotlinx.serialization/blob/master/license/LICENSE.txt + +/* + * Copyright 2017-2019 JetBrains s.r.o. + * + * 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. + */ diff --git a/pubnub-matchmaking-kotlin/js-matchmaking/README.md b/pubnub-matchmaking-kotlin/js-matchmaking/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/pubnub-matchmaking-kotlin/js-matchmaking/package_template.json b/pubnub-matchmaking-kotlin/js-matchmaking/package_template.json new file mode 100644 index 000000000..1e316b5c5 --- /dev/null +++ b/pubnub-matchmaking-kotlin/js-matchmaking/package_template.json @@ -0,0 +1,42 @@ +{ + "description": "PubNub JavaScript Matchmaking SDK", + "author": "PubNub ", + "license": "SEE LICENSE IN LICENSE", + "repository": { + "type": "git", + "url": "git+https://github.com/pubnub/kotlin/pubnub-matchmaking-kotlin.git" + }, + "bugs": { + "url": "https://github.com/pubnub/kotlin/issues" + }, + "homepage": "https://github.com/pubnub/kotlin/pubnub-matchmaking-kotlin/js-matchmaking#readme", + "files": [ + "dist", "NOTICE" + ], + "scripts": { + "test": "cp -r dist/ dist-test && rollup --config rollup-test.config.mjs && jest --forceExit", + "build": "rollup -c", + "dev": "tsc -w" + }, + "devDependencies": { + "@babel/core": "7.22.15", + "@babel/preset-env": "7.22.5", + "@babel/preset-typescript": "7.22.5", + "@rollup/plugin-replace": "^5.0.2", + "@rollup/plugin-terser": "^0.4.3", + "@rollup/plugin-commonjs": "28.0.1", + "@types/jest": "29.5.0", + "babel-jest": "29.5.0", + "dotenv": "16.0.3", + "jest": "29.5.0", + "jest-junit": "16.0.0", + "jest-silent-reporter": "0.6.0", + "rollup": "^3.29.2", + "rollup-plugin-ts": "^3.4.5", + "typescript": "4.9.5" + }, + "main": "dist/pubnub-pubnub-matchmaking-kotlin.js", + "module": "dist/pubnub-pubnub-matchmaking-kotlin.es.js", + "types": "dist/pubnub-pubnub-matchmaking-kotlin.d.ts", + "react-native": "dist/pubnub-pubnub-matchmaking-kotlin.es.js" +} diff --git a/pubnub-matchmaking-kotlin/js-matchmaking/rollup.config.mjs b/pubnub-matchmaking-kotlin/js-matchmaking/rollup.config.mjs new file mode 100644 index 000000000..7bdfeb3b2 --- /dev/null +++ b/pubnub-matchmaking-kotlin/js-matchmaking/rollup.config.mjs @@ -0,0 +1,23 @@ +// release script on github call "build" defined in package.json that in turn call this file that created two distribution cjs and esm +import pkg from "./package.json" assert { type: "json" } +import terser from "@rollup/plugin-terser" + +export default [ + { + input: "../build/packages/js/pubnub-pubnub-matchmaking-kotlin.mjs", // generated by kotlinJS + external: ["pubnub", "format-util"], + output: [ + { + file: pkg.main, + format: "cjs", + }, + { + file: pkg.module, + format: "esm", + }, + ], + plugins: [ + terser() + ], + }, +] diff --git a/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-api/api/pubnub-matchmaking-kotlin-api.api b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-api/api/pubnub-matchmaking-kotlin-api.api new file mode 100644 index 000000000..e69de29bb diff --git a/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-api/build.gradle.kts b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-api/build.gradle.kts new file mode 100644 index 000000000..1106458ff --- /dev/null +++ b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-api/build.gradle.kts @@ -0,0 +1,52 @@ +import com.pubnub.gradle.enableAnyIosTarget // why ENABLE_TARGET_IOS_OTHER=false +import com.pubnub.gradle.enableJsTarget + +plugins { + alias(libs.plugins.benmanes.versions) + id("pubnub.shared") + id("pubnub.dokka") + id("pubnub.multiplatform") // adds plugin to enables KMP + id("pubnub.ios-simulator-test") // todo what is this -> do odpalania testów na symulatorze ios +} + +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + api(project(":pubnub-kotlin:pubnub-kotlin-api")) + // todo do sprawdzania na projekcie z niższym kotlinem wymuszając niższa wersję odpowiednią komenda + implementation(libs.kotlinx.atomicfu) // todo in kotlin 2.1.2 this will be in standard library + } + } + val jvmMain by getting { + dependencies { + api(libs.retrofit2) + api(libs.okhttp) + api(libs.okhttp.logging) + api(libs.gson) + implementation(libs.slf4j) + } + } + if (enableAnyIosTarget) { + val appleMain by getting { + dependencies { + } + } + } + + if (enableJsTarget) { + val jsMain by getting { + dependencies { + } + } + } + + val commonTest by getting { + dependencies { +// implementation(project(":pubnub-kotlin:pubnub-kotlin-test")) // todo not needed for now + implementation(kotlin("test")) + implementation(libs.kotlinx.coroutines.test) + } + } + } +} diff --git a/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-api/src/commonMain/kotlin/com/pubnub/matchmaking/Matchmaking.kt b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-api/src/commonMain/kotlin/com/pubnub/matchmaking/Matchmaking.kt new file mode 100644 index 000000000..cce742483 --- /dev/null +++ b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-api/src/commonMain/kotlin/com/pubnub/matchmaking/Matchmaking.kt @@ -0,0 +1,71 @@ +package com.pubnub.matchmaking + +import com.pubnub.api.PubNub +import com.pubnub.api.models.consumer.objects.PNKey +import com.pubnub.api.models.consumer.objects.PNPage +import com.pubnub.api.models.consumer.objects.PNSortKey +import com.pubnub.kmp.CustomObject +import com.pubnub.kmp.PNFuture +import com.pubnub.matchmaking.entities.FindMatchResult +import com.pubnub.matchmaking.entities.GetUsersResponse +import com.pubnub.matchmaking.entities.MatchmakingStatus + +interface Matchmaking { + val pubNub: PubNub + + fun createUser( + id: String, + name: String? = null, + externalId: String? = null, + profileUrl: String? = null, + email: String? = null, + custom: CustomObject? = null, + status: String? = null, + type: String? = null, + ): PNFuture + + fun getUser(userId: String): PNFuture + + /** + * Returns a paginated list of all users and their details + * + * @param filter Expression used to filter the results. Returns only these users whose properties satisfy the + * given expression are returned. The filtering language is defined in [documentation](https://www.pubnub.com/docs/general/metadata/filtering). + * @param sort A collection to specify the sort order. Available options are id, name, and updated. Use asc or desc + * @param limit Number of objects to return in response. The default (and maximum) value is 100. + * @param page Object used for pagination to define which previous or next result page you want to fetch. + * + * @return [PNFuture] containing a set of users with pagination information (next, prev, total). + */ + fun getUsers( + filter: String? = null, + sort: Collection> = listOf(), + limit: Int? = null, + page: PNPage? = null, + ): GetUsersResponse + + fun updateUser( + id: String, + // TODO change nulls to Optionals when there is support. In Kotlin SDK there should be possibility to handle PatchValue + name: String? = null, + externalId: String? = null, + profileUrl: String? = null, + email: String? = null, + custom: CustomObject? = null, + status: String? = null, + type: String? = null, + ): PNFuture + + fun deleteUser(id: String, soft: Boolean = false): PNFuture + + fun findMatch(userId: String): PNFuture + + fun findMatch(userId: String, callback: ((MatchmakingStatus) -> Unit)?): PNFuture + + fun getStatus(userId: String): PNFuture + + fun cancelMatchmaking(userId: String): PNFuture + + // todo implement + fun addMissingUserToMatch(userId: String): PNFuture +} diff --git a/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-api/src/commonMain/kotlin/com/pubnub/matchmaking/User.kt b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-api/src/commonMain/kotlin/com/pubnub/matchmaking/User.kt new file mode 100644 index 000000000..d5fdbbe9a --- /dev/null +++ b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-api/src/commonMain/kotlin/com/pubnub/matchmaking/User.kt @@ -0,0 +1,70 @@ +package com.pubnub.matchmaking + +import com.pubnub.kmp.CustomObject +import com.pubnub.kmp.PNFuture + +// todo add kdoc +interface User { + val matchmaking: Matchmaking + val id: String + val name: String? + val externalId: String? + val profileUrl: String? + val email: String? + val custom: Map? + val status: String? + val type: String? + val updated: String? + val eTag: String? + + fun update( + name: String? = null, + externalId: String? = null, + profileUrl: String? = null, + email: String? = null, + custom: CustomObject? = null, + status: String? = null, + type: String? = null, + ): PNFuture + + fun update( + updateAction: UpdatableValues.( + user: User + ) -> Unit + ): PNFuture + + fun delete(soft: Boolean = false): PNFuture + + class UpdatableValues( + /** + * The new value for [User.name]. + */ + var name: String? = null, + /** + * The new value for [User.externalId]. + */ + var externalId: String? = null, + /** + * The new value for [User.profileUrl]. + */ + var profileUrl: String? = null, + /** + * The new value for [User.email]. + */ + var email: String? = null, + /** + * The new value for [User.custom]. + */ + var custom: CustomObject? = null, + /** + * The new value for [User.status]. + */ + var status: String? = null, + /** + * The new value for [User.type]. + */ + var type: String? = null, + ) + + companion object +} diff --git a/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-api/src/commonMain/kotlin/com/pubnub/matchmaking/entities/FindMatchResult.kt b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-api/src/commonMain/kotlin/com/pubnub/matchmaking/entities/FindMatchResult.kt new file mode 100644 index 000000000..aefcba00f --- /dev/null +++ b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-api/src/commonMain/kotlin/com/pubnub/matchmaking/entities/FindMatchResult.kt @@ -0,0 +1,6 @@ +package com.pubnub.matchmaking.entities + +class FindMatchResult( + val result: String, + val disconnect: AutoCloseable? +) diff --git a/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-api/src/commonMain/kotlin/com/pubnub/matchmaking/entities/GetUsersResponse.kt b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-api/src/commonMain/kotlin/com/pubnub/matchmaking/entities/GetUsersResponse.kt new file mode 100644 index 000000000..d95025968 --- /dev/null +++ b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-api/src/commonMain/kotlin/com/pubnub/matchmaking/entities/GetUsersResponse.kt @@ -0,0 +1,11 @@ +package com.pubnub.matchmaking.entities + +import com.pubnub.api.models.consumer.objects.PNPage +import com.pubnub.matchmaking.User + +class GetUsersResponse( + val users: List, + val next: PNPage.PNNext?, + val prev: PNPage.PNPrev?, + val total: Int, +) diff --git a/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-api/src/commonMain/kotlin/com/pubnub/matchmaking/entities/MatchmakingCallback.kt b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-api/src/commonMain/kotlin/com/pubnub/matchmaking/entities/MatchmakingCallback.kt new file mode 100644 index 000000000..10169a67c --- /dev/null +++ b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-api/src/commonMain/kotlin/com/pubnub/matchmaking/entities/MatchmakingCallback.kt @@ -0,0 +1,9 @@ +package com.pubnub.matchmaking.entities + +interface MatchmakingCallback { + fun onMatchFound(match: MatchmakingGroup?) + + fun onMatchmakingFailed(reason: String?) + + fun onStatusChange(status: MatchmakingStatus?) +} diff --git a/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-api/src/commonMain/kotlin/com/pubnub/matchmaking/entities/MatchmakingGroup.kt b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-api/src/commonMain/kotlin/com/pubnub/matchmaking/entities/MatchmakingGroup.kt new file mode 100644 index 000000000..7b8850556 --- /dev/null +++ b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-api/src/commonMain/kotlin/com/pubnub/matchmaking/entities/MatchmakingGroup.kt @@ -0,0 +1,5 @@ +package com.pubnub.matchmaking.entities + +import com.pubnub.matchmaking.User + +class MatchmakingGroup(val users: Set) // todo currently we returns Set. What about Set ? diff --git a/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-api/src/commonMain/kotlin/com/pubnub/matchmaking/entities/MatchmakingStatus.kt b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-api/src/commonMain/kotlin/com/pubnub/matchmaking/entities/MatchmakingStatus.kt new file mode 100644 index 000000000..22086523e --- /dev/null +++ b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-api/src/commonMain/kotlin/com/pubnub/matchmaking/entities/MatchmakingStatus.kt @@ -0,0 +1,20 @@ +package com.pubnub.matchmaking.entities + +enum class MatchmakingStatus { + IN_QUEUE, + MATCHMAKING_STARTED, + RE_ADDED_TO_QUEUE, + MATCH_FOUND, + CANCELLED, // todo how to cancel matchmaking? + FAILED, + UNKNOWN, + + INITIALLY_MATCHED, // todo do we need it, + WAITING_FOR_CONFIRMATION; // todo do we need it, + + companion object { + fun fromString(value: String): MatchmakingStatus { + return values().find { it.name.equals(value, ignoreCase = true) } ?: UNKNOWN + } + } +} diff --git a/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/build.gradle.kts b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/build.gradle.kts new file mode 100644 index 000000000..6fe564076 --- /dev/null +++ b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/build.gradle.kts @@ -0,0 +1,76 @@ +@file:OptIn(ExperimentalKotlinGradlePluginApi::class) + +import com.pubnub.gradle.enableAnyIosTarget +import com.pubnub.gradle.enableJsTarget +import com.pubnub.gradle.tasks.GenerateVersionTask +import org.gradle.kotlin.dsl.register +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.plugin.cocoapods.CocoapodsExtension + +plugins { + alias(libs.plugins.kotlinx.atomicfu) // todo do we need lib for atomic operations + id("pubnub.ios-simulator-test") // start x-code ios simulator before ios test run + id("pubnub.shared") + id("pubnub.dokka") + id("pubnub.multiplatform") +// alias(libs.plugins.mokkery) // todo downgrade version in libs.version.toml to be compatible with used kotlin version(2.0.21) +} + +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + api(libs.kotlinx.coroutines.core) // todo this is needed for Service that simulates matchmaking REST api call + implementation(project(":pubnub-matchmaking-kotlin:pubnub-matchmaking-kotlin-api")) + implementation(project(":pubnub-kotlin:pubnub-kotlin-api")) + implementation(libs.kotlinx.atomicfu) // todo this is needed for Service that simulates matchmaking REST api call + implementation(libs.touchlab.kermit) + } + } + + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + implementation(project(":pubnub-kotlin:pubnub-kotlin-test")) + implementation(project(":pubnub-kotlin:pubnub-kotlin-impl")) // this is required to have access to e.g. com.pubnub.internal.v2.PNConfigurationImpl$Builder + } + } + + val jvmMain by getting { + dependencies { +// implementation(project(":pubnub-kotlin")) //todo it shouldn't be required but because of some error might be + implementation(kotlin("test-junit")) + } + } + + if (enableJsTarget) { + val jsTest by getting { + dependencies { + implementation(kotlin("test-js")) // to jest potrzebne bo testy KMP na targecie JS nie działały, mimo, że powinny + } + } + } + } + + if (enableAnyIosTarget) { + (this as ExtensionAware).extensions.configure { + summary = "Some description for a Kotlin/Native module" + homepage = "Link to a Kotlin/Native module homepage" + } + } +} + +val generateVersion = + tasks.register("generateVersion") { + fileName.set("MatchmakingVersion") + packageName.set("com.pubnub.matchmaking.internal") + constName.set("PUBNUB_MATCHMAKING_VERSION") + version.set(providers.gradleProperty("VERSION_NAME")) + outputDirectory.set( + layout.buildDirectory.map { + it.dir("generated/sources/generateVersion") + }, + ) + } + +kotlin.sourceSets.getByName("commonMain").kotlin.srcDir(generateVersion) diff --git a/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/appleMain/kotlin/com/pubnub/matchmaking/internal/util/Utils.apple.kt b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/appleMain/kotlin/com/pubnub/matchmaking/internal/util/Utils.apple.kt new file mode 100644 index 000000000..32425f83f --- /dev/null +++ b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/appleMain/kotlin/com/pubnub/matchmaking/internal/util/Utils.apple.kt @@ -0,0 +1,8 @@ +package com.pubnub.matchmaking.internal.util + +import platform.Foundation.NSString +import platform.Foundation.stringByRemovingPercentEncoding + +internal actual fun urlDecode(encoded: String): String { + return (encoded as NSString).stringByRemovingPercentEncoding().orEmpty() +} \ No newline at end of file diff --git a/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/UserImpl.kt b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/UserImpl.kt new file mode 100644 index 000000000..ce6af6115 --- /dev/null +++ b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/UserImpl.kt @@ -0,0 +1,66 @@ +package com.pubnub.matchmaking.internal + +import com.pubnub.api.models.consumer.objects.uuid.PNUUIDMetadata +import com.pubnub.kmp.CustomObject +import com.pubnub.kmp.PNFuture +import com.pubnub.matchmaking.Matchmaking +import com.pubnub.matchmaking.User + +data class UserImpl( + override val matchmaking: Matchmaking, + override val id: String, + override val name: String? = null, + override val externalId: String? = null, + override val profileUrl: String? = null, + override val email: String? = null, + override val custom: Map? = null, + override val status: String? = null, + override val type: String? = null, + override val updated: String? = null, + override val eTag: String? = null, +) : User { + override fun update( + name: String?, + externalId: String?, + profileUrl: String?, + email: String?, + custom: CustomObject?, + status: String?, + type: String? + ): PNFuture { + return matchmaking.updateUser( + id, + name, + externalId, + profileUrl, + email, + custom, + status, + type + ) + } + + override fun update(updateAction: User.UpdatableValues.(user: User) -> Unit): PNFuture { + TODO("Not yet implemented") + } + + override fun delete(soft: Boolean): PNFuture { + TODO("Not yet implemented") + } + + companion object { + internal fun fromDTO(matchmaking: Matchmaking, user: PNUUIDMetadata): User = UserImpl( + matchmaking = matchmaking, + id = user.id, + name = user.name?.value, + externalId = user.externalId?.value, + profileUrl = user.profileUrl?.value, + email = user.email?.value, + custom = user.custom?.value, + updated = user.updated?.value, + status = user.status?.value, + type = user.type?.value, + eTag = user.eTag?.value + ) + } +} diff --git a/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/common/Constants.kt b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/common/Constants.kt new file mode 100644 index 000000000..ee9ce023a --- /dev/null +++ b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/common/Constants.kt @@ -0,0 +1,3 @@ +package com.pubnub.matchmaking.internal.common + +internal const val USER_STATUS_CHANNEL_PREFIX = "MATCHMAKING_STATUS." diff --git a/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/sdk/MatchmakingImpl.kt b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/sdk/MatchmakingImpl.kt new file mode 100644 index 000000000..205aa1d43 --- /dev/null +++ b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/sdk/MatchmakingImpl.kt @@ -0,0 +1,216 @@ +package com.pubnub.matchmaking.internal.sdk + +import co.touchlab.kermit.Logger +import com.pubnub.api.PubNub +import com.pubnub.api.PubNubException +import com.pubnub.api.asMap +import com.pubnub.api.asString +import com.pubnub.api.models.consumer.PNBoundedPage +import com.pubnub.api.models.consumer.history.PNFetchMessageItem +import com.pubnub.api.models.consumer.objects.PNKey +import com.pubnub.api.models.consumer.objects.PNPage +import com.pubnub.api.models.consumer.objects.PNSortKey +import com.pubnub.api.models.consumer.objects.uuid.PNUUIDMetadataResult +import com.pubnub.api.v2.callbacks.Result +import com.pubnub.kmp.CustomObject +import com.pubnub.kmp.PNFuture +import com.pubnub.kmp.asFuture +import com.pubnub.kmp.catch +import com.pubnub.kmp.createEventListener +import com.pubnub.kmp.then +import com.pubnub.kmp.thenAsync +import com.pubnub.matchmaking.Matchmaking +import com.pubnub.matchmaking.User +import com.pubnub.matchmaking.entities.FindMatchResult +import com.pubnub.matchmaking.entities.GetUsersResponse +import com.pubnub.matchmaking.entities.MatchmakingStatus +import com.pubnub.matchmaking.internal.UserImpl +import com.pubnub.matchmaking.internal.common.USER_STATUS_CHANNEL_PREFIX +import com.pubnub.matchmaking.internal.util.channelsUrlDecoded +import com.pubnub.matchmaking.internal.util.logErrorAndReturnException +import com.pubnub.matchmaking.internal.util.nullOn404 +import com.pubnub.matchmaking.internal.util.pnError +import com.pubnub.matchmaking.server.MatchmakingRestService + +class MatchmakingImpl(override val pubNub: PubNub) : Matchmaking { + private val matchmakingRestService: MatchmakingRestService = MatchmakingRestService(pubNub) + + companion object { + private val log = Logger.withTag("MatchmakingImpl") + + private const val ID_IS_REQUIRED = "Id is required" + private const val USER_ID_ALREADY_EXIST = "User with this ID already exists" + private const val FAILED_TO_CREATE_UPDATE_USER_DATA = "Failed to create/update user data." + } + + override fun createUser( + id: String, + name: String?, + externalId: String?, + profileUrl: String?, + email: String?, + custom: CustomObject?, // todo change to CustomObject ? + status: String?, + type: String?, + ): PNFuture { + if (!isValidId(id)) { + return log.logErrorAndReturnException(ID_IS_REQUIRED).asFuture() + } + + return getUser(id).thenAsync { user: User? -> + if (user != null) { + log.pnError(USER_ID_ALREADY_EXIST) + } else { + setUserMetadata( + id = id, + name = name, + externalId = externalId, + profileUrl = profileUrl, + email = email, + custom = custom, + type = type, + status = status + ) + } + } + } + + override fun getUser(userId: String): PNFuture { + if (!isValidId(userId)) { + return log.logErrorAndReturnException(ID_IS_REQUIRED).asFuture() + } + + return pubNub.getUUIDMetadata(uuid = userId, includeCustom = true) + .then { pnUUIDMetadataResult: PNUUIDMetadataResult -> + UserImpl.fromDTO(this, pnUUIDMetadataResult.data) + }.nullOn404() + } + + override fun getUsers( + filter: String?, + sort: Collection>, + limit: Int?, + page: PNPage? + ): GetUsersResponse { + TODO("Not yet implemented") + } + + override fun updateUser( + id: String, + name: String?, + externalId: String?, + profileUrl: String?, + email: String?, + custom: CustomObject?, + status: String?, + type: String? + ): PNFuture { + if (!isValidId(id)) { + throw PubNubException("Id is required") // todo is throwing ok ere + } + + TODO("Not yet implemented") + } + + override fun deleteUser(id: String, soft: Boolean): PNFuture { + TODO("Not yet implemented") + } + + // todo how to make sure that userId is unique? + override fun findMatch(userId: String): PNFuture { + return matchmakingRestService.findMatch(userId) + } + + override fun findMatch(userId: String, callback: ((MatchmakingStatus) -> Unit)?): PNFuture { + val disconnectListener: AutoCloseable? = callback?.let { createListenerAndStartListeningForStatus(userId, it) } + + return matchmakingRestService.findMatch(userId = userId, withDelay = true).then { result: String -> + FindMatchResult(result, disconnectListener) + } + } + + override fun getStatus(userId: String): PNFuture { + // last message in channel points to current status + val userStatusChannelId = USER_STATUS_CHANNEL_PREFIX + userId + return pubNub.fetchMessages(channels = listOf(userStatusChannelId), page = PNBoundedPage(limit = 1)) + .then { pnFetchMessagesResult -> + val pnFetchMessageItems: List = + pnFetchMessagesResult.channelsUrlDecoded[userStatusChannelId] ?: emptyList() + if (pnFetchMessageItems == emptyList()) { + MatchmakingStatus.UNKNOWN + } else { + val statusAsString: String = pnFetchMessageItems.first().message.asMap()?.get("status")?.asString() + ?: MatchmakingStatus.UNKNOWN.toString() + MatchmakingStatus.fromString(statusAsString) + } + } + } + + override fun cancelMatchmaking(userId: String): PNFuture { + TODO("Not yet implemented") + } + + override fun addMissingUserToMatch(userId: String): PNFuture { + TODO("Not yet implemented") + } + + private fun setUserMetadata( + id: String, + name: String?, + externalId: String?, + profileUrl: String?, + email: String?, + custom: CustomObject?, + type: String? = null, + status: String? = null, + ): PNFuture { + return pubNub.setUUIDMetadata( + uuid = id, + name = name, + externalId = externalId, + profileUrl = profileUrl, + email = email, + custom = custom, + includeCustom = true, + type = type, + status = status + ).then { pnUUIDMetadataResult -> + UserImpl.fromDTO(this, pnUUIDMetadataResult.data) + }.catch { exception -> + Result.failure(PubNubException(FAILED_TO_CREATE_UPDATE_USER_DATA, exception)) + } + } + + private fun createListenerAndStartListeningForStatus( + userId: String, + callback: (MatchmakingStatus) -> Unit + ): AutoCloseable { + val userStatusChannelName = USER_STATUS_CHANNEL_PREFIX + userId + + println("-=listening on userStatusChannelName: $userStatusChannelName") + val channelEntity = pubNub.channel(userStatusChannelName) + val subscription = channelEntity.subscription() + val listener = createEventListener( + pubnub = pubNub, + onMessage = { _, pnMessageResult -> + println("-=in on message") + try { + println("-=pnMessageResult: ${pnMessageResult.message}") + val statusAsString: String = pnMessageResult.message.asMap()?.get("status")?.asString() + ?: MatchmakingStatus.UNKNOWN.toString() + callback(MatchmakingStatus.fromString(statusAsString)) + } catch (e: Exception) { + // todo add log + println("-=Error handling onMessage event") + } + }, + ) + subscription.addListener(listener) + subscription.subscribe() + return subscription + } +} + +internal fun isValidId(id: String): Boolean { + return id.isNotEmpty() +} diff --git a/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/serverREST/MatchmakingRestService.kt b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/serverREST/MatchmakingRestService.kt new file mode 100644 index 000000000..70eca85ba --- /dev/null +++ b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/serverREST/MatchmakingRestService.kt @@ -0,0 +1,339 @@ +package com.pubnub.matchmaking.server + +import com.pubnub.api.PubNub +import com.pubnub.api.PubNubException +import com.pubnub.api.models.consumer.objects.uuid.PNUUIDMetadataResult +import com.pubnub.api.v2.callbacks.Result +import com.pubnub.kmp.PNFuture +import com.pubnub.kmp.asFuture +import com.pubnub.matchmaking.User +import com.pubnub.matchmaking.entities.MatchmakingStatus +import com.pubnub.matchmaking.internal.common.USER_STATUS_CHANNEL_PREFIX +import com.pubnub.matchmaking.internal.serverREST.entities.MatchGroup +import com.pubnub.matchmaking.internal.serverREST.entities.MatchmakingResult +import com.pubnub.matchmaking.internal.serverREST.entities.UserPairWithScore +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.math.abs + +private const val FIND_MATCH_REQUEST_ACCEPTED = "accepted" + +// this class represents server-side REST API +class MatchmakingRestService( // todo do we need this? + private val pubNub: PubNub, + private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default) +) { + // Use a mutable map guarded by a Mutex for thread safety + private val matchmakingQueue = mutableSetOf() + private val queueMutex = Mutex() + private var processingQueueInProgress = false + + // todo get Set instead of userId + internal fun findMatch(userId: String, withDelay: Boolean = false): PNFuture = + if (!isValidId(userId)) { + PubNubException("Id is required").asFuture() + } else { + PNFuture { callback -> + scope.launch { + //added delay so that callback (if provided is subscribe) + if (withDelay) { + delay(3000L) // todo delay because we need to make sure that status callback is subscribed before we send status message + // we could listen on status listener e.g. use createStatusListener and handle listener removal after status indicate subscription subscribe + } + +// val statusListener = createStatusListener(pubNub) { _, pnStatus -> +// if ((pnStatus.category == PNStatusCategory.PNConnectedCategory || pnStatus.category == PNStatusCategory.PNSubscriptionChanged) && +// pnStatus.affectedChannels.contains(USER_STATUS_CHANNEL_PREFIX + userId) +// ) { +// cont.resume(subscription) +// } +// if (pnStatus.category == PNStatusCategory.PNUnexpectedDisconnectCategory || pnStatus.category == PNStatusCategory.PNConnectionError) { +// cont.resumeWithException(pnStatus.exception ?: RuntimeException(pnStatus.category.toString())) +// } +// } +// +// pubNub.removeListener(statusListener) + + + try { + // check if user exists, if not then throw + val user = getUserMetadata(userId) + val result = findMatchInternal(userId) + callback.accept(Result.success(result)) + } catch (e: Exception) { + callback.accept(Result.failure(e)) + } + } + } + } + + // Registers a user for matchmaking. + private suspend fun findMatchInternal(userId: String): String { + queueMutex.withLock { + if (matchmakingQueue.contains(userId)) { + // todo throw exception ? + return "-=User already registered for matchmaking. Duplication not permitted." + } + matchmakingQueue.add(userId) + + // todo + // w SDK będzie metoda getStatus, która będzie odczytywała ostatnią wiadomość z tego kanału + // w SDK będzie metoda getStatus(callback), która będzie się subskrybowała na ten kanał i zwracała aktualny status + + setMatchMakingStatus(userId, MatchmakingStatus.IN_QUEUE) + } +// ensureMatchmakingIsRunning() //todo uncomment + return FIND_MATCH_REQUEST_ACCEPTED + } + + // matchmaking status for user will be stored in channel as a last message + private suspend fun setMatchMakingStatus(userId: String, status: MatchmakingStatus) { + println("-=setMatchMakingStatus to channel $USER_STATUS_CHANNEL_PREFIX$userId: $status") + val statusMap = buildMap { + put("status", status.toString()) + } + + pubNub.publish( + channel = USER_STATUS_CHANNEL_PREFIX + userId, + message = statusMap + ).await() + } + + // Starts a coroutine loop to process the matchmaking queue when enough users are available. + private suspend fun ensureMatchmakingIsRunning() { + queueMutex.withLock { + if (matchmakingQueue.size >= 2 && !processingQueueInProgress) { // todo 2 should depend on number of users in match + processingQueueInProgress = true + scope.launch { + try { + while (processingQueueInProgress) { + processMatchmakingQueue() + delay(5000L) // Wait 5 seconds between processing + } + } finally { + processingQueueInProgress = false + } + } + } + } + } + + // Processes the matchmaking queue by removing all user IDs for pairing from the queue + private suspend fun processMatchmakingQueue() { + val userIds: Set = queueMutex.withLock { + if (matchmakingQueue.size < 2) { // todo this should be configurable based on number of player in match + return + } + val ids = matchmakingQueue + matchmakingQueue.removeAll(ids) + ids + } + if (userIds.isNotEmpty()) { + setMatchmakingStatusForUsers(userIds, MatchmakingStatus.MATCHMAKING_STARTED) + performMatchmaking(userIds) + } + // If not enough users remain, stop processing. + queueMutex.withLock { + if (matchmakingQueue.size < 2) { + processingQueueInProgress = false + } + } + } + + private suspend fun setMatchmakingStatusForUsers(userIds: Set, status: MatchmakingStatus) { + userIds.forEach { userId -> + setMatchMakingStatus(userId, status) + } + } + + // Executes matchmaking on the given set of user IDs. + private suspend fun performMatchmaking(userIds: Set) { + val users = getUsersByIds(userIds) + val matchmakingResult: MatchmakingResult = pairUsersBySkill(users.toList()) + notifyAboutSuccessfulMatch(matchmakingResult.matchGroups) + addUnmatchedUsersIdsBackToQueue(matchmakingResult.unmatchedUserIds) + } + + private suspend fun getUsersByIds(userIds: Set): Set { + TODO("Not yet implemented") +// val users = mutableSetOf() +// userIds.forEach { userId -> +// val pnUuidMetadataResult: PNUUIDMetadataResult = getUserMetadata(userId) +// val user = UserImpl.fromDTO(matchmaking = matchmaking, user = pnUuidMetadataResult.data) +// users.add(user) +// } +// return users + } + + private suspend fun getUserMetadata(userId: String): PNUUIDMetadataResult { + val pnUuidMetadataResult: PNUUIDMetadataResult + try { + pnUuidMetadataResult = pubNub.getUUIDMetadata(uuid = userId, includeCustom = true).await() + } catch (e: PubNubException) { + if (e.statusCode == 404) { + // Log.error + println("User does not exist in AppContext") + throw PubNubException("getUsersByIds: User does not exist") + } else { + throw PubNubException(e.message) + } + } + return pnUuidMetadataResult + } + + private suspend fun addUnmatchedUsersIdsBackToQueue(unmatchedUserIds: Set) { + if (unmatchedUserIds.isEmpty()) { + println("No unmatched users to re-add to matchmaking queue") + return + } + println("Adding ${unmatchedUserIds.size} unmatched users back to matchmaking queue.") + queueMutex.withLock { + unmatchedUserIds.forEach { userId -> + if (!matchmakingQueue.contains(userId)) { + matchmakingQueue.add(userId) + println("- Added $userId back to matchmaking queue.") + // todo we could introduce count of how many time userId has been re-added to queue + setMatchMakingStatus(userId, MatchmakingStatus.RE_ADDED_TO_QUEUE) + } + } + } + } + + private suspend fun notifyAboutSuccessfulMatch(matchGroups: Set) { + matchGroups.forEach { group -> + group.users.forEach { user -> + println("Found match for userId: ${user.id} in group: ${group.users.map { it.id }}") + setMatchMakingStatus(user.id, MatchmakingStatus.MATCH_FOUND) + // todo pass matchMaking result data + } + } + } + + private fun calculateScore(userA: User, userB: User): Double { + val constraints = Constraints.getConstraints() + // todo those 3 values will be taken from Illuminate + val maxEloGap = constraints["MAX_ELO_GAP"] as Int + val skillGapWeight = constraints["SKILL_GAP_WEIGHT"] as Double + val regionalPriority = constraints["REGIONAL_PRIORITY"] as Double + + val eloA = (userA.custom?.get("elo") as? Int) ?: 0 + val eloB = (userB.custom?.get("elo") as? Int) ?: 0 + val regionA = (userA.custom?.get("server") as? String) ?: "global" + val regionB = (userB.custom?.get("server") as? String) ?: "global" + + val eloDifference = abs(eloA - eloB) + if (eloDifference > maxEloGap) { + return Double.POSITIVE_INFINITY + } + + val regionMismatchPenalty = if (regionA == regionB) { + 0.0 + } else { + regionalPriority + } + + return skillGapWeight * eloDifference + regionMismatchPenalty + } + + // Create all possible pairs (only including allowed pairs) + private fun createAllPairs(users: List): List { + val pairs = mutableListOf() + for (i in users.indices) { + for (j in i + 1 until users.size) { + val score = calculateScore(users[i], users[j]) + if (score != Double.POSITIVE_INFINITY) { + pairs.add(UserPairWithScore(i, j, score)) + } + } + } + return pairs + } + + // Greedy pairing: sort by score and select pairs without conflicts. +// Instead of returning MatchmakingPair, we create a MatchGroup that holds a set of users. + + private fun pairUsersBySkill(users: List, groupSize: Int = 2): MatchmakingResult { + // If there aren’t enough users to form a single group, return them as unpaired. + if (users.size < groupSize) { + return MatchmakingResult(emptySet(), users.map { it.id }.toSet()) + } + + // For pairs, use the existing logic with explicit naming via UserPair. + if (groupSize == 2) { + val allPairs = createAllPairs(users).sortedBy { it.score } + val pairedIndices = mutableSetOf() + val groups = mutableSetOf() + + for (pair in allPairs) { + if (pair.firstUserIndex !in pairedIndices && pair.secondUserIndex !in pairedIndices) { + groups.add( + MatchGroup( + setOf( + users[pair.firstUserIndex], + users[pair.secondUserIndex] + ) + ) + ) + pairedIndices.add(pair.firstUserIndex) + pairedIndices.add(pair.secondUserIndex) + } + } + val unpaired = users.indices.filter { it !in pairedIndices } + .map { users[it].id }.toSet() + return MatchmakingResult(groups, unpaired) + } else { + // For groups larger than 2, we use a simple greedy grouping. + // First, sort users by their Elo rating (fallback to 0 if missing). + val sortedUsers = users.sortedBy { (it.custom?.get("elo") as? Int) ?: 0 } + val groups = mutableSetOf() + val usedIndices = mutableSetOf() + + // Greedily form groups of the desired size from the sorted list. + var i = 0 + while (i <= sortedUsers.size - groupSize) { + // Create a group from a consecutive sublist. + val groupUsers = sortedUsers.subList(i, i + groupSize).toSet() + groups.add(MatchGroup(groupUsers)) + // Mark these users as grouped. + groupUsers.forEach { user -> + usedIndices.add(users.indexOf(user)) + } + i += groupSize + } + // Any remaining users who didn't fit into a full group are considered unpaired. + val unpaired = users.indices.filter { it !in usedIndices } + .map { users[it].id }.toSet() + return MatchmakingResult(groups, unpaired) + } + } + + private fun isValidId(id: String): Boolean { + return id.isNotEmpty() + } + + private suspend fun PNFuture.await(): T = + suspendCancellableCoroutine { cont -> + async { result -> + result.onSuccess { + cont.resume(it) + }.onFailure { + cont.resumeWithException(it) + } + } + } +} + +object Constraints { + fun getConstraints(): Map = mapOf( + "MAX_ELO_GAP" to 100, + "SKILL_GAP_WEIGHT" to 1.0, + "REGIONAL_PRIORITY" to 10.0 + ) +} diff --git a/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/serverREST/MatchmakingRestServiceNew.kt b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/serverREST/MatchmakingRestServiceNew.kt new file mode 100644 index 000000000..785fb6a13 --- /dev/null +++ b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/serverREST/MatchmakingRestServiceNew.kt @@ -0,0 +1,201 @@ +package com.pubnub.matchmaking.internal.serverREST + +import com.pubnub.api.PubNubException +import com.pubnub.api.models.consumer.objects.uuid.PNUUIDMetadataResult +import com.pubnub.api.v2.callbacks.Result +import com.pubnub.kmp.PNFuture +import com.pubnub.kmp.asFuture +import com.pubnub.matchmaking.Matchmaking +import com.pubnub.matchmaking.User +import com.pubnub.matchmaking.internal.UserImpl +import com.pubnub.matchmaking.internal.serverREST.entities.MatchGroup +import com.pubnub.matchmaking.internal.serverREST.entities.MatchResult +import com.pubnub.matchmaking.server.Constraints +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.math.abs + +// this class represents server-side REST API +class MatchmakingRestServiceNew( + private val matchmaking: Matchmaking, + private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default) +) { + // Instead of a queue, we maintain a list of open match groups. + private val openMatchGroups = mutableListOf() + private val groupsMutex = Mutex() + + fun findMatch(userId: String, requiredMatchSize: Int = 2): PNFuture = + if (!isValidId(userId)) { + PubNubException("Id is required").asFuture() + } else { + PNFuture { callback -> + scope.launch { + try { + // Validate user exists + val userMeta = getUserMetadata(userId) + val user = UserImpl.fromDTO(matchmaking = matchmaking, user = userMeta.data) + // Attempt to join an existing group or create a new one. + val result: MatchResult = findOrCreateMatchGroup(user, requiredMatchSize) + callback.accept(Result.success(result)) + } catch (e: Exception) { + callback.accept(Result.failure(e)) + } + } + } + } + + // Attempts to find a compatible group or creates a new one. + private suspend fun findOrCreateMatchGroup(user: User, requiredSize: Int): MatchResult { + // Create a channel for the current user’s match notification. + val userChannel = Channel(Channel.RENDEZVOUS) + var groupToJoin: OpenMatchGroup? + + groupsMutex.withLock { + // Look for an existing group with the same requiredSize that is compatible. + groupToJoin = openMatchGroups.firstOrNull { group -> + group.requiredSize == requiredSize && isSkillCompatible(user, group) && group.users.size < group.requiredSize + } + if (groupToJoin != null) { + // Join the found group. + groupToJoin!!.users.add(user) + groupToJoin!!.waitingChannels.add(userChannel) + // If the group is now full, create the final match result and notify all waiting users. + if (groupToJoin!!.users.size == groupToJoin!!.requiredSize) { + val finalGroup = MatchGroup(users = groupToJoin!!.users.toSet()) + finalGroup.users.forEach { groupUser -> + println("Found match for user: $groupUser") + } + val matchData = mapOf( + "status" to "matchFound", + "groupSize" to groupToJoin!!.requiredSize.toString() + ) + val matchResult = MatchResult(match = finalGroup, matchData = matchData) + groupToJoin!!.waitingChannels.forEach { channel -> + scope.launch { + channel.send(matchResult) + } + } + openMatchGroups.remove(groupToJoin) + } else { // 'if' must have both main and 'else' branches if used as an expression + Unit + } + } else { + // No suitable group found; create a new one with the specified requiredSize. + groupToJoin = OpenMatchGroup(requiredSize = requiredSize) + groupToJoin!!.users.add(user) + groupToJoin!!.waitingChannels.add(userChannel) + openMatchGroups.add(groupToJoin!!) + } + } + // Wait until a match result is received. + return userChannel.receive() + } + + // factors MAX_ELO_GAP, SKILL_GAP_WEIGHT, REGIONAL_PRIORITY + private fun isSkillCompatible(user: User, group: OpenMatchGroup): Boolean { + // If the group is empty, any user is compatible. + if (group.users.isEmpty()) { + return true + } + + // Retrieve configuration constraints. + val constraints = Constraints.getConstraints() + val maxEloGap = constraints["MAX_ELO_GAP"] as? Int ?: 100 + val skillGapWeight = constraints["SKILL_GAP_WEIGHT"] as? Double ?: 1.0 + val regionalPriority = constraints["REGIONAL_PRIORITY"] as? Double ?: 10.0 + + // Retrieve additional dynamic parameters for threshold calculation. + val baseThreshold = constraints["BASE_THRESHOLD"] as? Double ?: 20.0 + val thresholdIncrement = constraints["THRESHOLD_INCREMENT"] as? Double ?: 5.0 + + // Get the new user's Elo. + val userElo = (user.custom?.get("elo") as? Int) ?: 0 + + // Compute the group's average Elo. + val groupElos: List = group.users.map { (it.custom?.get("elo") as? Int ?: 0) } + val groupAverageElo = groupElos.average() + + // Automatically reject if the difference exceeds the maximum allowed gap. + if (abs(userElo - groupAverageElo) > maxEloGap) { + return false + } + + // Calculate the weighted Elo difference. + val weightedDifference = skillGapWeight * abs(userElo - groupAverageElo) + + // Determine the region for the new user. + val userRegion = (user.custom?.get("server") as? String) ?: "global" + // Calculate the majority region in the group. + val regionCounts = group.users.groupingBy { (it.custom?.get("server") as? String ?: "global") }.eachCount() + val majorityRegion = regionCounts.maxByOrNull { it.value }?.key ?: "global" + val regionMismatchPenalty = if (userRegion == majorityRegion){ + 0.0 + } else { + regionalPriority + } + + // Overall compatibility score. + val compatibilityScore = weightedDifference + regionMismatchPenalty + + // Calculate a dynamic threshold that scales with the number of users already in the group. + // For example, for a 2-player match the threshold might be baseThreshold, + // and for each additional user the tolerance increases by thresholdIncrement. + val dynamicThreshold = baseThreshold + (group.users.size - 1) * thresholdIncrement + + // todo consider + // val waitTime = System.currentTimeMillis() - group.creationTime + // val timeFactor = waitTime / SOME_TIME_UNIT // e.g., seconds or minutes + // val dynamicThreshold = baseThreshold + (group.users.size - 1) * thresholdIncrement + timeFactor + + return compatibilityScore <= dynamicThreshold + } + + + private suspend fun getUserMetadata(userId: String): PNUUIDMetadataResult { + val pnUuidMetadataResult: PNUUIDMetadataResult + try { + pnUuidMetadataResult = matchmaking.pubNub.getUUIDMetadata(uuid = userId, includeCustom = true).await() + } catch (e: PubNubException) { + if (e.statusCode == 404) { + // Log.error + println("User does not exist in AppContext") + throw PubNubException("getUsersByIds: User does not exist") + } else { + throw PubNubException(e.message) + } + } + return pnUuidMetadataResult + } + + private fun isValidId(id: String): Boolean { + return id.isNotEmpty() + } + + private suspend fun PNFuture.await(): T = + suspendCancellableCoroutine { cont -> + async { result -> + result.onSuccess { + cont.resume(it) + }.onFailure { + cont.resumeWithException(it) + } + } + } +} + +class MatchMakingException : Exception() // todo implement + +// todo ad creation time, as time passes the algorithm an gradually incrase the acceptable difference in skill or latency +private data class OpenMatchGroup( + val requiredSize: Int = 2, // For pairing, group size is 2 (can be configurable) + val users: MutableList = mutableListOf(), + // Each waiting user gets a channel to receive the match result. + val waitingChannels: MutableList> = mutableListOf() +) diff --git a/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/serverREST/entities/MatchGroup.kt b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/serverREST/entities/MatchGroup.kt new file mode 100644 index 000000000..76d52aac8 --- /dev/null +++ b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/serverREST/entities/MatchGroup.kt @@ -0,0 +1,5 @@ +package com.pubnub.matchmaking.internal.serverREST.entities + +import com.pubnub.matchmaking.User + +data class MatchGroup(val users: Set) diff --git a/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/serverREST/entities/MatchResult.kt b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/serverREST/entities/MatchResult.kt new file mode 100644 index 000000000..ecfca53da --- /dev/null +++ b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/serverREST/entities/MatchResult.kt @@ -0,0 +1,3 @@ +package com.pubnub.matchmaking.internal.serverREST.entities + +class MatchResult(val match: MatchGroup, val matchData: Map) diff --git a/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/serverREST/entities/MatchmakingResult.kt b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/serverREST/entities/MatchmakingResult.kt new file mode 100644 index 000000000..78790cae2 --- /dev/null +++ b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/serverREST/entities/MatchmakingResult.kt @@ -0,0 +1,3 @@ +package com.pubnub.matchmaking.internal.serverREST.entities + +class MatchmakingResult(val matchGroups: Set, val unmatchedUserIds: Set) diff --git a/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/serverREST/entities/UserPairWithScore.kt b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/serverREST/entities/UserPairWithScore.kt new file mode 100644 index 000000000..721d1d265 --- /dev/null +++ b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/serverREST/entities/UserPairWithScore.kt @@ -0,0 +1,3 @@ +package com.pubnub.matchmaking.internal.serverREST.entities + +data class UserPairWithScore(val firstUserIndex: Int, val secondUserIndex: Int, val score: Double) diff --git a/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/util/Utils.kt b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/util/Utils.kt new file mode 100644 index 000000000..fcb80219d --- /dev/null +++ b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonMain/kotlin/com/pubnub/matchmaking/internal/util/Utils.kt @@ -0,0 +1,41 @@ +package com.pubnub.matchmaking.internal.util + +import co.touchlab.kermit.Logger +import com.pubnub.api.PubNubException +import com.pubnub.api.models.consumer.history.PNFetchMessageItem +import com.pubnub.api.models.consumer.history.PNFetchMessagesResult +import com.pubnub.api.v2.callbacks.Result +import com.pubnub.kmp.PNFuture +import com.pubnub.kmp.catch + +internal const val HTTP_ERROR_404 = 404 +internal expect fun urlDecode(encoded: String): String + +internal val PNFetchMessagesResult.channelsUrlDecoded: Map> + get() = channels.mapKeys { + urlDecode( + it.key + ) + } + +inline fun PubNubException.logErrorAndReturnException(log: Logger): PubNubException = this.apply { + log.e(throwable = this) { this.message.orEmpty() } +} + +inline fun PubNubException.logWarnAndReturnException(log: Logger): PubNubException = this.apply { + log.w(throwable = this) { this.message.orEmpty() } +} + +inline fun Logger.pnError(message: String): Nothing = throw PubNubException(message).logErrorAndReturnException(this) + +inline fun Logger.logErrorAndReturnException(message: String): PubNubException { + return PubNubException(message).logErrorAndReturnException(this) +} + +internal fun PNFuture.nullOn404() = catch { + if (it is PubNubException && it.statusCode == HTTP_ERROR_404) { + Result.success(null) + } else { + Result.failure(it) + } +} \ No newline at end of file diff --git a/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonTest/kotlin/com/pubnub/integration/BaseMatchmakingIntegrationTest.kt b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonTest/kotlin/com/pubnub/integration/BaseMatchmakingIntegrationTest.kt new file mode 100644 index 000000000..6f4cb08f2 --- /dev/null +++ b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonTest/kotlin/com/pubnub/integration/BaseMatchmakingIntegrationTest.kt @@ -0,0 +1,70 @@ +package com.pubnub.integration + +import com.pubnub.matchmaking.User +import com.pubnub.matchmaking.internal.UserImpl +import com.pubnub.matchmaking.internal.sdk.MatchmakingImpl +import com.pubnub.test.BaseIntegrationTest +import com.pubnub.test.await +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.supervisorScope +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext +import kotlin.test.AfterTest +import kotlin.test.BeforeTest + + +abstract class BaseMatchmakingIntegrationTest : BaseIntegrationTest() { + private val usersToRemove = mutableSetOf() + private val userid = randomString() + "!_=-@" +// private val userid = randomString() + lateinit var user1: User + + val matchmaking: MatchmakingImpl by lazy(LazyThreadSafetyMode.NONE) { + MatchmakingImpl(pubnub) + } + + + @BeforeTest + override fun before() { + super.before() // Initializes pubnub, etc. + user1 = UserImpl( + matchmaking = matchmaking, + id = userid, + name = randomString(), + externalId = randomString(), + profileUrl = randomString(), + email = randomString(), + status = randomString(), + type = "type", + ).also { usersToRemove.add(it.id) } + } + + @AfterTest + override fun after(){ + val exceptionHandler = CoroutineExceptionHandler { _, _ -> } + runTest { + supervisorScope { + usersToRemove.forEach { + launch(exceptionHandler) { + pubnub.removeUUIDMetadata(it).await() + } + } + + } + } + + super.after() + } + +} + +internal suspend fun delayInMillis(timeMillis: Long) { + withContext(Dispatchers.Default) { + delay(timeMillis) + } +} + +fun randomString() = (0..6).map { "abcdefghijklmnopqrstuvw".random() }.joinToString("") \ No newline at end of file diff --git a/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonTest/kotlin/com/pubnub/integration/MatchmakingTest.kt b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonTest/kotlin/com/pubnub/integration/MatchmakingTest.kt new file mode 100644 index 000000000..88998a8ba --- /dev/null +++ b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/commonTest/kotlin/com/pubnub/integration/MatchmakingTest.kt @@ -0,0 +1,47 @@ +package com.pubnub.integration + +import com.pubnub.matchmaking.entities.FindMatchResult +import com.pubnub.matchmaking.entities.MatchmakingStatus +import com.pubnub.test.await +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.time.Duration.Companion.seconds + +class MatchmakingTest : BaseMatchmakingIntegrationTest() { + + + @Test // todo make it working for JS and Swift + fun can_get_matchmaking_status_synchronously() = runTest { + val userId = user1.id + val user = matchmaking.createUser(id = userId, name = "userName-$userId").await() + assertEquals(userId, user.id) + + val immediateResponse: String = matchmaking.findMatch(userId).await() + assertEquals("accepted", immediateResponse) + + val status: MatchmakingStatus = matchmaking.getStatus(userId).await() + assertEquals(MatchmakingStatus.IN_QUEUE, status) + } + + @Test + fun can_get_matchmaking_status_using_callback() = runTest(timeout = 10.seconds) { + val userId = user1.id + val callbackCompleted = CompletableDeferred() + + val user = matchmaking.createUser(id = userId, name = "userName-$userId").await() + + val findMatch: FindMatchResult = + matchmaking.findMatch(userId = userId) { matchmakingStatus: MatchmakingStatus -> + callbackCompleted.complete(matchmakingStatus) + }.await() + + assertEquals("accepted", findMatch.result) + assertEquals(MatchmakingStatus.IN_QUEUE, callbackCompleted.await()) + + findMatch.disconnect?.close() + } + +} + diff --git a/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/jsMain/kotlin/com/pubnub/matchmaking/Player.kt b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/jsMain/kotlin/com/pubnub/matchmaking/Player.kt new file mode 100644 index 000000000..68bf572ca --- /dev/null +++ b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/jsMain/kotlin/com/pubnub/matchmaking/Player.kt @@ -0,0 +1,12 @@ +package com.pubnub.matchmaking + +import kotlin.js.ExperimentalJsExport +import kotlin.js.JsExport + +@OptIn(ExperimentalJsExport::class) +@JsExport // todo remove, this is for JS Matchmaking test purposes +class Player(val name: String, val age: Int, val email: String) { + fun printMy() { + println("name: $name, age: $age") + } +} diff --git a/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/jsMain/kotlin/com/pubnub/matchmaking/internal/util/Utils.js.kt b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/jsMain/kotlin/com/pubnub/matchmaking/internal/util/Utils.js.kt new file mode 100644 index 000000000..de64a70d7 --- /dev/null +++ b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/jsMain/kotlin/com/pubnub/matchmaking/internal/util/Utils.js.kt @@ -0,0 +1,5 @@ +package com.pubnub.matchmaking.internal.util + +internal actual fun urlDecode(encoded: String): String = decodeURIComponent(encoded) + +external fun decodeURIComponent(encoded: String): String diff --git a/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/jvmMain/kotlin/com/pubnub/matchmaking/internal/util/Utils.jvm.kt b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/jvmMain/kotlin/com/pubnub/matchmaking/internal/util/Utils.jvm.kt new file mode 100644 index 000000000..5e99efb63 --- /dev/null +++ b/pubnub-matchmaking-kotlin/pubnub-matchmaking-kotlin-impl/src/jvmMain/kotlin/com/pubnub/matchmaking/internal/util/Utils.jvm.kt @@ -0,0 +1,5 @@ +package com.pubnub.matchmaking.internal.util + +import java.net.URLDecoder + +internal actual fun urlDecode(encoded: String): String = URLDecoder.decode(encoded, Charsets.UTF_8.name()) diff --git a/pubnub-matchmaking-kotlin/src/jsMain/kotlin/Empty.kt b/pubnub-matchmaking-kotlin/src/jsMain/kotlin/Empty.kt new file mode 100644 index 000000000..bd5b4bef5 --- /dev/null +++ b/pubnub-matchmaking-kotlin/src/jsMain/kotlin/Empty.kt @@ -0,0 +1 @@ +// this file should exist for JS Matchmaking to build successfully. Seems like Kotlin Gradle bug. \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index be3896955..4e0a1110e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -30,4 +30,7 @@ include("examples:kotlin-app") include("examples:java-app") includeBuild("build-logic/ktlint-custom-rules") includeBuild("migration_utils") - +include("pubnub-matchmaking-kotlin") +include("pubnub-matchmaking-kotlin:pubnub-matchmaking-kotlin-api") +include("pubnub-matchmaking-kotlin:pubnub-matchmaking-kotlin-impl") +//include("pubnub-matchmaking-kotlin:pubnub-matchmaking-kotlin-test") //todo remove