Added rsocket sample code
This commit is contained in:
parent
3c0692394d
commit
c047a903a5
6
.gitignore
vendored
6
.gitignore
vendored
@ -29,3 +29,9 @@ build/
|
|||||||
|
|
||||||
### VS Code ###
|
### VS Code ###
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
dist/
|
5
.prettierrc
Normal file
5
.prettierrc
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"printWidth": 80
|
||||||
|
}
|
9
frontend/src/EchoResponder.ts
Normal file
9
frontend/src/EchoResponder.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export class EchoResponder {
|
||||||
|
callback: any;
|
||||||
|
constructor(callback) {
|
||||||
|
this.callback = callback;
|
||||||
|
}
|
||||||
|
fireAndForget(payload) {
|
||||||
|
this.callback(payload);
|
||||||
|
}
|
||||||
|
}
|
130
frontend/src/main.ts
Normal file
130
frontend/src/main.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import {
|
||||||
|
RSocketClient,
|
||||||
|
JsonSerializer,
|
||||||
|
IdentitySerializer,
|
||||||
|
Encodable,
|
||||||
|
encodeAndAddWellKnownMetadata,
|
||||||
|
encodeAndAddCustomMetadata,
|
||||||
|
MESSAGE_RSOCKET_ROUTING,
|
||||||
|
MESSAGE_RSOCKET_COMPOSITE_METADATA,
|
||||||
|
TEXT_PLAIN,
|
||||||
|
WellKnownMimeType,
|
||||||
|
APPLICATION_JSON,
|
||||||
|
} from "rsocket-core";
|
||||||
|
import { EchoResponder } from "./EchoResponder";
|
||||||
|
import { every } from "rsocket-flowable";
|
||||||
|
import RSocketTcpClient from "rsocket-tcp-client";
|
||||||
|
import RSocketWebSocketClient from "rsocket-websocket-client";
|
||||||
|
|
||||||
|
const maxRSocketRequestN = 2147483647;
|
||||||
|
const host = "127.0.0.1";
|
||||||
|
const port = 7000;
|
||||||
|
const keepAlive = 60000;
|
||||||
|
const lifetime = 180000;
|
||||||
|
const dataMimeType = "application/octet-stream";
|
||||||
|
const metadataMimeType = MESSAGE_RSOCKET_COMPOSITE_METADATA.string;
|
||||||
|
|
||||||
|
const address = { host: "localhost", port: 7000 };
|
||||||
|
|
||||||
|
const messageReceiver = (payload) => {
|
||||||
|
//do what you want to do with received message
|
||||||
|
if ((payload.metadata as string).slice(1) == "user.queue.reply") console.log("YES");
|
||||||
|
else console.log("No");
|
||||||
|
console.log((payload.metadata as string).slice(1))
|
||||||
|
console.log(payload);
|
||||||
|
};
|
||||||
|
const responder = new EchoResponder(messageReceiver);
|
||||||
|
|
||||||
|
function getClientTransport(host: string, port: number) {
|
||||||
|
return new RSocketWebSocketClient({
|
||||||
|
url: "ws://localhost:7000/client-id",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Message {
|
||||||
|
toUser: string;
|
||||||
|
fromUser: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new RSocketClient({
|
||||||
|
// send/receive JSON objects instead of strings/buffers
|
||||||
|
serializers: {
|
||||||
|
data: JsonSerializer,
|
||||||
|
metadata: IdentitySerializer,
|
||||||
|
},
|
||||||
|
setup: {
|
||||||
|
//for connection mapping on server
|
||||||
|
payload: {
|
||||||
|
data: "1234",
|
||||||
|
metadata: String.fromCharCode("client-id".length) + "client-id",
|
||||||
|
},
|
||||||
|
// ms btw sending keepalive to server
|
||||||
|
keepAlive: 60000,
|
||||||
|
|
||||||
|
// ms timeout if no keepalive response
|
||||||
|
lifetime: 180000,
|
||||||
|
|
||||||
|
// format of `data`
|
||||||
|
dataMimeType: "application/json",
|
||||||
|
|
||||||
|
// format of `metadata`
|
||||||
|
metadataMimeType: "message/x.rsocket.routing.v0",
|
||||||
|
},
|
||||||
|
responder: responder,
|
||||||
|
transport: getClientTransport(address.host, address.port),
|
||||||
|
});
|
||||||
|
const route = "user.queue.reply";
|
||||||
|
const sendRoute = "private.news";
|
||||||
|
client.connect().subscribe({
|
||||||
|
onComplete: (rSocket) => {
|
||||||
|
every(1000).subscribe({
|
||||||
|
onNext: (time) => {
|
||||||
|
console.log(`Requester availability: ${rSocket.availability()}`);
|
||||||
|
// rSocket
|
||||||
|
// .requestResponse({
|
||||||
|
// data: time.toString(),
|
||||||
|
// metadata: "",
|
||||||
|
// })
|
||||||
|
// .subscribe({
|
||||||
|
// onComplete: (response) => {
|
||||||
|
// const data = response.data;
|
||||||
|
// if (data) {
|
||||||
|
// console.log(`Requester response: ${data}`);
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// onError: (error) =>
|
||||||
|
// console.log(`Requester error: ${error.message}`),
|
||||||
|
// });
|
||||||
|
|
||||||
|
// rSocket
|
||||||
|
// .requestStream({
|
||||||
|
// metadata: String.fromCharCode(route.length) + route,
|
||||||
|
// })
|
||||||
|
// .subscribe({
|
||||||
|
// onComplete: () => console.log("Request-stream completed"),
|
||||||
|
// onError: (error) =>
|
||||||
|
// console.error(`Request-stream error:${error.message}`),
|
||||||
|
// onNext: (value) => console.log("%s", value.data),
|
||||||
|
// onSubscribe: (sub) => sub.request(maxRSocketRequestN),
|
||||||
|
// });
|
||||||
|
|
||||||
|
rSocket.fireAndForget({
|
||||||
|
data: { toUser: "4567", fromUser: "1234", message: "testHello" },
|
||||||
|
metadata: String.fromCharCode(sendRoute.length) + sendRoute,
|
||||||
|
});
|
||||||
|
// S APPLICATION_JSON.string
|
||||||
|
},
|
||||||
|
onSubscribe: (subscription) =>
|
||||||
|
subscription.request(Number.MAX_SAFE_INTEGER),
|
||||||
|
});
|
||||||
|
console.log("RSocket completed");
|
||||||
|
|
||||||
|
rSocket.connectionStatus().subscribe((status) => {
|
||||||
|
console.log("Connection status:", status);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error) => console.log(`RSocket error: ${error.message}`),
|
||||||
|
});
|
||||||
|
|
||||||
|
// setTimeout(() => {}, 360000);
|
28
package.json
Normal file
28
package.json
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"main": "index.js",
|
||||||
|
"repository": "https://git.arcusiridis.com/nova/Async-Spring-Kotlin-JOOQ-Demo.git",
|
||||||
|
"author": "Rohan Sircar <rohansircar@tuta.io>",
|
||||||
|
"license": "Unlicense",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/rsocket-core": "^0.0.5",
|
||||||
|
"@types/rsocket-flowable": "^0.0.5",
|
||||||
|
"@types/rsocket-tcp-client": "^0.0.1",
|
||||||
|
"@types/rsocket-websocket-client": "^0.0.3",
|
||||||
|
"rsocket-core": "^0.0.19",
|
||||||
|
"rsocket-flowable": "^0.0.14",
|
||||||
|
"rsocket-tcp-client": "^0.0.19",
|
||||||
|
"rsocket-websocket-client": "^0.0.19"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"browserify": "^16.5.2",
|
||||||
|
"tsify": "^5.0.0",
|
||||||
|
"typescript": "^3.9.7",
|
||||||
|
"watchify": "^3.11.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build" : "browserify frontend/src/main.ts --debug -p [ tsify ] -o src/main/resources/static/js/dist/bundle.js",
|
||||||
|
"watch" : "watchify frontend/src/main.ts --debug -p [ tsify ] -o src/main/resources/static/js/dist/bundle.js"
|
||||||
|
}
|
||||||
|
}
|
14
pom.xml
14
pom.xml
@ -103,6 +103,20 @@
|
|||||||
<artifactId>spring-boot-starter-rsocket</artifactId>
|
<artifactId>spring-boot-starter-rsocket</artifactId>
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- https://mvnrepository.com/artifact/io.vavr/vavr -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.vavr</groupId>
|
||||||
|
<artifactId>vavr</artifactId>
|
||||||
|
<version>0.10.3</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- https://mvnrepository.com/artifact/io.vavr/vavr-kotlin -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.vavr</groupId>
|
||||||
|
<artifactId>vavr-kotlin</artifactId>
|
||||||
|
<version>0.10.2</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<build>
|
<build>
|
||||||
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
|
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
|
||||||
|
7
rsocket-commands.txt
Normal file
7
rsocket-commands.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
java -jar rsc.jar --debug --stream --route messages.findAll tcp://localhost:7000
|
||||||
|
java -jar rsc.jar --debug --request --route users.John tcp://localhost:7000
|
||||||
|
java -jar rsc.jar --debug --request --route users.Doe tcp://localhost:7000
|
||||||
|
|
||||||
|
java -jar rsc.jar --debug --request --data "{\"origin\":\"Client\",\"interaction\":\"Request\"}" --route request-response tcp://localhost:7000
|
||||||
|
|
||||||
|
java -jar rsc.jar --setup "{\"data\":\"1234\",\"metadata\":\"client\-id\"}" --route client-id tcp://localhost:7000
|
@ -1,73 +1,12 @@
|
|||||||
package com.example.demo.controller
|
package com.example.demo.controller
|
||||||
|
|
||||||
import com.example.demo.model.User
|
import org.springframework.stereotype.Controller
|
||||||
import com.example.demo.service.UserService
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.toList
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
import org.springframework.web.bind.annotation.PathVariable
|
|
||||||
import org.springframework.web.bind.annotation.RestController
|
|
||||||
import reactor.core.publisher.Flux
|
|
||||||
import reactor.core.publisher.Mono
|
|
||||||
import org.springframework.context.annotation.Lazy
|
|
||||||
import org.springframework.messaging.handler.annotation.DestinationVariable
|
|
||||||
import org.springframework.messaging.handler.annotation.MessageMapping
|
|
||||||
|
|
||||||
|
@Controller
|
||||||
@RestController
|
class HomeController {
|
||||||
@Lazy
|
|
||||||
class HomeController(@Autowired @Lazy private val userService: UserService) {
|
|
||||||
@GetMapping("/")
|
@GetMapping("/")
|
||||||
fun index(): Mono<String> {
|
fun home(): String {
|
||||||
return Mono.just("foo")
|
return "rsocket"
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/data")
|
|
||||||
fun dataHandler(): Mono<MyData> {
|
|
||||||
return Mono.just(MyData(1, "hello2"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/data2")
|
|
||||||
suspend fun dataHandler2(): MyData = withContext(Dispatchers.IO) {
|
|
||||||
delay(10_000)
|
|
||||||
MyData(1, "hello3")
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/users")
|
|
||||||
fun users(): Flux<User> {
|
|
||||||
return userService.users()
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/users2")
|
|
||||||
suspend fun users2(): Flow<User> {
|
|
||||||
return userService.users2()
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/users3")
|
|
||||||
suspend fun users3() = coroutineScope {
|
|
||||||
val fun1 = async {
|
|
||||||
userService.users2().toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
Pair(fun1.await(), 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/messages/{userName}")
|
|
||||||
suspend fun messages(@PathVariable userName: String) = coroutineScope {
|
|
||||||
userService.getUserMessages(userName)
|
|
||||||
}
|
|
||||||
|
|
||||||
@MessageMapping("messages.findAll")
|
|
||||||
suspend fun all() = coroutineScope {
|
|
||||||
userService.getAllMessages()
|
|
||||||
}
|
|
||||||
|
|
||||||
@MessageMapping("users.{name}")
|
|
||||||
suspend fun getUser(@DestinationVariable name: String) = coroutineScope {
|
|
||||||
userService.getUserByName(name)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class MyData(val id: Int, val name2: String)
|
|
@ -0,0 +1,75 @@
|
|||||||
|
package com.example.demo.controller
|
||||||
|
|
||||||
|
import com.example.demo.model.User
|
||||||
|
import com.example.demo.service.UserService
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.toList
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
import reactor.core.publisher.Flux
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
import org.springframework.context.annotation.Lazy
|
||||||
|
import org.springframework.messaging.handler.annotation.DestinationVariable
|
||||||
|
import org.springframework.messaging.handler.annotation.MessageMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@Lazy
|
||||||
|
@RequestMapping("/api")
|
||||||
|
class HomeRestController(@Autowired @Lazy private val userService: UserService) {
|
||||||
|
@GetMapping("/")
|
||||||
|
fun index(): Mono<String> {
|
||||||
|
return Mono.just("foo")
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/data")
|
||||||
|
fun dataHandler(): Mono<MyData> {
|
||||||
|
return Mono.just(MyData(1, "hello2"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/data2")
|
||||||
|
suspend fun dataHandler2(): MyData = withContext(Dispatchers.IO) {
|
||||||
|
delay(10_000)
|
||||||
|
MyData(1, "hello3")
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/users")
|
||||||
|
fun users(): Flux<User> {
|
||||||
|
return userService.users()
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/users2")
|
||||||
|
suspend fun users2(): Flow<User> {
|
||||||
|
return userService.users2()
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/users3")
|
||||||
|
suspend fun users3() = coroutineScope {
|
||||||
|
val fun1 = async {
|
||||||
|
userService.users2().toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
Pair(fun1.await(), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/messages/{userName}")
|
||||||
|
suspend fun messages(@PathVariable userName: String) = coroutineScope {
|
||||||
|
userService.getUserMessages(userName)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MessageMapping("messages.findAll")
|
||||||
|
suspend fun all() = coroutineScope {
|
||||||
|
userService.getAllMessages()
|
||||||
|
}
|
||||||
|
|
||||||
|
@MessageMapping("users.{name}")
|
||||||
|
suspend fun getUser(@DestinationVariable name: String) = coroutineScope {
|
||||||
|
userService.getUserByName(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class MyData(val id: Int, val name2: String)
|
75
src/main/kotlin/com/example/demo/controller/RsocketClient.kt
Normal file
75
src/main/kotlin/com/example/demo/controller/RsocketClient.kt
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package com.example.demo.controller
|
||||||
|
|
||||||
|
import com.example.demo.model.Message
|
||||||
|
import io.vavr.collection.HashMap
|
||||||
|
import io.vavr.collection.Map
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.messaging.handler.annotation.MessageExceptionHandler
|
||||||
|
import org.springframework.messaging.handler.annotation.MessageMapping
|
||||||
|
import org.springframework.messaging.rsocket.RSocketRequester
|
||||||
|
import org.springframework.messaging.rsocket.annotation.ConnectMapping
|
||||||
|
import org.springframework.stereotype.Controller
|
||||||
|
import org.springframework.util.MimeType
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
class RSocketConnectionController {
|
||||||
|
|
||||||
|
private val log = LoggerFactory.getLogger(RSocketConnectionController::class.java)
|
||||||
|
|
||||||
|
private var requesterMap: Map<String, RSocketRequester> = HashMap.empty()
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun getRequesterMap(): Map<String, RSocketRequester> {
|
||||||
|
return requesterMap
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun addRequester(rSocketRequester: RSocketRequester, clientId: String) {
|
||||||
|
log.info("adding requester {}", clientId)
|
||||||
|
requesterMap = requesterMap.put(clientId, rSocketRequester)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun removeRequester(clientId: String) {
|
||||||
|
log.info("removing requester {}", clientId)
|
||||||
|
requesterMap = requesterMap.remove(clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConnectMapping("client-id")
|
||||||
|
fun onConnect(rSocketRequester: RSocketRequester, clientId: String) {
|
||||||
|
val clientIdFixed = clientId.replace("\"", "") //check why the serializer adds " to strings
|
||||||
|
// rSocketRequester.rsocket().dispose() //to reject connection
|
||||||
|
rSocketRequester
|
||||||
|
.rsocket()
|
||||||
|
.onClose()
|
||||||
|
.subscribe(null, null, {
|
||||||
|
log.info("{} just disconnected", clientIdFixed)
|
||||||
|
removeRequester(clientIdFixed)
|
||||||
|
})
|
||||||
|
addRequester(rSocketRequester, clientIdFixed)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MessageMapping("private.news")
|
||||||
|
fun privateNews(message: Message, rSocketRequesterParam: RSocketRequester) {
|
||||||
|
getRequesterMap()
|
||||||
|
.filterKeys { key -> key == message.toUser || key == message.fromUser }
|
||||||
|
.values()
|
||||||
|
.forEach { requester -> sendMessage(requester, message) }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@MessageExceptionHandler
|
||||||
|
suspend fun handleException(ex: IllegalArgumentException): String {
|
||||||
|
delay(10)
|
||||||
|
return "${ex.message} handled"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendMessage(requester: RSocketRequester, message: Message) =
|
||||||
|
requester
|
||||||
|
.route("user.queue.reply")
|
||||||
|
.data(message)
|
||||||
|
.send()
|
||||||
|
.subscribe()
|
||||||
|
|
||||||
|
}
|
3
src/main/kotlin/com/example/demo/model/Message.kt
Normal file
3
src/main/kotlin/com/example/demo/model/Message.kt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
package com.example.demo.model
|
||||||
|
|
||||||
|
data class Message(val fromUser: String, val toUser: String, val message: String)
|
@ -7,4 +7,4 @@ spring.datasource.username=test_user
|
|||||||
spring.datasource.password=password
|
spring.datasource.password=password
|
||||||
|
|
||||||
spring.rsocket.server.port=7000
|
spring.rsocket.server.port=7000
|
||||||
spring.rsocket.server.transport=tcp
|
spring.rsocket.server.transport=websocket
|
15
src/main/resources/templates/rsocket.html
Normal file
15
src/main/resources/templates/rsocket.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns:th="http://www.thymeleaf.org">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Rsocket</title>
|
||||||
|
</head>
|
||||||
|
<script th:src="@{/js/dist/bundle.js}"></script>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
home
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user