├── grails-app ├── assets │ └── javascripts │ │ ├── spring-websocket.js │ │ └── stomp.umd.js └── init │ └── grails │ └── plugin │ └── springwebsocket │ └── Application.groovy ├── gradle.properties ├── settings.gradle ├── src ├── integration-test │ ├── resources │ │ └── application-test.yml │ └── groovy │ │ └── grails │ │ └── plugin │ │ └── springwebsocket │ │ ├── DefaultConfigSpec.groovy │ │ ├── CustomConfigSpringSpec.groovy │ │ └── CustomConfigDslSpec.groovy └── main │ ├── templates │ └── grails │ │ └── plugin │ │ └── springwebsocket │ │ ├── WebSocket.groovy │ │ └── WebSocketConfig.groovy │ ├── resources │ └── META-INF │ │ └── spring │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports │ ├── groovy │ └── grails │ │ └── plugin │ │ └── springwebsocket │ │ ├── GrailsWebSocketClass.java │ │ ├── DefaultGrailsWebSocketClass.java │ │ ├── WebSocketTraitInjector.groovy │ │ ├── WebSocket.groovy │ │ ├── WebSocketAutoConfiguration.groovy │ │ ├── WebSocketArtefact.groovy │ │ ├── WebSocketArtefactHandler.groovy │ │ ├── WebSocketArtefactTypeTransformation.groovy │ │ ├── SpringWebsocketGrailsPlugin.groovy │ │ ├── GrailsSimpAnnotationMethodMessageHandler.groovy │ │ ├── GrailsWebSocketAnnotationMethodMessageHandler.groovy │ │ └── DefaultWebSocketConfig.groovy │ └── scripts │ ├── CreateWebSocketConfig.groovy │ └── CreateWebSocket.groovy ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── gradle-daemon-jvm.properties ├── .gitignore ├── .github └── workflows │ └── ci.yml ├── gradlew.bat ├── gradlew └── README.md /grails-app/assets/javascripts/spring-websocket.js: -------------------------------------------------------------------------------- 1 | //=require stomp.umd 2 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.caching=true 2 | org.gradle.configuration-cache=true 3 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "org.gradle.toolchains.foojay-resolver-convention" version "1.0.0" 3 | } 4 | -------------------------------------------------------------------------------- /src/integration-test/resources/application-test.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | main: 3 | allow-bean-definition-overriding: true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyro23/grails-spring-websocket/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/templates/grails/plugin/springwebsocket/WebSocket.groovy: -------------------------------------------------------------------------------- 1 | package ${model.packageName} 2 | 3 | class ${model.className} { 4 | 5 | 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports: -------------------------------------------------------------------------------- 1 | grails.plugin.springwebsocket.WebSocketAutoConfiguration 2 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/springwebsocket/GrailsWebSocketClass.java: -------------------------------------------------------------------------------- 1 | package grails.plugin.springwebsocket; 2 | 3 | import grails.core.InjectableGrailsClass; 4 | 5 | public interface GrailsWebSocketClass extends InjectableGrailsClass { 6 | } -------------------------------------------------------------------------------- /src/main/scripts/CreateWebSocketConfig.groovy: -------------------------------------------------------------------------------- 1 | def model = model(args[0] ?: "WebSocketConfig") 2 | 3 | render( 4 | template: "grails/plugin/springwebsocket/WebSocketConfig.groovy", 5 | destination: "src/main/groovy/${model.packagePath}/${model.className}.groovy", 6 | model: [model: model] 7 | ) -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.gradle 2 | /.settings 3 | /target 4 | /target-eclipse 5 | /.classpath 6 | /.project 7 | /.link_to_grails_plugins 8 | /plugin.xml 9 | /grails-spring-websocket-* 10 | /web-app/WEB-INF/tld 11 | /bin 12 | /build 13 | /grails-spring-websocket.iws 14 | /grails-spring-websocket.ipr 15 | /.idea 16 | /out 17 | /*.iml 18 | /*.ipr 19 | /*.iws -------------------------------------------------------------------------------- /grails-app/init/grails/plugin/springwebsocket/Application.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugin.springwebsocket 2 | 3 | import grails.boot.GrailsApp 4 | import grails.boot.config.GrailsAutoConfiguration 5 | 6 | class Application extends GrailsAutoConfiguration { 7 | 8 | static void main(String[] args) { 9 | GrailsApp.run this 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /src/main/scripts/CreateWebSocket.groovy: -------------------------------------------------------------------------------- 1 | def model = model(args[0] ? (args[0].endsWith('WebSocket') ? args[0] : "${args[0]}WebSocket") : "WebSocket") 2 | 3 | render( 4 | template: "grails/plugin/springwebsocket/WebSocket.groovy", 5 | destination: "grails-app/websockets/${model.packagePath}/${model.className}.groovy", 6 | model: [model: model] 7 | ) -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/springwebsocket/DefaultGrailsWebSocketClass.java: -------------------------------------------------------------------------------- 1 | package grails.plugin.springwebsocket; 2 | 3 | import org.grails.core.AbstractInjectableGrailsClass; 4 | 5 | public class DefaultGrailsWebSocketClass extends AbstractInjectableGrailsClass { 6 | 7 | public static final String ARTEFACT_TYPE = "WebSocket"; 8 | 9 | public DefaultGrailsWebSocketClass(Class clazz) { 10 | super(clazz, ARTEFACT_TYPE); 11 | } 12 | 13 | } -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/springwebsocket/WebSocketTraitInjector.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugin.springwebsocket 2 | 3 | import grails.compiler.traits.TraitInjector 4 | import groovy.transform.CompileStatic 5 | 6 | @CompileStatic 7 | class WebSocketTraitInjector implements TraitInjector { 8 | 9 | @Override 10 | Class getTrait() { 11 | return WebSocket 12 | } 13 | 14 | @Override 15 | String[] getArtefactTypes() { 16 | return [DefaultGrailsWebSocketClass.ARTEFACT_TYPE] as String[] 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/springwebsocket/WebSocket.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugin.springwebsocket 2 | 3 | import groovy.transform.CompileStatic 4 | import org.springframework.beans.factory.annotation.Autowired 5 | import org.springframework.beans.factory.annotation.Qualifier 6 | import org.springframework.messaging.simp.SimpMessageSendingOperations 7 | 8 | @CompileStatic 9 | trait WebSocket { 10 | 11 | @Autowired 12 | @Delegate 13 | @Qualifier("brokerMessagingTemplate") 14 | SimpMessageSendingOperations brokerMessagingTemplate 15 | 16 | } -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/springwebsocket/WebSocketAutoConfiguration.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugin.springwebsocket 2 | 3 | import groovy.transform.CompileStatic 4 | import org.springframework.boot.autoconfigure.AutoConfiguration 5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean 6 | import org.springframework.context.annotation.Import 7 | 8 | @AutoConfiguration 9 | @CompileStatic 10 | @ConditionalOnMissingBean(name = "webSocketConfig") 11 | @Import(DefaultWebSocketConfig) 12 | class WebSocketAutoConfiguration { 13 | } 14 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/springwebsocket/WebSocketArtefact.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugin.springwebsocket 2 | 3 | import org.codehaus.groovy.transform.GroovyASTTransformationClass 4 | 5 | import java.lang.annotation.ElementType 6 | import java.lang.annotation.Retention 7 | import java.lang.annotation.RetentionPolicy 8 | import java.lang.annotation.Target 9 | 10 | @GroovyASTTransformationClass("grails.plugin.springwebsocket.WebSocketArtefactTypeTransformation") 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @Target([ElementType.TYPE]) 13 | @interface WebSocketArtefact {} -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/springwebsocket/WebSocketArtefactHandler.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugin.springwebsocket 2 | 3 | import grails.core.ArtefactHandlerAdapter 4 | 5 | class WebSocketArtefactHandler extends ArtefactHandlerAdapter { 6 | 7 | static final String PLUGIN_NAME = "springWebsocket" 8 | 9 | WebSocketArtefactHandler() { 10 | super(DefaultGrailsWebSocketClass.ARTEFACT_TYPE, GrailsWebSocketClass.class, DefaultGrailsWebSocketClass.class, DefaultGrailsWebSocketClass.ARTEFACT_TYPE) 11 | } 12 | 13 | @Override 14 | String getPluginName() { 15 | return PLUGIN_NAME 16 | } 17 | } -------------------------------------------------------------------------------- /src/integration-test/groovy/grails/plugin/springwebsocket/DefaultConfigSpec.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugin.springwebsocket 2 | 3 | 4 | import org.springframework.beans.factory.annotation.Autowired 5 | import org.springframework.boot.test.context.SpringBootTest 6 | import org.springframework.context.ApplicationContext 7 | import org.springframework.messaging.simp.SimpMessagingTemplate 8 | import org.springframework.test.context.ActiveProfiles 9 | import spock.lang.Specification 10 | 11 | @ActiveProfiles("test") 12 | @SpringBootTest 13 | class DefaultConfigSpec extends Specification { 14 | 15 | @Autowired 16 | ApplicationContext applicationContext 17 | 18 | void "ctx loads with default websocket (auto-)config"() { 19 | expect: 20 | applicationContext.getBean("webSocketConfig") instanceof DefaultWebSocketConfig 21 | applicationContext.getBean(SimpMessagingTemplate) 22 | } 23 | 24 | } 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/springwebsocket/WebSocketArtefactTypeTransformation.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugin.springwebsocket 2 | 3 | import groovy.transform.CompileStatic 4 | import org.codehaus.groovy.ast.AnnotationNode 5 | import org.codehaus.groovy.ast.ClassNode 6 | import org.codehaus.groovy.control.CompilePhase 7 | import org.codehaus.groovy.control.SourceUnit 8 | import org.codehaus.groovy.transform.GroovyASTTransformation 9 | import org.grails.compiler.injection.ArtefactTypeAstTransformation 10 | 11 | @CompileStatic 12 | @GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) 13 | class WebSocketArtefactTypeTransformation extends ArtefactTypeAstTransformation { 14 | 15 | @Override 16 | protected String resolveArtefactType(SourceUnit sourceUnit, AnnotationNode annotationNode, ClassNode classNode) { 17 | return DefaultGrailsWebSocketClass.ARTEFACT_TYPE 18 | } 19 | 20 | @Override 21 | protected Class getAnnotationTypeClass() { 22 | return WebSocket 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /gradle/gradle-daemon-jvm.properties: -------------------------------------------------------------------------------- 1 | #This file is generated by updateDaemonJvm 2 | toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/368e8384c1697dc3e24a6bdaeb00faac/redirect 3 | toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/b93e7ecbd5436ff09e7f5bf92cc6e098/redirect 4 | toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/368e8384c1697dc3e24a6bdaeb00faac/redirect 5 | toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/b93e7ecbd5436ff09e7f5bf92cc6e098/redirect 6 | toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/fdbe420d142244e86336bc2adfc312a2/redirect 7 | toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/5b027b8bd8440f1f6c20d159efd0965d/redirect 8 | toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/368e8384c1697dc3e24a6bdaeb00faac/redirect 9 | toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/b93e7ecbd5436ff09e7f5bf92cc6e098/redirect 10 | toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/808a5b252b36c97226a4e003e8a6eaae/redirect 11 | toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/16008c489780dfb402c44316e612a16c/redirect 12 | toolchainVersion=17 13 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/springwebsocket/SpringWebsocketGrailsPlugin.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugin.springwebsocket 2 | 3 | import grails.plugins.Plugin 4 | import groovy.util.logging.Slf4j 5 | 6 | @Slf4j 7 | class SpringWebsocketGrailsPlugin extends Plugin { 8 | 9 | def grailsVersion = "6.0.0 > *" 10 | def title = "Spring WebSocket Plugin" 11 | def author = "zyro" 12 | def authorEmail = "" 13 | def description = "Spring WebSocket Plugin" 14 | def documentation = "https://github.com/zyro23/grails-spring-websocket" 15 | def issueManagement = [system: "GitHub", url: "https://github.com/zyro23/grails-spring-websocket/issues"] 16 | def scm = [url: "https://github.com/zyro23/grails-spring-websocket"] 17 | 18 | def watchedResources = "file:./grails-app/websockets/**/*WebSocket.groovy" 19 | def profiles = ["web"] 20 | def loadAfter = ["hibernate3", "hibernate4", "hibernate5", "services"] 21 | 22 | @Override 23 | Closure doWithSpring() { 24 | return { 25 | for (websocket in grailsApplication.getArtefacts(DefaultGrailsWebSocketClass.ARTEFACT_TYPE)) { 26 | log.debug "configuring webSocket ${websocket.propertyName}" 27 | "${websocket.propertyName}"(websocket.clazz) { bean -> 28 | bean.autowire = "byName" 29 | } 30 | } 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/springwebsocket/GrailsSimpAnnotationMethodMessageHandler.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugin.springwebsocket 2 | 3 | import grails.artefact.Controller 4 | import groovy.transform.CompileStatic 5 | import org.springframework.beans.factory.annotation.Autowired 6 | import org.springframework.beans.factory.annotation.Qualifier 7 | import org.springframework.messaging.MessageChannel 8 | import org.springframework.messaging.SubscribableChannel 9 | import org.springframework.messaging.converter.MessageConverter 10 | import org.springframework.messaging.simp.SimpMessageSendingOperations 11 | import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler 12 | 13 | @CompileStatic 14 | class GrailsSimpAnnotationMethodMessageHandler extends SimpAnnotationMethodMessageHandler { 15 | 16 | GrailsSimpAnnotationMethodMessageHandler( 17 | SubscribableChannel clientInboundChannel, 18 | MessageChannel clientOutboundChannel, 19 | SimpMessageSendingOperations brokerTemplate 20 | ) { 21 | super(clientInboundChannel, clientOutboundChannel, brokerTemplate) 22 | } 23 | 24 | @Autowired 25 | @Override 26 | @Qualifier("brokerMessageConverter") 27 | void setMessageConverter(MessageConverter messageConverter) { 28 | super.setMessageConverter(messageConverter) 29 | } 30 | 31 | @Override 32 | protected boolean isHandler(Class beanType) { 33 | return Controller.isAssignableFrom(beanType) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/springwebsocket/GrailsWebSocketAnnotationMethodMessageHandler.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugin.springwebsocket 2 | 3 | import groovy.transform.CompileStatic 4 | import org.springframework.beans.factory.annotation.Autowired 5 | import org.springframework.beans.factory.annotation.Qualifier 6 | import org.springframework.messaging.MessageChannel 7 | import org.springframework.messaging.SubscribableChannel 8 | import org.springframework.messaging.converter.MessageConverter 9 | import org.springframework.messaging.simp.SimpMessageSendingOperations 10 | import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler 11 | 12 | @CompileStatic 13 | class GrailsWebSocketAnnotationMethodMessageHandler extends SimpAnnotationMethodMessageHandler { 14 | 15 | GrailsWebSocketAnnotationMethodMessageHandler( 16 | SubscribableChannel clientInboundChannel, 17 | MessageChannel clientOutboundChannel, 18 | SimpMessageSendingOperations brokerTemplate) { 19 | super(clientInboundChannel, clientOutboundChannel, brokerTemplate) 20 | } 21 | 22 | @Autowired 23 | @Override 24 | @Qualifier("brokerMessageConverter") 25 | void setMessageConverter(MessageConverter messageConverter) { 26 | super.setMessageConverter(messageConverter) 27 | } 28 | 29 | @Override 30 | protected boolean isHandler(Class beanType) { 31 | return WebSocket.isAssignableFrom(beanType) 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/integration-test/groovy/grails/plugin/springwebsocket/CustomConfigSpringSpec.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugin.springwebsocket 2 | 3 | import org.springframework.beans.factory.annotation.Autowired 4 | import org.springframework.boot.test.context.SpringBootTest 5 | import org.springframework.boot.test.context.TestConfiguration 6 | import org.springframework.context.ApplicationContext 7 | import org.springframework.messaging.simp.SimpMessagingTemplate 8 | import org.springframework.test.context.ActiveProfiles 9 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker 10 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry 11 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer 12 | import spock.lang.Specification 13 | 14 | @ActiveProfiles("test") 15 | @SpringBootTest 16 | class CustomConfigSpringSpec extends Specification { 17 | 18 | @EnableWebSocketMessageBroker 19 | @TestConfiguration("webSocketConfig") 20 | static class TestWebSocketConfig implements WebSocketMessageBrokerConfigurer { 21 | @Override 22 | void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) { 23 | stompEndpointRegistry.addEndpoint("/test") 24 | } 25 | } 26 | 27 | @Autowired 28 | ApplicationContext applicationContext 29 | 30 | void "ctx loads with custom websocket (spring-)config"() { 31 | expect: 32 | applicationContext.getBean("webSocketConfig") instanceof TestWebSocketConfig 33 | applicationContext.getBean(SimpMessagingTemplate) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/integration-test/groovy/grails/plugin/springwebsocket/CustomConfigDslSpec.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugin.springwebsocket 2 | 3 | 4 | import grails.spring.BeanBuilder 5 | import org.springframework.beans.factory.annotation.Autowired 6 | import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor 7 | import org.springframework.boot.test.context.SpringBootTest 8 | import org.springframework.boot.test.context.TestConfiguration 9 | import org.springframework.context.ApplicationContext 10 | import org.springframework.context.annotation.Bean 11 | import org.springframework.context.annotation.Configuration 12 | import org.springframework.messaging.simp.SimpMessagingTemplate 13 | import org.springframework.test.context.ActiveProfiles 14 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker 15 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry 16 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer 17 | import spock.lang.Specification 18 | 19 | @ActiveProfiles("test") 20 | @SpringBootTest(properties = "spring.autoconfigure.exclude=grails.plugin.springwebsocket.WebSocketAutoConfiguration") 21 | class CustomConfigDslSpec extends Specification { 22 | 23 | @TestConfiguration 24 | static class TestConfig { 25 | @Bean 26 | static BeanDefinitionRegistryPostProcessor beanDefinitionRegistryPostProcessor() { 27 | return { beanDefinitionRegistry -> 28 | new BeanBuilder().beans { 29 | webSocketConfig(TestWebSocketConfig) 30 | }.registerBeans(beanDefinitionRegistry) 31 | } 32 | } 33 | } 34 | 35 | @Configuration("webSocketConfig") 36 | @EnableWebSocketMessageBroker 37 | private static class TestWebSocketConfig implements WebSocketMessageBrokerConfigurer { 38 | @Override 39 | void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) { 40 | stompEndpointRegistry.addEndpoint("/test") 41 | } 42 | } 43 | 44 | @Autowired 45 | ApplicationContext applicationContext 46 | 47 | void "ctx loads with custom websocket (dsl-)config"() { 48 | expect: 49 | applicationContext.getBean("webSocketConfig") instanceof TestWebSocketConfig 50 | applicationContext.getBean(SimpMessagingTemplate) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | test: 4 | runs-on: ubuntu-24.04 5 | steps: 6 | - uses: actions/checkout@v4 7 | - uses: actions/setup-java@v4 8 | with: 9 | distribution: liberica 10 | java-version: "17" 11 | - uses: gradle/actions/setup-gradle@v4 12 | with: 13 | cache-read-only: false 14 | - run: ./gradlew test integrationTest 15 | - uses: actions/upload-artifact@v4 16 | with: 17 | path: build/reports 18 | release-snapshot: 19 | needs: 20 | - test 21 | if: github.ref_type == 'branch' 22 | environment: release 23 | runs-on: ubuntu-24.04 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: actions/setup-java@v4 27 | with: 28 | distribution: liberica 29 | java-version: "17" 30 | - uses: gradle/actions/setup-gradle@v4 31 | with: 32 | cache-read-only: false 33 | - run: echo "${{ secrets.SIGNING_SECRING_FILE }}" | base64 -d > ${{ github.workspace }}/secring.gpg 34 | - run: > 35 | ./gradlew publishMavenPublicationToMavenCentralSnapshotsRepository 36 | -Psigning.keyId=${{ secrets.SIGNING_KEY_ID }} 37 | -Psigning.password=${{ secrets.SIGNING_PASSWORD }} 38 | -Psigning.secretKeyRingFile=${{ github.workspace }}/secring.gpg 39 | -PmavenCentralSnapshotsUsername=${{ secrets.MAVEN_CENTRAL_USERNAME }} 40 | -PmavenCentralSnapshotsPassword=${{ secrets.MAVEN_CENTRAL_PASSWORD }} 41 | release: 42 | needs: 43 | - test 44 | if: github.ref_type == 'tag' 45 | environment: release 46 | runs-on: ubuntu-24.04 47 | steps: 48 | - uses: actions/checkout@v4 49 | - uses: actions/setup-java@v4 50 | with: 51 | distribution: liberica 52 | java-version: "17" 53 | - uses: gradle/actions/setup-gradle@v4 54 | with: 55 | cache-read-only: false 56 | - run: echo "${{ secrets.SIGNING_SECRING_FILE }}" | base64 -d > ${{ github.workspace }}/secring.gpg 57 | - run: > 58 | ./gradlew publishMavenPublicationToMavenCentralStagingRepository 59 | -Psigning.keyId=${{ secrets.SIGNING_KEY_ID }} 60 | -Psigning.password=${{ secrets.SIGNING_PASSWORD }} 61 | -Psigning.secretKeyRingFile=${{ github.workspace }}/secring.gpg 62 | -PmavenCentralStagingUsername=${{ secrets.MAVEN_CENTRAL_USERNAME }} 63 | -PmavenCentralStagingPassword=${{ secrets.MAVEN_CENTRAL_PASSWORD }} 64 | 65 | -------------------------------------------------------------------------------- /src/main/groovy/grails/plugin/springwebsocket/DefaultWebSocketConfig.groovy: -------------------------------------------------------------------------------- 1 | package grails.plugin.springwebsocket 2 | 3 | import groovy.transform.CompileStatic 4 | import org.springframework.beans.factory.annotation.Qualifier 5 | import org.springframework.context.annotation.Bean 6 | import org.springframework.context.annotation.Configuration 7 | import org.springframework.messaging.MessageChannel 8 | import org.springframework.messaging.SubscribableChannel 9 | import org.springframework.messaging.simp.SimpMessageSendingOperations 10 | import org.springframework.messaging.simp.config.MessageBrokerRegistry 11 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker 12 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry 13 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer 14 | 15 | @CompileStatic 16 | @Configuration("webSocketConfig") 17 | @EnableWebSocketMessageBroker 18 | class DefaultWebSocketConfig implements WebSocketMessageBrokerConfigurer { 19 | 20 | @Override 21 | void configureMessageBroker(MessageBrokerRegistry messageBrokerRegistry) { 22 | messageBrokerRegistry.enableSimpleBroker("/queue", "/topic") 23 | messageBrokerRegistry.setApplicationDestinationPrefixes("/app") 24 | } 25 | 26 | @Override 27 | void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) { 28 | stompEndpointRegistry.addEndpoint("/stomp") 29 | } 30 | 31 | @Bean 32 | GrailsSimpAnnotationMethodMessageHandler grailsSimpAnnotationMethodMessageHandler( 33 | @Qualifier("clientInboundChannel") SubscribableChannel clientInboundChannel, 34 | @Qualifier("clientOutboundChannel") MessageChannel clientOutboundChannel, 35 | @Qualifier("brokerMessagingTemplate") SimpMessageSendingOperations brokerMessagingTemplate) { 36 | GrailsSimpAnnotationMethodMessageHandler handler = new GrailsSimpAnnotationMethodMessageHandler(clientInboundChannel, clientOutboundChannel, brokerMessagingTemplate) 37 | handler.destinationPrefixes = ["/app"] 38 | return handler 39 | } 40 | 41 | @Bean 42 | GrailsWebSocketAnnotationMethodMessageHandler grailsWebSocketAnnotationMethodMessageHandler( 43 | @Qualifier("clientInboundChannel") SubscribableChannel clientInboundChannel, 44 | @Qualifier("clientOutboundChannel") MessageChannel clientOutboundChannel, 45 | @Qualifier("brokerMessagingTemplate") SimpMessageSendingOperations brokerMessagingTemplate) { 46 | GrailsWebSocketAnnotationMethodMessageHandler handler = new GrailsWebSocketAnnotationMethodMessageHandler(clientInboundChannel, clientOutboundChannel, brokerMessagingTemplate) 47 | handler.destinationPrefixes = ["/app"] 48 | return handler 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/templates/grails/plugin/springwebsocket/WebSocketConfig.groovy: -------------------------------------------------------------------------------- 1 | package ${model.packageName} 2 | 3 | import grails.plugin.springwebsocket.GrailsSimpAnnotationMethodMessageHandler 4 | import grails.plugin.springwebsocket.GrailsWebSocketAnnotationMethodMessageHandler 5 | import groovy.transform.CompileStatic 6 | import org.springframework.beans.factory.annotation.Qualifier 7 | import org.springframework.context.annotation.Bean 8 | import org.springframework.context.annotation.Configuration 9 | import org.springframework.messaging.MessageChannel 10 | import org.springframework.messaging.SubscribableChannel 11 | import org.springframework.messaging.simp.SimpMessageSendingOperations 12 | import org.springframework.messaging.simp.config.MessageBrokerRegistry 13 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker 14 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry 15 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer 16 | 17 | @CompileStatic 18 | @Configuration("webSocketConfig") 19 | @EnableWebSocketMessageBroker 20 | class ${model.className} implements WebSocketMessageBrokerConfigurer { 21 | 22 | @Override 23 | void configureMessageBroker(MessageBrokerRegistry messageBrokerRegistry) { 24 | messageBrokerRegistry.enableSimpleBroker("/queue", "/topic") 25 | messageBrokerRegistry.setApplicationDestinationPrefixes("/app") 26 | } 27 | 28 | @Override 29 | void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) { 30 | stompEndpointRegistry.addEndpoint("/stomp") 31 | } 32 | 33 | @Bean 34 | GrailsSimpAnnotationMethodMessageHandler grailsSimpAnnotationMethodMessageHandler( 35 | @Qualifier("clientInboundChannel") SubscribableChannel clientInboundChannel, 36 | @Qualifier("clientOutboundChannel") MessageChannel clientOutboundChannel, 37 | @Qualifier("brokerMessagingTemplate") SimpMessageSendingOperations brokerMessagingTemplate) { 38 | GrailsSimpAnnotationMethodMessageHandler handler = new GrailsSimpAnnotationMethodMessageHandler(clientInboundChannel, clientOutboundChannel, brokerMessagingTemplate) 39 | handler.destinationPrefixes = ["/app"] 40 | return handler 41 | } 42 | 43 | @Bean 44 | GrailsWebSocketAnnotationMethodMessageHandler grailsWebSocketAnnotationMethodMessageHandler( 45 | @Qualifier("clientInboundChannel") SubscribableChannel clientInboundChannel, 46 | @Qualifier("clientOutboundChannel") MessageChannel clientOutboundChannel, 47 | @Qualifier("brokerMessagingTemplate") SimpMessageSendingOperations brokerMessagingTemplate) { 48 | GrailsWebSocketAnnotationMethodMessageHandler handler = new GrailsWebSocketAnnotationMethodMessageHandler(clientInboundChannel, clientOutboundChannel, brokerMessagingTemplate) 49 | handler.destinationPrefixes = ["/app"] 50 | return handler 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | 118 | 119 | # Determine the Java command to use to start the JVM. 120 | if [ -n "$JAVA_HOME" ] ; then 121 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 122 | # IBM's JDK on AIX uses strange locations for the executables 123 | JAVACMD=$JAVA_HOME/jre/sh/java 124 | else 125 | JAVACMD=$JAVA_HOME/bin/java 126 | fi 127 | if [ ! -x "$JAVACMD" ] ; then 128 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 129 | 130 | Please set the JAVA_HOME variable in your environment to match the 131 | location of your Java installation." 132 | fi 133 | else 134 | JAVACMD=java 135 | if ! command -v java >/dev/null 2>&1 136 | then 137 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 138 | 139 | Please set the JAVA_HOME variable in your environment to match the 140 | location of your Java installation." 141 | fi 142 | fi 143 | 144 | # Increase the maximum file descriptors if we can. 145 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 146 | case $MAX_FD in #( 147 | max*) 148 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 149 | # shellcheck disable=SC2039,SC3045 150 | MAX_FD=$( ulimit -H -n ) || 151 | warn "Could not query maximum file descriptor limit" 152 | esac 153 | case $MAX_FD in #( 154 | '' | soft) :;; #( 155 | *) 156 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 157 | # shellcheck disable=SC2039,SC3045 158 | ulimit -n "$MAX_FD" || 159 | warn "Could not set maximum file descriptor limit to $MAX_FD" 160 | esac 161 | fi 162 | 163 | # Collect all arguments for the java command, stacking in reverse order: 164 | # * args from the command line 165 | # * the main class name 166 | # * -classpath 167 | # * -D...appname settings 168 | # * --module-path (only if needed) 169 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 170 | 171 | # For Cygwin or MSYS, switch paths to Windows format before running java 172 | if "$cygwin" || "$msys" ; then 173 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 214 | "$@" 215 | 216 | # Stop when "xargs" is not available. 217 | if ! command -v xargs >/dev/null 2>&1 218 | then 219 | die "xargs is not available" 220 | fi 221 | 222 | # Use "xargs" to parse quoted args. 223 | # 224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 225 | # 226 | # In Bash we could simply go: 227 | # 228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 229 | # set -- "${ARGS[@]}" "$@" 230 | # 231 | # but POSIX shell has neither arrays nor command substitution, so instead we 232 | # post-process each arg (as a line of input to sed) to backslash-escape any 233 | # character that might be a shell metacharacter, then use eval to reverse 234 | # that process (while maintaining the separation between arguments), and wrap 235 | # the whole thing up as a single "set" statement. 236 | # 237 | # This will of course break if any of these variables contains a newline or 238 | # an unmatched quote. 239 | # 240 | 241 | eval "set -- $( 242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 243 | xargs -n1 | 244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 245 | tr '\n' ' ' 246 | )" '"$@"' 247 | 248 | exec "$JAVACMD" "$@" 249 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Websocket Grails Plugin 2 | 3 | This plugin aims at making the websocket support introduced in Spring 4.0 available to Grails applications. 4 | 5 | You can also use the corresponding Spring docs/apis/samples as a reference. 6 | 7 | That is mentioned multiple times in this readme because there is everything explained in fine detail. 8 | 9 | Version compatibility: 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
Plugin versionGrails version
io.github.zyro23:grails-spring-websocket:2.7.x7.0.0+
io.github.zyro23:grails-spring-websocket:2.6.x6.0.0+
org.grails.plugins:grails-spring-websocket:2.5.x4.0.0+
org.grails.plugins:grails-spring-websocket:2.4.x3.2.7+
32 | 33 | ## Installation 34 | 35 | To install the plugin into a Grails application add the following line to your `build.gradle` dependencies section: 36 | 37 | implementation "io.github.zyro23:grails-spring-websocket:2.7.0" 38 | 39 | Plugin releases are published to maven central. 40 | 41 | ### Snapshots 42 | 43 | To install a `-SNAPSHOT` version, add the snapshot repository: 44 | 45 | repositories { 46 | maven { 47 | url = "https://central.sonatype.com/repository/maven-snapshots" 48 | } 49 | } 50 | 51 | And add the following line to your `build.gradle` dependencies section: 52 | 53 | implementation "io.github.zyro23:grails-spring-websocket:2.8.0-SNAPSHOT" 54 | 55 | Plugin snapshots are published to the maven central snapshot repository which has an automatic cleanup policy (90 days). 56 | 57 | ## Usage 58 | 59 | The plugin makes the Spring websocket/messaging web-mvc annotations usable in Grails, too. 60 | 61 | Those annotations can be used in: 62 | * Regular Grails controllers 63 | * `WebSocket` Grails artefacts (`./grailsw create-web-socket my.package.name.MyWebSocket`) 64 | * Spring `@Controller` beans 65 | 66 | I think basic usage is explained best by example code. 67 | 68 | But: the code below is just some very minimal it-works proof. 69 | 70 | Check the Spring docs/apis/samples for more advanced use-cases, e.g. security and authentication. 71 | 72 | ### Controller (annotated handler method) 73 | 74 | */grails-app/controllers/example/ExampleController.groovy*: 75 | 76 | ```groovy 77 | package example 78 | 79 | import org.springframework.messaging.handler.annotation.MessageMapping 80 | import org.springframework.messaging.handler.annotation.SendTo 81 | 82 | class ExampleController { 83 | 84 | def index() {} 85 | 86 | @MessageMapping("/hello") 87 | @SendTo("/topic/hello") 88 | protected String hello(String world) { 89 | return "hello, ${world}!" 90 | } 91 | 92 | } 93 | ``` 94 | 95 | Unless you want your handler method to be exposed as a Grails controller action, you should define the annotated method as protected or add an additional annotation `@grails.web.controllers.ControllerMethod`. 96 | 97 | Alternatively, `WebSocket` Grails artefacts and/or Spring `@Controller` beans can be used as well, for example: 98 | 99 | */grails-app/websockets/example/ExampleWebSocket.groovy*: 100 | 101 | ```groovy 102 | package example 103 | 104 | import org.springframework.messaging.handler.annotation.MessageMapping 105 | import org.springframework.messaging.handler.annotation.SendTo 106 | 107 | class ExampleWebSocket { 108 | 109 | @MessageMapping("/hello") 110 | @SendTo("/topic/hello") 111 | String hello(String world) { 112 | return "hello, ${world}!" 113 | } 114 | 115 | } 116 | ``` 117 | 118 | ### Client-side (stomp.js) 119 | 120 | */grails-app/views/example/index.gsp*: 121 | 122 | ```gsp 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 147 | 148 | 149 | 150 |
151 | 152 | 153 | ``` 154 | 155 | This would be the index view of the controller above. The js connects to the message broker and subscribes to /topic/hello. 156 | 157 | For this example, I added a button allowing to trigger a send/receive roundtrip. 158 | 159 | While this example shows jquery used with the asset-pipeline plugin, the use of jquery is **not required**. 160 | 161 | ### Service (brokerMessagingTemplate bean) 162 | 163 | To send messages directly, the `brokerMessagingTemplate` bean (of type `SimpMessageSendingOperations`) can be used. 164 | 165 | The plugin provides a `WebSocket` trait that autowires the `brokerMessagingTemplate` and delegates to it. 166 | 167 | That `WebSocket` trait is automatically implemented by `WebSocket` artefacts, but you can implement it from other beans as well, e.g. from a service. 168 | 169 | */grails-app/services/example/ExampleService.groovy*: 170 | 171 | ```groovy 172 | package example 173 | 174 | import grails.plugin.springwebsocket.WebSocket 175 | 176 | class ExampleService implements WebSocket { 177 | 178 | void hello() { 179 | convertAndSend("/topic/hello", "hello from service!") 180 | } 181 | 182 | } 183 | ``` 184 | 185 | Or, if you prefer, you can also inject and use the `brokerMessagingTemplate` bean directly. 186 | 187 | */grails-app/services/example/ExampleService.groovy*: 188 | 189 | ```groovy 190 | package example 191 | 192 | import org.springframework.messaging.simp.SimpMessageSendingOperations 193 | 194 | class ExampleService { 195 | 196 | SimpMessageSendingOperations brokerMessagingTemplate 197 | 198 | void hello() { 199 | brokerMessagingTemplate.convertAndSend("/topic/hello", "hello from service!") 200 | } 201 | 202 | } 203 | ``` 204 | 205 | ## Configuration 206 | 207 | Configuration relies on Spring java config, especially `@EnableWebSocketMessageBroker`. 208 | 209 | ### Default Configuration 210 | 211 | By default, a configuration bean named `webSocketConfig` of type `grails.plugin.springwebsocket.DefaultWebSocketConfig` is registered: 212 | 213 | * An in-memory `Map`-based message broker implementation is used 214 | * The prefixes for broker destinations ("outgoing messages") are: `/queue` or `/topic` 215 | * The prefix for application destinations ("incoming messages") is: `/app` 216 | * The stomp-endpoint URI is: `/stomp` 217 | * A `GrailsSimpAnnotationMethodMessageHandler` bean is defined to allow Grails controller methods to act as message handlers 218 | * A `GrailsWebSocketAnnotationMethodMessageHandler` bean is defined to allow Grails webSocket methods to act as message handlers 219 | 220 | If the default values are fine for your application, you are good to go. No further configuration required then. 221 | 222 | ### Custom Configuration 223 | 224 | The default configuration can be customized/overridden by providing a `@Configuration` bean named `webSocketConfig`. 225 | 226 | As a starting point, you can take a look at `DefaultWebSocketConfig` or you can create a config class/bean resembling the default config with: 227 | 228 | ./grailsw create-web-socket-config my.package.name.MyClassName 229 | 230 | That class will be placed under `src/main/groovy` and needs to be registered as a Spring configuration bean named `webSocketConfig`. 231 | 232 | That can be accomplished in different ways, depending on your project and preferences, e.g.: 233 | 234 | * By making sure the class is in a package covered by `@ComponentScan` 235 | * Or, by adding `@Import(MyClassName)` to your `Application` class 236 | 237 | As an alternative, you can disable the `WebSocketAutoConfiguration` explicitly and use a custom config with a different name or register it via the grails spring bean dsl: 238 | 239 | * Add `@EnableAutoConfiguration(exclude = WebSocketAutoConfiguration)` to your `Application` class 240 | * Or, configure `spring.autoconfigure.exclude=grails.plugin.springwebsocket.WebSocketAutoConfiguration` 241 | 242 | */grails-app/conf/spring/resources.groovy*: 243 | 244 | ```groovy 245 | beans = { 246 | webSocketConfig(my.package.name.MyClassName) 247 | } 248 | ``` 249 | 250 | Check the Spring docs/apis/samples for the available configuration options. 251 | 252 | ### Full-Featured Broker 253 | 254 | To use a full-featured (e.g. RabbitMQ, ActiveMQ, etc.) instead of the default simple broker, please refer to the Spring docs regarding configuration. 255 | Additionally, add a dependency for TCP connection management. 256 | 257 | implementation platform("io.projectreactor:reactor-bom:2024.0.8") 258 | implementation "io.projectreactor.netty:reactor-netty" 259 | 260 | It is a good idea to align the BOM version with the one your current spring-boot BOM is using. 261 | 262 | ## User Destinations 263 | 264 | To send messages to specific users, you can (among other ways) annotate message handler methods with `@SendToUser` and/or use the `SimpMessagingTemplate.convertAndSendToUser(...)` methods. 265 | 266 | */grails-app/controllers/example/ExampleController.groovy*: 267 | 268 | ```groovy 269 | class ExampleController { 270 | 271 | @MessageMapping("/hello") 272 | @SendToUser("/queue/hello") 273 | protected String hello(String world) { 274 | return "hello from controller, ${world}!" 275 | } 276 | 277 | } 278 | ``` 279 | 280 | To receive messages for the above `/queue/hello` user destination, the js client would have to subscribe to `/user/queue/hello`. 281 | 282 | If a user is not logged in, `@SendToUser` will still work and only the user who sent the ingoing message will receive the outgoing one returned by the method. 283 | 284 | */grails-app/services/example/ExampleService.groovy*: 285 | 286 | ```groovy 287 | class ExampleService implements WebSocket { 288 | 289 | void hello() { 290 | convertAndSendToUser("myTargetUsername", "/queue/hello", "hello, target user!") 291 | } 292 | 293 | } 294 | ``` 295 | 296 | Again, to receive messages for the above `/queue/hello` user destination, the js client would have to subscribe to `/user/queue/hello`. 297 | 298 | ## Security 299 | 300 | To secure websocket messaging, we can leverage the first-class websocket security support of Spring Security 4.0+. 301 | 302 | Check the Spring Security docs and the Spring Guides to get a jump-start into the topic. 303 | 304 | There is a variety of options how to build your solution, including: 305 | * Securing message handler methods in a declarative fashion using annotations (e.g. `@PreAuthorize`) 306 | * Securing message handler methods by using an `@AuthenticationPrincipal`-annotated argument. 307 | * Filtering messages and subscriptions (e.g. with an `SecurityWebSocketMessageBrokerConfigurer`) 308 | 309 | I will only show a short example of securing message handler methods with security annotations and filtering inbound messages. I hope you do not mind the lack of import statements in the following code snippets ;) 310 | 311 | A working Spring Security setup is required. For the sake of brevity, here a super-minimalistic Spring Security dummy configuration: 312 | 313 | */build.gradle*: 314 | 315 | ```groovy 316 | dependencies { 317 | implementation "org.springframework.security:spring-security-config" 318 | implementation "org.springframework.security:spring-security-messaging" 319 | implementation "org.springframework.security:spring-security-web" 320 | } 321 | ``` 322 | 323 | */src/main/groovy/example/WebSecurityConfig.groovy*: 324 | 325 | ```groovy 326 | @Configuration 327 | @EnableGlobalMethodSecurity(prePostEnabled = true) 328 | @EnableWebSecurity 329 | class WebSecurityConfig extends WebSecurityConfigurerAdapter { 330 | 331 | @Override 332 | protected void configure(HttpSecurity http) throws Exception { 333 | http.httpBasic() 334 | http.authorizeRequests().anyRequest().authenticated() 335 | } 336 | 337 | @Autowired 338 | void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { 339 | auth.inMemoryAuthentication() 340 | .withUser("user").password("password").roles("USER") 341 | } 342 | 343 | } 344 | ``` 345 | 346 | Spring security will by default enable CSRF protection for websocket messages. 347 | 348 | To include the required token in the stomp headers, your js code could look like this: 349 | 350 | */grails-app/views/example/index.gsp*: 351 | 352 | ```javascript 353 | $(function() { 354 | var url = "ws${createLink(uri: '/stomp', absolute: true).replaceFirst('(?i)http', '')}" 355 | var csrfHeaderName = "${request._csrf.headerName}"; 356 | var csrfToken = "${request._csrf.token}"; 357 | var client = new StompJs.Client({ 358 | brokerURL: url, 359 | connectHeaders: { 360 | [csrfHeaderName]: csrfToken 361 | }, 362 | onConnect: () => { 363 | // subscriptions etc. [...] 364 | }, 365 | }); 366 | client.activate(); 367 | }); 368 | ``` 369 | 370 | There are still embedded GSP GString expressions present, which means that snippet will only work in a GSP as-is. If you plan on extracting the js properly into an own js file (or similar), you will have to pass those values along. 371 | 372 | ### Securing Message Handler Methods 373 | 374 | Securing message handler methods can be achieved with annotations in a declarative fashion. 375 | 376 | The following example shows a Grails controller with a secured message handler method and an message exception handler method. 377 | 378 | */grails-app/controllers/example/ExampleController.groovy*: 379 | 380 | ```groovy 381 | class ExampleController { 382 | 383 | @ControllerMethod 384 | @MessageMapping("/hello") 385 | @PreAuthorize("hasRole('USER')") 386 | @SendTo("/topic/hello") 387 | String hello(String world) { 388 | return "hello from secured controller, ${world}!" 389 | } 390 | 391 | @ControllerMethod 392 | @MessageExceptionHandler 393 | @SendToUser(value = "/queue/errors", broadcast = false) 394 | String handleException(Exception e) { 395 | return "caught ${e.message}" 396 | } 397 | 398 | } 399 | ``` 400 | 401 | Besides the security handling itself, this snippet shows one important catch: if you want to secure Grails controller actions with `@PreAuthorize`, the secured method has to be public. However, as we still do not want the method to be exposed as a controller action but only as message handler, in this case the use of `@ControllerMethod` is required. 402 | 403 | If you use Grails `WebSocket` artefacts or Spring `@Controller` beans as message handlers, you do obviously not require those additional `@ControllerMethod` annotations. 404 | 405 | ### Filtering messages 406 | 407 | The following example shows how you can filter inbound messages by type and/or by destination pattern. 408 | 409 | */src/main/groovy/example/WebSecurityConfig.groovy*: 410 | 411 | ```groovy 412 | @Configuration 413 | class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer { 414 | 415 | @Override 416 | void configureInbound(MessageSecurityMetadataSourceRegistry messages) { 417 | messages 418 | .nullDestMatcher().authenticated() 419 | .simpSubscribeDestMatchers("/user/queue/errors").permitAll() 420 | .simpDestMatchers("/app/**").hasRole("USER") 421 | .simpSubscribeDestMatchers("/user/**", "/topic/**").hasRole("USER") 422 | .simpTypeMatchers(SimpMessageType.MESSAGE, SimpMessageType.SUBSCRIBE).denyAll() 423 | .anyMessage().denyAll() 424 | } 425 | 426 | } 427 | ``` 428 | 429 | ## Event Handling 430 | 431 | Starting with Grails 3, grails-plugin-events is a core plugin allowing to use the Reactor framework for event handling. 432 | 433 | While there is no special event integration regarding websocket messaging (because it is not really necessary anymore), a service that handles application events can look like the follwing snippet. I am _not_ talking about Spring `ApplicationEvent`s here, but Reactor `Event`s. 434 | 435 | */grails-app/services/example/ExampleService.groovy*: 436 | 437 | ```groovy 438 | @Consumer 439 | class ExampleService implements WebSocket { 440 | 441 | @Selector("myEvent") 442 | void hello(Event event) { 443 | convertAndSend("/topic/myEventTopic", "myEvent: ${event.data}") 444 | } 445 | 446 | } 447 | ``` 448 | 449 | Events can be fired/sent from all application artefacts/beans that implement the trait `Events`. Grails service beans do so by convention. Those beans also allow dynamic registration of event listeners. E.g.: 450 | 451 | */grails-app/services/example/ExampleService.groovy*: 452 | 453 | ```groovy 454 | class ExampleService { 455 | 456 | void fireMyEvent() { 457 | notify("myEvent", "hello from myEvent!") 458 | } 459 | 460 | } 461 | ``` 462 | 463 | */grails-app/init/BootStrap.groovy*: 464 | 465 | ```groovy 466 | class BootStrap implements Events, WebSocket { 467 | 468 | def init = { 469 | on("myEvent") { Event event -> 470 | convertAndSend("/topic/myEventTopic", "myEvent: ${event.data}") 471 | } 472 | } 473 | 474 | } 475 | ``` 476 | 477 | For further information check the Grails async docs. 478 | 479 | ## Misc 480 | 481 | ### Startup performance 482 | 483 | Scanning Grails controllers for message handler methods can impact application startup time if you have many controllers. 484 | 485 | One way around this is to put your message handler methods into Grails `WebSocket` artefacts instead of Grails controllers and then use a custom websocket config class without the `GrailsSimpAnnotationMethodMessageHandler`. 486 | -------------------------------------------------------------------------------- /grails-app/assets/javascripts/stomp.umd.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 3 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 4 | (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.StompJs = {})); 5 | })(this, (function (exports) { 'use strict'; 6 | 7 | /** 8 | * @internal 9 | */ 10 | function augmentWebsocket(webSocket, debug) { 11 | webSocket.terminate = function () { 12 | const noOp = () => { }; 13 | // set all callbacks to no op 14 | this.onerror = noOp; 15 | this.onmessage = noOp; 16 | this.onopen = noOp; 17 | const ts = new Date(); 18 | const id = Math.random().toString().substring(2, 8); // A simulated id 19 | const origOnClose = this.onclose; 20 | // Track delay in actual closure of the socket 21 | this.onclose = closeEvent => { 22 | const delay = new Date().getTime() - ts.getTime(); 23 | debug(`Discarded socket (#${id}) closed after ${delay}ms, with code/reason: ${closeEvent.code}/${closeEvent.reason}`); 24 | }; 25 | this.close(); 26 | origOnClose?.call(webSocket, { 27 | code: 4001, 28 | reason: `Quick discarding socket (#${id}) without waiting for the shutdown sequence.`, 29 | wasClean: false, 30 | }); 31 | }; 32 | } 33 | 34 | /** 35 | * Some byte values, used as per STOMP specifications. 36 | * 37 | * Part of `@stomp/stompjs`. 38 | * 39 | * @internal 40 | */ 41 | const BYTE = { 42 | // LINEFEED byte (octet 10) 43 | LF: '\x0A', 44 | // NULL byte (octet 0) 45 | NULL: '\x00', 46 | }; 47 | 48 | /** 49 | * Frame class represents a STOMP frame. 50 | * 51 | * @internal 52 | */ 53 | class FrameImpl { 54 | /** 55 | * body of the frame 56 | */ 57 | get body() { 58 | if (!this._body && this.isBinaryBody) { 59 | this._body = new TextDecoder().decode(this._binaryBody); 60 | } 61 | return this._body || ''; 62 | } 63 | /** 64 | * body as Uint8Array 65 | */ 66 | get binaryBody() { 67 | if (!this._binaryBody && !this.isBinaryBody) { 68 | this._binaryBody = new TextEncoder().encode(this._body); 69 | } 70 | // At this stage it will definitely have a valid value 71 | return this._binaryBody; 72 | } 73 | /** 74 | * Frame constructor. `command`, `headers` and `body` are available as properties. 75 | * 76 | * @internal 77 | */ 78 | constructor(params) { 79 | const { command, headers, body, binaryBody, escapeHeaderValues, skipContentLengthHeader, } = params; 80 | this.command = command; 81 | this.headers = Object.assign({}, headers || {}); 82 | if (binaryBody) { 83 | this._binaryBody = binaryBody; 84 | this.isBinaryBody = true; 85 | } 86 | else { 87 | this._body = body || ''; 88 | this.isBinaryBody = false; 89 | } 90 | this.escapeHeaderValues = escapeHeaderValues || false; 91 | this.skipContentLengthHeader = skipContentLengthHeader || false; 92 | } 93 | /** 94 | * deserialize a STOMP Frame from raw data. 95 | * 96 | * @internal 97 | */ 98 | static fromRawFrame(rawFrame, escapeHeaderValues) { 99 | const headers = {}; 100 | const trim = (str) => str.replace(/^\s+|\s+$/g, ''); 101 | // In case of repeated headers, as per standards, first value need to be used 102 | for (const header of rawFrame.headers.reverse()) { 103 | header.indexOf(':'); 104 | const key = trim(header[0]); 105 | let value = trim(header[1]); 106 | if (escapeHeaderValues && 107 | rawFrame.command !== 'CONNECT' && 108 | rawFrame.command !== 'CONNECTED') { 109 | value = FrameImpl.hdrValueUnEscape(value); 110 | } 111 | headers[key] = value; 112 | } 113 | return new FrameImpl({ 114 | command: rawFrame.command, 115 | headers, 116 | binaryBody: rawFrame.binaryBody, 117 | escapeHeaderValues, 118 | }); 119 | } 120 | /** 121 | * @internal 122 | */ 123 | toString() { 124 | return this.serializeCmdAndHeaders(); 125 | } 126 | /** 127 | * serialize this Frame in a format suitable to be passed to WebSocket. 128 | * If the body is string the output will be string. 129 | * If the body is binary (i.e. of type Unit8Array) it will be serialized to ArrayBuffer. 130 | * 131 | * @internal 132 | */ 133 | serialize() { 134 | const cmdAndHeaders = this.serializeCmdAndHeaders(); 135 | if (this.isBinaryBody) { 136 | return FrameImpl.toUnit8Array(cmdAndHeaders, this._binaryBody).buffer; 137 | } 138 | else { 139 | return cmdAndHeaders + this._body + BYTE.NULL; 140 | } 141 | } 142 | serializeCmdAndHeaders() { 143 | const lines = [this.command]; 144 | if (this.skipContentLengthHeader) { 145 | delete this.headers['content-length']; 146 | } 147 | for (const name of Object.keys(this.headers || {})) { 148 | const value = this.headers[name]; 149 | if (this.escapeHeaderValues && 150 | this.command !== 'CONNECT' && 151 | this.command !== 'CONNECTED') { 152 | lines.push(`${name}:${FrameImpl.hdrValueEscape(`${value}`)}`); 153 | } 154 | else { 155 | lines.push(`${name}:${value}`); 156 | } 157 | } 158 | if (this.isBinaryBody || 159 | (!this.isBodyEmpty() && !this.skipContentLengthHeader)) { 160 | lines.push(`content-length:${this.bodyLength()}`); 161 | } 162 | return lines.join(BYTE.LF) + BYTE.LF + BYTE.LF; 163 | } 164 | isBodyEmpty() { 165 | return this.bodyLength() === 0; 166 | } 167 | bodyLength() { 168 | const binaryBody = this.binaryBody; 169 | return binaryBody ? binaryBody.length : 0; 170 | } 171 | /** 172 | * Compute the size of a UTF-8 string by counting its number of bytes 173 | * (and not the number of characters composing the string) 174 | */ 175 | static sizeOfUTF8(s) { 176 | return s ? new TextEncoder().encode(s).length : 0; 177 | } 178 | static toUnit8Array(cmdAndHeaders, binaryBody) { 179 | const uint8CmdAndHeaders = new TextEncoder().encode(cmdAndHeaders); 180 | const nullTerminator = new Uint8Array([0]); 181 | const uint8Frame = new Uint8Array(uint8CmdAndHeaders.length + binaryBody.length + nullTerminator.length); 182 | uint8Frame.set(uint8CmdAndHeaders); 183 | uint8Frame.set(binaryBody, uint8CmdAndHeaders.length); 184 | uint8Frame.set(nullTerminator, uint8CmdAndHeaders.length + binaryBody.length); 185 | return uint8Frame; 186 | } 187 | /** 188 | * Serialize a STOMP frame as per STOMP standards, suitable to be sent to the STOMP broker. 189 | * 190 | * @internal 191 | */ 192 | static marshall(params) { 193 | const frame = new FrameImpl(params); 194 | return frame.serialize(); 195 | } 196 | /** 197 | * Escape header values 198 | */ 199 | static hdrValueEscape(str) { 200 | return str 201 | .replace(/\\/g, '\\\\') 202 | .replace(/\r/g, '\\r') 203 | .replace(/\n/g, '\\n') 204 | .replace(/:/g, '\\c'); 205 | } 206 | /** 207 | * UnEscape header values 208 | */ 209 | static hdrValueUnEscape(str) { 210 | return str 211 | .replace(/\\r/g, '\r') 212 | .replace(/\\n/g, '\n') 213 | .replace(/\\c/g, ':') 214 | .replace(/\\\\/g, '\\'); 215 | } 216 | } 217 | 218 | /** 219 | * @internal 220 | */ 221 | const NULL = 0; 222 | /** 223 | * @internal 224 | */ 225 | const LF = 10; 226 | /** 227 | * @internal 228 | */ 229 | const CR = 13; 230 | /** 231 | * @internal 232 | */ 233 | const COLON = 58; 234 | /** 235 | * This is an evented, rec descent parser. 236 | * A stream of Octets can be passed and whenever it recognizes 237 | * a complete Frame or an incoming ping it will invoke the registered callbacks. 238 | * 239 | * All incoming Octets are fed into _onByte function. 240 | * Depending on current state the _onByte function keeps changing. 241 | * Depending on the state it keeps accumulating into _token and _results. 242 | * State is indicated by current value of _onByte, all states are named as _collect. 243 | * 244 | * STOMP standards https://stomp.github.io/stomp-specification-1.2.html 245 | * imply that all lengths are considered in bytes (instead of string lengths). 246 | * So, before actual parsing, if the incoming data is String it is converted to Octets. 247 | * This allows faithful implementation of the protocol and allows NULL Octets to be present in the body. 248 | * 249 | * There is no peek function on the incoming data. 250 | * When a state change occurs based on an Octet without consuming the Octet, 251 | * the Octet, after state change, is fed again (_reinjectByte). 252 | * This became possible as the state change can be determined by inspecting just one Octet. 253 | * 254 | * There are two modes to collect the body, if content-length header is there then it by counting Octets 255 | * otherwise it is determined by NULL terminator. 256 | * 257 | * Following the standards, the command and headers are converted to Strings 258 | * and the body is returned as Octets. 259 | * Headers are returned as an array and not as Hash - to allow multiple occurrence of an header. 260 | * 261 | * This parser does not use Regular Expressions as that can only operate on Strings. 262 | * 263 | * It handles if multiple STOMP frames are given as one chunk, a frame is split into multiple chunks, or 264 | * any combination there of. The parser remembers its state (any partial frame) and continues when a new chunk 265 | * is pushed. 266 | * 267 | * Typically the higher level function will convert headers to Hash, handle unescaping of header values 268 | * (which is protocol version specific), and convert body to text. 269 | * 270 | * Check the parser.spec.js to understand cases that this parser is supposed to handle. 271 | * 272 | * Part of `@stomp/stompjs`. 273 | * 274 | * @internal 275 | */ 276 | class Parser { 277 | constructor(onFrame, onIncomingPing) { 278 | this.onFrame = onFrame; 279 | this.onIncomingPing = onIncomingPing; 280 | this._encoder = new TextEncoder(); 281 | this._decoder = new TextDecoder(); 282 | this._token = []; 283 | this._initState(); 284 | } 285 | parseChunk(segment, appendMissingNULLonIncoming = false) { 286 | let chunk; 287 | if (typeof segment === 'string') { 288 | chunk = this._encoder.encode(segment); 289 | } 290 | else { 291 | chunk = new Uint8Array(segment); 292 | } 293 | // See https://github.com/stomp-js/stompjs/issues/89 294 | // Remove when underlying issue is fixed. 295 | // 296 | // Send a NULL byte, if the last byte of a Text frame was not NULL.F 297 | if (appendMissingNULLonIncoming && chunk[chunk.length - 1] !== 0) { 298 | const chunkWithNull = new Uint8Array(chunk.length + 1); 299 | chunkWithNull.set(chunk, 0); 300 | chunkWithNull[chunk.length] = 0; 301 | chunk = chunkWithNull; 302 | } 303 | // tslint:disable-next-line:prefer-for-of 304 | for (let i = 0; i < chunk.length; i++) { 305 | const byte = chunk[i]; 306 | this._onByte(byte); 307 | } 308 | } 309 | // The following implements a simple Rec Descent Parser. 310 | // The grammar is simple and just one byte tells what should be the next state 311 | _collectFrame(byte) { 312 | if (byte === NULL) { 313 | // Ignore 314 | return; 315 | } 316 | if (byte === CR) { 317 | // Ignore CR 318 | return; 319 | } 320 | if (byte === LF) { 321 | // Incoming Ping 322 | this.onIncomingPing(); 323 | return; 324 | } 325 | this._onByte = this._collectCommand; 326 | this._reinjectByte(byte); 327 | } 328 | _collectCommand(byte) { 329 | if (byte === CR) { 330 | // Ignore CR 331 | return; 332 | } 333 | if (byte === LF) { 334 | this._results.command = this._consumeTokenAsUTF8(); 335 | this._onByte = this._collectHeaders; 336 | return; 337 | } 338 | this._consumeByte(byte); 339 | } 340 | _collectHeaders(byte) { 341 | if (byte === CR) { 342 | // Ignore CR 343 | return; 344 | } 345 | if (byte === LF) { 346 | this._setupCollectBody(); 347 | return; 348 | } 349 | this._onByte = this._collectHeaderKey; 350 | this._reinjectByte(byte); 351 | } 352 | _reinjectByte(byte) { 353 | this._onByte(byte); 354 | } 355 | _collectHeaderKey(byte) { 356 | if (byte === COLON) { 357 | this._headerKey = this._consumeTokenAsUTF8(); 358 | this._onByte = this._collectHeaderValue; 359 | return; 360 | } 361 | this._consumeByte(byte); 362 | } 363 | _collectHeaderValue(byte) { 364 | if (byte === CR) { 365 | // Ignore CR 366 | return; 367 | } 368 | if (byte === LF) { 369 | this._results.headers.push([ 370 | this._headerKey, 371 | this._consumeTokenAsUTF8(), 372 | ]); 373 | this._headerKey = undefined; 374 | this._onByte = this._collectHeaders; 375 | return; 376 | } 377 | this._consumeByte(byte); 378 | } 379 | _setupCollectBody() { 380 | const contentLengthHeader = this._results.headers.filter((header) => { 381 | return header[0] === 'content-length'; 382 | })[0]; 383 | if (contentLengthHeader) { 384 | this._bodyBytesRemaining = parseInt(contentLengthHeader[1], 10); 385 | this._onByte = this._collectBodyFixedSize; 386 | } 387 | else { 388 | this._onByte = this._collectBodyNullTerminated; 389 | } 390 | } 391 | _collectBodyNullTerminated(byte) { 392 | if (byte === NULL) { 393 | this._retrievedBody(); 394 | return; 395 | } 396 | this._consumeByte(byte); 397 | } 398 | _collectBodyFixedSize(byte) { 399 | // It is post decrement, so that we discard the trailing NULL octet 400 | if (this._bodyBytesRemaining-- === 0) { 401 | this._retrievedBody(); 402 | return; 403 | } 404 | this._consumeByte(byte); 405 | } 406 | _retrievedBody() { 407 | this._results.binaryBody = this._consumeTokenAsRaw(); 408 | try { 409 | this.onFrame(this._results); 410 | } 411 | catch (e) { 412 | console.log(`Ignoring an exception thrown by a frame handler. Original exception: `, e); 413 | } 414 | this._initState(); 415 | } 416 | // Rec Descent Parser helpers 417 | _consumeByte(byte) { 418 | this._token.push(byte); 419 | } 420 | _consumeTokenAsUTF8() { 421 | return this._decoder.decode(this._consumeTokenAsRaw()); 422 | } 423 | _consumeTokenAsRaw() { 424 | const rawResult = new Uint8Array(this._token); 425 | this._token = []; 426 | return rawResult; 427 | } 428 | _initState() { 429 | this._results = { 430 | command: undefined, 431 | headers: [], 432 | binaryBody: undefined, 433 | }; 434 | this._token = []; 435 | this._headerKey = undefined; 436 | this._onByte = this._collectFrame; 437 | } 438 | } 439 | 440 | /** 441 | * Possible states for the IStompSocket 442 | */ 443 | exports.StompSocketState = void 0; 444 | (function (StompSocketState) { 445 | StompSocketState[StompSocketState["CONNECTING"] = 0] = "CONNECTING"; 446 | StompSocketState[StompSocketState["OPEN"] = 1] = "OPEN"; 447 | StompSocketState[StompSocketState["CLOSING"] = 2] = "CLOSING"; 448 | StompSocketState[StompSocketState["CLOSED"] = 3] = "CLOSED"; 449 | })(exports.StompSocketState || (exports.StompSocketState = {})); 450 | /** 451 | * Possible activation state 452 | */ 453 | exports.ActivationState = void 0; 454 | (function (ActivationState) { 455 | ActivationState[ActivationState["ACTIVE"] = 0] = "ACTIVE"; 456 | ActivationState[ActivationState["DEACTIVATING"] = 1] = "DEACTIVATING"; 457 | ActivationState[ActivationState["INACTIVE"] = 2] = "INACTIVE"; 458 | })(exports.ActivationState || (exports.ActivationState = {})); 459 | /** 460 | * Possible reconnection wait time modes 461 | */ 462 | exports.ReconnectionTimeMode = void 0; 463 | (function (ReconnectionTimeMode) { 464 | ReconnectionTimeMode[ReconnectionTimeMode["LINEAR"] = 0] = "LINEAR"; 465 | ReconnectionTimeMode[ReconnectionTimeMode["EXPONENTIAL"] = 1] = "EXPONENTIAL"; 466 | })(exports.ReconnectionTimeMode || (exports.ReconnectionTimeMode = {})); 467 | /** 468 | * Possible ticker strategies for outgoing heartbeat ping 469 | */ 470 | exports.TickerStrategy = void 0; 471 | (function (TickerStrategy) { 472 | TickerStrategy["Interval"] = "interval"; 473 | TickerStrategy["Worker"] = "worker"; 474 | })(exports.TickerStrategy || (exports.TickerStrategy = {})); 475 | 476 | class Ticker { 477 | constructor(_interval, _strategy = exports.TickerStrategy.Interval, _debug) { 478 | this._interval = _interval; 479 | this._strategy = _strategy; 480 | this._debug = _debug; 481 | this._workerScript = ` 482 | var startTime = Date.now(); 483 | setInterval(function() { 484 | self.postMessage(Date.now() - startTime); 485 | }, ${this._interval}); 486 | `; 487 | } 488 | start(tick) { 489 | this.stop(); 490 | if (this.shouldUseWorker()) { 491 | this.runWorker(tick); 492 | } 493 | else { 494 | this.runInterval(tick); 495 | } 496 | } 497 | stop() { 498 | this.disposeWorker(); 499 | this.disposeInterval(); 500 | } 501 | shouldUseWorker() { 502 | return (typeof Worker !== 'undefined' && this._strategy === exports.TickerStrategy.Worker); 503 | } 504 | runWorker(tick) { 505 | this._debug('Using runWorker for outgoing pings'); 506 | if (!this._worker) { 507 | this._worker = new Worker(URL.createObjectURL(new Blob([this._workerScript], { type: 'text/javascript' }))); 508 | this._worker.onmessage = message => tick(message.data); 509 | } 510 | } 511 | runInterval(tick) { 512 | this._debug('Using runInterval for outgoing pings'); 513 | if (!this._timer) { 514 | const startTime = Date.now(); 515 | this._timer = setInterval(() => { 516 | tick(Date.now() - startTime); 517 | }, this._interval); 518 | } 519 | } 520 | disposeWorker() { 521 | if (this._worker) { 522 | this._worker.terminate(); 523 | delete this._worker; 524 | this._debug('Outgoing ping disposeWorker'); 525 | } 526 | } 527 | disposeInterval() { 528 | if (this._timer) { 529 | clearInterval(this._timer); 530 | delete this._timer; 531 | this._debug('Outgoing ping disposeInterval'); 532 | } 533 | } 534 | } 535 | 536 | /** 537 | * Supported STOMP versions 538 | * 539 | * Part of `@stomp/stompjs`. 540 | */ 541 | class Versions { 542 | /** 543 | * Takes an array of versions, typical elements '1.2', '1.1', or '1.0' 544 | * 545 | * You will be creating an instance of this class if you want to override 546 | * supported versions to be declared during STOMP handshake. 547 | */ 548 | constructor(versions) { 549 | this.versions = versions; 550 | } 551 | /** 552 | * Used as part of CONNECT STOMP Frame 553 | */ 554 | supportedVersions() { 555 | return this.versions.join(','); 556 | } 557 | /** 558 | * Used while creating a WebSocket 559 | */ 560 | protocolVersions() { 561 | return this.versions.map(x => `v${x.replace('.', '')}.stomp`); 562 | } 563 | } 564 | /** 565 | * Indicates protocol version 1.0 566 | */ 567 | Versions.V1_0 = '1.0'; 568 | /** 569 | * Indicates protocol version 1.1 570 | */ 571 | Versions.V1_1 = '1.1'; 572 | /** 573 | * Indicates protocol version 1.2 574 | */ 575 | Versions.V1_2 = '1.2'; 576 | /** 577 | * @internal 578 | */ 579 | Versions.default = new Versions([ 580 | Versions.V1_2, 581 | Versions.V1_1, 582 | Versions.V1_0, 583 | ]); 584 | 585 | /** 586 | * The STOMP protocol handler 587 | * 588 | * Part of `@stomp/stompjs`. 589 | * 590 | * @internal 591 | */ 592 | class StompHandler { 593 | get connectedVersion() { 594 | return this._connectedVersion; 595 | } 596 | get connected() { 597 | return this._connected; 598 | } 599 | constructor(_client, _webSocket, config) { 600 | this._client = _client; 601 | this._webSocket = _webSocket; 602 | this._connected = false; 603 | this._serverFrameHandlers = { 604 | // [CONNECTED Frame](https://stomp.github.com/stomp-specification-1.2.html#CONNECTED_Frame) 605 | CONNECTED: frame => { 606 | this.debug(`connected to server ${frame.headers.server}`); 607 | this._connected = true; 608 | this._connectedVersion = frame.headers.version; 609 | // STOMP version 1.2 needs header values to be escaped 610 | if (this._connectedVersion === Versions.V1_2) { 611 | this._escapeHeaderValues = true; 612 | } 613 | this._setupHeartbeat(frame.headers); 614 | this.onConnect(frame); 615 | }, 616 | // [MESSAGE Frame](https://stomp.github.com/stomp-specification-1.2.html#MESSAGE) 617 | MESSAGE: frame => { 618 | // the callback is registered when the client calls 619 | // `subscribe()`. 620 | // If there is no registered subscription for the received message, 621 | // the default `onUnhandledMessage` callback is used that the client can set. 622 | // This is useful for subscriptions that are automatically created 623 | // on the browser side (e.g. [RabbitMQ's temporary 624 | // queues](https://www.rabbitmq.com/stomp.html)). 625 | const subscription = frame.headers.subscription; 626 | const onReceive = this._subscriptions[subscription] || this.onUnhandledMessage; 627 | // bless the frame to be a Message 628 | const message = frame; 629 | const client = this; 630 | const messageId = this._connectedVersion === Versions.V1_2 631 | ? message.headers.ack 632 | : message.headers['message-id']; 633 | // add `ack()` and `nack()` methods directly to the returned frame 634 | // so that a simple call to `message.ack()` can acknowledge the message. 635 | message.ack = (headers = {}) => { 636 | return client.ack(messageId, subscription, headers); 637 | }; 638 | message.nack = (headers = {}) => { 639 | return client.nack(messageId, subscription, headers); 640 | }; 641 | onReceive(message); 642 | }, 643 | // [RECEIPT Frame](https://stomp.github.com/stomp-specification-1.2.html#RECEIPT) 644 | RECEIPT: frame => { 645 | const callback = this._receiptWatchers[frame.headers['receipt-id']]; 646 | if (callback) { 647 | callback(frame); 648 | // Server will acknowledge only once, remove the callback 649 | delete this._receiptWatchers[frame.headers['receipt-id']]; 650 | } 651 | else { 652 | this.onUnhandledReceipt(frame); 653 | } 654 | }, 655 | // [ERROR Frame](https://stomp.github.com/stomp-specification-1.2.html#ERROR) 656 | ERROR: frame => { 657 | this.onStompError(frame); 658 | }, 659 | }; 660 | // used to index subscribers 661 | this._counter = 0; 662 | // subscription callbacks indexed by subscriber's ID 663 | this._subscriptions = {}; 664 | // receipt-watchers indexed by receipts-ids 665 | this._receiptWatchers = {}; 666 | this._partialData = ''; 667 | this._escapeHeaderValues = false; 668 | this._lastServerActivityTS = Date.now(); 669 | this.debug = config.debug; 670 | this.stompVersions = config.stompVersions; 671 | this.connectHeaders = config.connectHeaders; 672 | this.disconnectHeaders = config.disconnectHeaders; 673 | this.heartbeatIncoming = config.heartbeatIncoming; 674 | this.heartbeatToleranceMultiplier = config.heartbeatGracePeriods; 675 | this.heartbeatOutgoing = config.heartbeatOutgoing; 676 | this.splitLargeFrames = config.splitLargeFrames; 677 | this.maxWebSocketChunkSize = config.maxWebSocketChunkSize; 678 | this.forceBinaryWSFrames = config.forceBinaryWSFrames; 679 | this.logRawCommunication = config.logRawCommunication; 680 | this.appendMissingNULLonIncoming = config.appendMissingNULLonIncoming; 681 | this.discardWebsocketOnCommFailure = config.discardWebsocketOnCommFailure; 682 | this.onConnect = config.onConnect; 683 | this.onDisconnect = config.onDisconnect; 684 | this.onStompError = config.onStompError; 685 | this.onWebSocketClose = config.onWebSocketClose; 686 | this.onWebSocketError = config.onWebSocketError; 687 | this.onUnhandledMessage = config.onUnhandledMessage; 688 | this.onUnhandledReceipt = config.onUnhandledReceipt; 689 | this.onUnhandledFrame = config.onUnhandledFrame; 690 | this.onHeartbeatReceived = config.onHeartbeatReceived; 691 | this.onHeartbeatLost = config.onHeartbeatLost; 692 | } 693 | start() { 694 | const parser = new Parser( 695 | // On Frame 696 | rawFrame => { 697 | const frame = FrameImpl.fromRawFrame(rawFrame, this._escapeHeaderValues); 698 | // if this.logRawCommunication is set, the rawChunk is logged at this._webSocket.onmessage 699 | if (!this.logRawCommunication) { 700 | this.debug(`<<< ${frame}`); 701 | } 702 | const serverFrameHandler = this._serverFrameHandlers[frame.command] || this.onUnhandledFrame; 703 | serverFrameHandler(frame); 704 | }, 705 | // On Incoming Ping 706 | () => { 707 | this.debug('<<< PONG'); 708 | this.onHeartbeatReceived(); 709 | }); 710 | this._webSocket.onmessage = (evt) => { 711 | this.debug('Received data'); 712 | this._lastServerActivityTS = Date.now(); 713 | if (this.logRawCommunication) { 714 | const rawChunkAsString = evt.data instanceof ArrayBuffer 715 | ? new TextDecoder().decode(evt.data) 716 | : evt.data; 717 | this.debug(`<<< ${rawChunkAsString}`); 718 | } 719 | parser.parseChunk(evt.data, this.appendMissingNULLonIncoming); 720 | }; 721 | this._webSocket.onclose = (closeEvent) => { 722 | this.debug(`Connection closed to ${this._webSocket.url}`); 723 | this._cleanUp(); 724 | this.onWebSocketClose(closeEvent); 725 | }; 726 | this._webSocket.onerror = (errorEvent) => { 727 | this.onWebSocketError(errorEvent); 728 | }; 729 | this._webSocket.onopen = () => { 730 | // Clone before updating 731 | const connectHeaders = Object.assign({}, this.connectHeaders); 732 | this.debug('Web Socket Opened...'); 733 | connectHeaders['accept-version'] = this.stompVersions.supportedVersions(); 734 | connectHeaders['heart-beat'] = [ 735 | this.heartbeatOutgoing, 736 | this.heartbeatIncoming, 737 | ].join(','); 738 | this._transmit({ command: 'CONNECT', headers: connectHeaders }); 739 | }; 740 | } 741 | _setupHeartbeat(headers) { 742 | if (headers.version !== Versions.V1_1 && 743 | headers.version !== Versions.V1_2) { 744 | return; 745 | } 746 | // It is valid for the server to not send this header 747 | // https://stomp.github.io/stomp-specification-1.2.html#Heart-beating 748 | if (!headers['heart-beat']) { 749 | return; 750 | } 751 | // heart-beat header received from the server looks like: 752 | // 753 | // heart-beat: sx, sy 754 | const [serverOutgoing, serverIncoming] = headers['heart-beat'] 755 | .split(',') 756 | .map((v) => parseInt(v, 10)); 757 | if (this.heartbeatOutgoing !== 0 && serverIncoming !== 0) { 758 | const ttl = Math.max(this.heartbeatOutgoing, serverIncoming); 759 | this.debug(`send PING every ${ttl}ms`); 760 | this._pinger = new Ticker(ttl, this._client.heartbeatStrategy, this.debug); 761 | this._pinger.start(() => { 762 | if (this._webSocket.readyState === exports.StompSocketState.OPEN) { 763 | this._webSocket.send(BYTE.LF); 764 | this.debug('>>> PING'); 765 | } 766 | }); 767 | } 768 | if (this.heartbeatIncoming !== 0 && serverOutgoing !== 0) { 769 | const ttl = Math.max(this.heartbeatIncoming, serverOutgoing); 770 | this.debug(`check PONG every ${ttl}ms`); 771 | this._ponger = setInterval(() => { 772 | const delta = Date.now() - this._lastServerActivityTS; 773 | // We wait multiple grace periods to be flexible on window's setInterval calls 774 | if (delta > ttl * this.heartbeatToleranceMultiplier) { 775 | this.debug(`did not receive server activity for the last ${delta}ms`); 776 | this.onHeartbeatLost(); 777 | this._closeOrDiscardWebsocket(); 778 | } 779 | }, ttl); 780 | } 781 | } 782 | _closeOrDiscardWebsocket() { 783 | if (this.discardWebsocketOnCommFailure) { 784 | this.debug('Discarding websocket, the underlying socket may linger for a while'); 785 | this.discardWebsocket(); 786 | } 787 | else { 788 | this.debug('Issuing close on the websocket'); 789 | this._closeWebsocket(); 790 | } 791 | } 792 | forceDisconnect() { 793 | if (this._webSocket) { 794 | if (this._webSocket.readyState === exports.StompSocketState.CONNECTING || 795 | this._webSocket.readyState === exports.StompSocketState.OPEN) { 796 | this._closeOrDiscardWebsocket(); 797 | } 798 | } 799 | } 800 | _closeWebsocket() { 801 | this._webSocket.onmessage = () => { }; // ignore messages 802 | this._webSocket.close(); 803 | } 804 | discardWebsocket() { 805 | if (typeof this._webSocket.terminate !== 'function') { 806 | augmentWebsocket(this._webSocket, (msg) => this.debug(msg)); 807 | } 808 | // @ts-ignore - this method will be there at this stage 809 | this._webSocket.terminate(); 810 | } 811 | _transmit(params) { 812 | const { command, headers, body, binaryBody, skipContentLengthHeader } = params; 813 | const frame = new FrameImpl({ 814 | command, 815 | headers, 816 | body, 817 | binaryBody, 818 | escapeHeaderValues: this._escapeHeaderValues, 819 | skipContentLengthHeader, 820 | }); 821 | let rawChunk = frame.serialize(); 822 | if (this.logRawCommunication) { 823 | this.debug(`>>> ${rawChunk}`); 824 | } 825 | else { 826 | this.debug(`>>> ${frame}`); 827 | } 828 | if (this.forceBinaryWSFrames && typeof rawChunk === 'string') { 829 | rawChunk = new TextEncoder().encode(rawChunk); 830 | } 831 | if (typeof rawChunk !== 'string' || !this.splitLargeFrames) { 832 | this._webSocket.send(rawChunk); 833 | } 834 | else { 835 | let out = rawChunk; 836 | while (out.length > 0) { 837 | const chunk = out.substring(0, this.maxWebSocketChunkSize); 838 | out = out.substring(this.maxWebSocketChunkSize); 839 | this._webSocket.send(chunk); 840 | this.debug(`chunk sent = ${chunk.length}, remaining = ${out.length}`); 841 | } 842 | } 843 | } 844 | dispose() { 845 | if (this.connected) { 846 | try { 847 | // clone before updating 848 | const disconnectHeaders = Object.assign({}, this.disconnectHeaders); 849 | if (!disconnectHeaders.receipt) { 850 | disconnectHeaders.receipt = `close-${this._counter++}`; 851 | } 852 | this.watchForReceipt(disconnectHeaders.receipt, frame => { 853 | this._closeWebsocket(); 854 | this._cleanUp(); 855 | this.onDisconnect(frame); 856 | }); 857 | this._transmit({ command: 'DISCONNECT', headers: disconnectHeaders }); 858 | } 859 | catch (error) { 860 | this.debug(`Ignoring error during disconnect ${error}`); 861 | } 862 | } 863 | else { 864 | if (this._webSocket.readyState === exports.StompSocketState.CONNECTING || 865 | this._webSocket.readyState === exports.StompSocketState.OPEN) { 866 | this._closeWebsocket(); 867 | } 868 | } 869 | } 870 | _cleanUp() { 871 | this._connected = false; 872 | if (this._pinger) { 873 | this._pinger.stop(); 874 | this._pinger = undefined; 875 | } 876 | if (this._ponger) { 877 | clearInterval(this._ponger); 878 | this._ponger = undefined; 879 | } 880 | } 881 | publish(params) { 882 | const { destination, headers, body, binaryBody, skipContentLengthHeader } = params; 883 | const hdrs = Object.assign({ destination }, headers); 884 | this._transmit({ 885 | command: 'SEND', 886 | headers: hdrs, 887 | body, 888 | binaryBody, 889 | skipContentLengthHeader, 890 | }); 891 | } 892 | watchForReceipt(receiptId, callback) { 893 | this._receiptWatchers[receiptId] = callback; 894 | } 895 | subscribe(destination, callback, headers = {}) { 896 | headers = Object.assign({}, headers); 897 | if (!headers.id) { 898 | headers.id = `sub-${this._counter++}`; 899 | } 900 | headers.destination = destination; 901 | this._subscriptions[headers.id] = callback; 902 | this._transmit({ command: 'SUBSCRIBE', headers }); 903 | const client = this; 904 | return { 905 | id: headers.id, 906 | unsubscribe(hdrs) { 907 | return client.unsubscribe(headers.id, hdrs); 908 | }, 909 | }; 910 | } 911 | unsubscribe(id, headers = {}) { 912 | headers = Object.assign({}, headers); 913 | delete this._subscriptions[id]; 914 | headers.id = id; 915 | this._transmit({ command: 'UNSUBSCRIBE', headers }); 916 | } 917 | begin(transactionId) { 918 | const txId = transactionId || `tx-${this._counter++}`; 919 | this._transmit({ 920 | command: 'BEGIN', 921 | headers: { 922 | transaction: txId, 923 | }, 924 | }); 925 | const client = this; 926 | return { 927 | id: txId, 928 | commit() { 929 | client.commit(txId); 930 | }, 931 | abort() { 932 | client.abort(txId); 933 | }, 934 | }; 935 | } 936 | commit(transactionId) { 937 | this._transmit({ 938 | command: 'COMMIT', 939 | headers: { 940 | transaction: transactionId, 941 | }, 942 | }); 943 | } 944 | abort(transactionId) { 945 | this._transmit({ 946 | command: 'ABORT', 947 | headers: { 948 | transaction: transactionId, 949 | }, 950 | }); 951 | } 952 | ack(messageId, subscriptionId, headers = {}) { 953 | headers = Object.assign({}, headers); 954 | if (this._connectedVersion === Versions.V1_2) { 955 | headers.id = messageId; 956 | } 957 | else { 958 | headers['message-id'] = messageId; 959 | } 960 | headers.subscription = subscriptionId; 961 | this._transmit({ command: 'ACK', headers }); 962 | } 963 | nack(messageId, subscriptionId, headers = {}) { 964 | headers = Object.assign({}, headers); 965 | if (this._connectedVersion === Versions.V1_2) { 966 | headers.id = messageId; 967 | } 968 | else { 969 | headers['message-id'] = messageId; 970 | } 971 | headers.subscription = subscriptionId; 972 | return this._transmit({ command: 'NACK', headers }); 973 | } 974 | } 975 | 976 | /** 977 | * STOMP Client Class. 978 | * 979 | * Part of `@stomp/stompjs`. 980 | * 981 | * This class provides a robust implementation for connecting to and interacting with a 982 | * STOMP-compliant messaging broker over WebSocket. It supports STOMP versions 1.2, 1.1, and 1.0. 983 | * 984 | * Features: 985 | * - Handles automatic reconnections. 986 | * - Supports heartbeat mechanisms to detect and report communication failures. 987 | * - Allows customization of connection and WebSocket behaviors through configurations. 988 | * - Compatible with both browser environments and Node.js with polyfill support for WebSocket. 989 | */ 990 | class Client { 991 | /** 992 | * Provides access to the underlying WebSocket instance. 993 | * This property is **read-only**. 994 | * 995 | * Example: 996 | * ```javascript 997 | * const webSocket = client.webSocket; 998 | * if (webSocket) { 999 | * console.log('WebSocket is connected:', webSocket.readyState === WebSocket.OPEN); 1000 | * } 1001 | * ``` 1002 | * 1003 | * **Caution:** 1004 | * Directly interacting with the WebSocket instance (e.g., sending or receiving frames) 1005 | * can interfere with the proper functioning of this library. Such actions may cause 1006 | * unexpected behavior, disconnections, or invalid state in the library's internal mechanisms. 1007 | * 1008 | * Instead, use the library's provided methods to manage STOMP communication. 1009 | * 1010 | * @returns The WebSocket instance used by the STOMP handler, or `undefined` if not connected. 1011 | */ 1012 | get webSocket() { 1013 | return this._stompHandler?._webSocket; 1014 | } 1015 | /** 1016 | * Allows customization of the disconnection headers. 1017 | * 1018 | * Any changes made during an active session will also be applied immediately. 1019 | * 1020 | * Example: 1021 | * ```javascript 1022 | * client.disconnectHeaders = { 1023 | * receipt: 'custom-receipt-id' 1024 | * }; 1025 | * ``` 1026 | */ 1027 | get disconnectHeaders() { 1028 | return this._disconnectHeaders; 1029 | } 1030 | set disconnectHeaders(value) { 1031 | this._disconnectHeaders = value; 1032 | if (this._stompHandler) { 1033 | this._stompHandler.disconnectHeaders = this._disconnectHeaders; 1034 | } 1035 | } 1036 | /** 1037 | * Indicates whether there is an active connection to the STOMP broker. 1038 | * 1039 | * Usage: 1040 | * ```javascript 1041 | * if (client.connected) { 1042 | * console.log('Client is connected to the broker.'); 1043 | * } else { 1044 | * console.log('No connection to the broker.'); 1045 | * } 1046 | * ``` 1047 | * 1048 | * @returns `true` if the client is currently connected, `false` otherwise. 1049 | */ 1050 | get connected() { 1051 | return !!this._stompHandler && this._stompHandler.connected; 1052 | } 1053 | /** 1054 | * The version of the STOMP protocol negotiated with the server during connection. 1055 | * 1056 | * This is a **read-only** property and reflects the negotiated protocol version after 1057 | * a successful connection. 1058 | * 1059 | * Example: 1060 | * ```javascript 1061 | * console.log('Connected STOMP version:', client.connectedVersion); 1062 | * ``` 1063 | * 1064 | * @returns The negotiated STOMP protocol version or `undefined` if not connected. 1065 | */ 1066 | get connectedVersion() { 1067 | return this._stompHandler ? this._stompHandler.connectedVersion : undefined; 1068 | } 1069 | /** 1070 | * Indicates whether the client is currently active. 1071 | * 1072 | * A client is considered active if it is connected or actively attempting to reconnect. 1073 | * 1074 | * Example: 1075 | * ```javascript 1076 | * if (client.active) { 1077 | * console.log('The client is active.'); 1078 | * } else { 1079 | * console.log('The client is inactive.'); 1080 | * } 1081 | * ``` 1082 | * 1083 | * @returns `true` if the client is active, otherwise `false`. 1084 | */ 1085 | get active() { 1086 | return this.state === exports.ActivationState.ACTIVE; 1087 | } 1088 | _changeState(state) { 1089 | this.state = state; 1090 | this.onChangeState(state); 1091 | } 1092 | /** 1093 | * Constructs a new STOMP client instance. 1094 | * 1095 | * The constructor initializes default values and sets up no-op callbacks for all events. 1096 | * Configuration can be passed during construction, or updated later using `configure`. 1097 | * 1098 | * Example: 1099 | * ```javascript 1100 | * const client = new Client({ 1101 | * brokerURL: 'wss://broker.example.com', 1102 | * reconnectDelay: 5000 1103 | * }); 1104 | * ``` 1105 | * 1106 | * @param conf Optional configuration object to initialize the client with. 1107 | */ 1108 | constructor(conf = {}) { 1109 | /** 1110 | * STOMP protocol versions to use during the handshake. By default, the client will attempt 1111 | * versions `1.2`, `1.1`, and `1.0` in descending order of preference. 1112 | * 1113 | * Example: 1114 | * ```javascript 1115 | * // Configure the client to only use versions 1.1 and 1.0 1116 | * client.stompVersions = new Versions(['1.1', '1.0']); 1117 | * ``` 1118 | */ 1119 | this.stompVersions = Versions.default; 1120 | /** 1121 | * Timeout for establishing STOMP connection, in milliseconds. 1122 | * 1123 | * If the connection is not established within this period, the attempt will fail. 1124 | * The default is `0`, meaning no timeout is set for connection attempts. 1125 | * 1126 | * Example: 1127 | * ```javascript 1128 | * client.connectionTimeout = 5000; // Fail connection if not established in 5 seconds 1129 | * ``` 1130 | */ 1131 | this.connectionTimeout = 0; 1132 | /** 1133 | * Delay (in milliseconds) between reconnection attempts if the connection drops. 1134 | * 1135 | * Set to `0` to disable automatic reconnections. The default value is `5000` ms (5 seconds). 1136 | * 1137 | * Example: 1138 | * ```javascript 1139 | * client.reconnectDelay = 3000; // Attempt reconnection every 3 seconds 1140 | * client.reconnectDelay = 0; // Disable automatic reconnection 1141 | * ``` 1142 | */ 1143 | this.reconnectDelay = 5000; 1144 | /** 1145 | * The next reconnection delay, used internally. 1146 | * Initialized to the value of [Client#reconnectDelay]{@link Client#reconnectDelay}, and it may 1147 | * dynamically change based on [Client#reconnectTimeMode]{@link Client#reconnectTimeMode}. 1148 | */ 1149 | this._nextReconnectDelay = 0; 1150 | /** 1151 | * Maximum delay (in milliseconds) between reconnection attempts when using exponential backoff. 1152 | * 1153 | * Default is 15 minutes (`15 * 60 * 1000` milliseconds). If `0`, there will be no upper limit. 1154 | * 1155 | * Example: 1156 | * ```javascript 1157 | * client.maxReconnectDelay = 10000; // Maximum wait time is 10 seconds 1158 | * ``` 1159 | */ 1160 | this.maxReconnectDelay = 15 * 60 * 1000; 1161 | /** 1162 | * Mode for determining the time interval between reconnection attempts. 1163 | * 1164 | * Available modes: 1165 | * - `ReconnectionTimeMode.LINEAR` (default): Fixed delays between reconnection attempts. 1166 | * - `ReconnectionTimeMode.EXPONENTIAL`: Delay doubles after each attempt, capped by [maxReconnectDelay]{@link Client#maxReconnectDelay}. 1167 | * 1168 | * Example: 1169 | * ```javascript 1170 | * client.reconnectTimeMode = ReconnectionTimeMode.EXPONENTIAL; 1171 | * client.reconnectDelay = 200; // Initial delay of 200 ms, doubles with each attempt 1172 | * client.maxReconnectDelay = 2 * 60 * 1000; // Cap delay at 10 minutes 1173 | * ``` 1174 | */ 1175 | this.reconnectTimeMode = exports.ReconnectionTimeMode.LINEAR; 1176 | /** 1177 | * Interval (in milliseconds) for receiving heartbeat signals from the server. 1178 | * 1179 | * Specifies the expected frequency of heartbeats sent by the server. Set to `0` to disable. 1180 | * 1181 | * Example: 1182 | * ```javascript 1183 | * client.heartbeatIncoming = 10000; // Expect a heartbeat every 10 seconds 1184 | * ``` 1185 | */ 1186 | this.heartbeatIncoming = 10000; 1187 | /** 1188 | * Multiplier for adjusting tolerance when processing heartbeat signals. 1189 | * 1190 | * Tolerance level is calculated using the multiplier: 1191 | * `tolerance = heartbeatIncoming * heartbeatToleranceMultiplier`. 1192 | * This helps account for delays in network communication or variations in timings. 1193 | * 1194 | * Default value is `2`. 1195 | * 1196 | * Example: 1197 | * ```javascript 1198 | * client.heartbeatToleranceMultiplier = 2.5; // Tolerates longer delays 1199 | * ``` 1200 | */ 1201 | this.heartbeatToleranceMultiplier = 2; 1202 | /** 1203 | * Interval (in milliseconds) for sending heartbeat signals to the server. 1204 | * 1205 | * Specifies how frequently heartbeats should be sent to the server. Set to `0` to disable. 1206 | * 1207 | * Example: 1208 | * ```javascript 1209 | * client.heartbeatOutgoing = 5000; // Send a heartbeat every 5 seconds 1210 | * ``` 1211 | */ 1212 | this.heartbeatOutgoing = 10000; 1213 | /** 1214 | * Strategy for sending outgoing heartbeats. 1215 | * 1216 | * Options: 1217 | * - `TickerStrategy.Worker`: Uses Web Workers for sending heartbeats (recommended for long-running or background sessions). 1218 | * - `TickerStrategy.Interval`: Uses standard JavaScript `setInterval` (default). 1219 | * 1220 | * Note: 1221 | * - If Web Workers are unavailable (e.g., in Node.js), the `Interval` strategy is used automatically. 1222 | * - Web Workers are preferable in browsers for reducing disconnects when tabs are in the background. 1223 | * 1224 | * Example: 1225 | * ```javascript 1226 | * client.heartbeatStrategy = TickerStrategy.Worker; 1227 | * ``` 1228 | */ 1229 | this.heartbeatStrategy = exports.TickerStrategy.Interval; 1230 | /** 1231 | * Enables splitting of large text WebSocket frames into smaller chunks. 1232 | * 1233 | * This setting is enabled for brokers that support only chunked messages (e.g., Java Spring-based brokers). 1234 | * Default is `false`. 1235 | * 1236 | * Warning: 1237 | * - Should not be used with WebSocket-compliant brokers, as chunking may cause large message failures. 1238 | * - Binary WebSocket frames are never split. 1239 | * 1240 | * Example: 1241 | * ```javascript 1242 | * client.splitLargeFrames = true; 1243 | * client.maxWebSocketChunkSize = 4096; // Allow chunks of 4 KB 1244 | * ``` 1245 | */ 1246 | this.splitLargeFrames = false; 1247 | /** 1248 | * Maximum size (in bytes) for individual WebSocket chunks if [splitLargeFrames]{@link Client#splitLargeFrames} is enabled. 1249 | * 1250 | * Default is 8 KB (`8 * 1024` bytes). This value has no effect if [splitLargeFrames]{@link Client#splitLargeFrames} is `false`. 1251 | */ 1252 | this.maxWebSocketChunkSize = 8 * 1024; 1253 | /** 1254 | * Forces all WebSocket frames to use binary transport, irrespective of payload type. 1255 | * 1256 | * Default behavior determines frame type based on payload (e.g., binary data for ArrayBuffers). 1257 | * 1258 | * Example: 1259 | * ```javascript 1260 | * client.forceBinaryWSFrames = true; 1261 | * ``` 1262 | */ 1263 | this.forceBinaryWSFrames = false; 1264 | /** 1265 | * Workaround for a React Native WebSocket bug, where messages containing `NULL` are chopped. 1266 | * 1267 | * Enabling this appends a `NULL` character to incoming frames to ensure they remain valid STOMP packets. 1268 | * 1269 | * Warning: 1270 | * - For brokers that split large messages, this may cause data loss or connection termination. 1271 | * 1272 | * Example: 1273 | * ```javascript 1274 | * client.appendMissingNULLonIncoming = true; 1275 | * ``` 1276 | */ 1277 | this.appendMissingNULLonIncoming = false; 1278 | /** 1279 | * Instruct the library to immediately terminate the socket on communication failures, even 1280 | * before the WebSocket is completely closed. 1281 | * 1282 | * This is particularly useful in browser environments where WebSocket closure may get delayed, 1283 | * causing prolonged reconnection intervals under certain failure conditions. 1284 | * 1285 | * 1286 | * Example: 1287 | * ```javascript 1288 | * client.discardWebsocketOnCommFailure = true; // Enable aggressive closing of WebSocket 1289 | * ``` 1290 | * 1291 | * Default value: `false`. 1292 | */ 1293 | this.discardWebsocketOnCommFailure = false; 1294 | /** 1295 | * Current activation state of the client. 1296 | * 1297 | * Possible states: 1298 | * - `ActivationState.ACTIVE`: Client is connected or actively attempting to connect. 1299 | * - `ActivationState.INACTIVE`: Client is disconnected and not attempting to reconnect. 1300 | * - `ActivationState.DEACTIVATING`: Client is in the process of disconnecting. 1301 | * 1302 | * Note: The client may transition directly from `ACTIVE` to `INACTIVE` without entering 1303 | * the `DEACTIVATING` state. 1304 | */ 1305 | this.state = exports.ActivationState.INACTIVE; 1306 | // No op callbacks 1307 | const noOp = () => { }; 1308 | this.debug = noOp; 1309 | this.beforeConnect = noOp; 1310 | this.onConnect = noOp; 1311 | this.onDisconnect = noOp; 1312 | this.onUnhandledMessage = noOp; 1313 | this.onUnhandledReceipt = noOp; 1314 | this.onUnhandledFrame = noOp; 1315 | this.onHeartbeatReceived = noOp; 1316 | this.onHeartbeatLost = noOp; 1317 | this.onStompError = noOp; 1318 | this.onWebSocketClose = noOp; 1319 | this.onWebSocketError = noOp; 1320 | this.logRawCommunication = false; 1321 | this.onChangeState = noOp; 1322 | // These parameters would typically get proper values before connect is called 1323 | this.connectHeaders = {}; 1324 | this._disconnectHeaders = {}; 1325 | // Apply configuration 1326 | this.configure(conf); 1327 | } 1328 | /** 1329 | * Updates the client's configuration. 1330 | * 1331 | * All properties in the provided configuration object will override the current settings. 1332 | * 1333 | * Additionally, a warning is logged if `maxReconnectDelay` is configured to a 1334 | * value lower than `reconnectDelay`, and `maxReconnectDelay` is adjusted to match `reconnectDelay`. 1335 | * 1336 | * Example: 1337 | * ```javascript 1338 | * client.configure({ 1339 | * reconnectDelay: 3000, 1340 | * maxReconnectDelay: 10000 1341 | * }); 1342 | * ``` 1343 | * 1344 | * @param conf Configuration object containing the new settings. 1345 | */ 1346 | configure(conf) { 1347 | // bulk assign all properties to this 1348 | Object.assign(this, conf); 1349 | // Warn on incorrect maxReconnectDelay settings 1350 | if (this.maxReconnectDelay > 0 && 1351 | this.maxReconnectDelay < this.reconnectDelay) { 1352 | this.debug(`Warning: maxReconnectDelay (${this.maxReconnectDelay}ms) is less than reconnectDelay (${this.reconnectDelay}ms). Using reconnectDelay as the maxReconnectDelay delay.`); 1353 | this.maxReconnectDelay = this.reconnectDelay; 1354 | } 1355 | } 1356 | /** 1357 | * Activates the client, initiating a connection to the STOMP broker. 1358 | * 1359 | * On activation, the client attempts to connect and sets its state to `ACTIVE`. If the connection 1360 | * is lost, it will automatically retry based on `reconnectDelay` or `maxReconnectDelay`. If 1361 | * `reconnectTimeMode` is set to `EXPONENTIAL`, the reconnect delay increases exponentially. 1362 | * 1363 | * To stop reconnection attempts and disconnect, call [Client#deactivate]{@link Client#deactivate}. 1364 | * 1365 | * Example: 1366 | * ```javascript 1367 | * client.activate(); // Connect to the broker 1368 | * ``` 1369 | * 1370 | * If the client is currently `DEACTIVATING`, connection is delayed until the deactivation process completes. 1371 | */ 1372 | activate() { 1373 | const _activate = () => { 1374 | if (this.active) { 1375 | this.debug('Already ACTIVE, ignoring request to activate'); 1376 | return; 1377 | } 1378 | this._changeState(exports.ActivationState.ACTIVE); 1379 | this._nextReconnectDelay = this.reconnectDelay; 1380 | this._connect(); 1381 | }; 1382 | // if it is deactivating, wait for it to complete before activating. 1383 | if (this.state === exports.ActivationState.DEACTIVATING) { 1384 | this.debug('Waiting for deactivation to finish before activating'); 1385 | this.deactivate().then(() => { 1386 | _activate(); 1387 | }); 1388 | } 1389 | else { 1390 | _activate(); 1391 | } 1392 | } 1393 | async _connect() { 1394 | await this.beforeConnect(this); 1395 | if (this._stompHandler) { 1396 | this.debug('There is already a stompHandler, skipping the call to connect'); 1397 | return; 1398 | } 1399 | if (!this.active) { 1400 | this.debug('Client has been marked inactive, will not attempt to connect'); 1401 | return; 1402 | } 1403 | // setup connection watcher 1404 | if (this.connectionTimeout > 0) { 1405 | // clear first 1406 | if (this._connectionWatcher) { 1407 | clearTimeout(this._connectionWatcher); 1408 | } 1409 | this._connectionWatcher = setTimeout(() => { 1410 | if (this.connected) { 1411 | return; 1412 | } 1413 | // Connection not established, close the underlying socket 1414 | // a reconnection will be attempted 1415 | this.debug(`Connection not established in ${this.connectionTimeout}ms, closing socket`); 1416 | this.forceDisconnect(); 1417 | }, this.connectionTimeout); 1418 | } 1419 | this.debug('Opening Web Socket...'); 1420 | // Get the actual WebSocket (or a similar object) 1421 | const webSocket = this._createWebSocket(); 1422 | this._stompHandler = new StompHandler(this, webSocket, { 1423 | debug: this.debug, 1424 | stompVersions: this.stompVersions, 1425 | connectHeaders: this.connectHeaders, 1426 | disconnectHeaders: this._disconnectHeaders, 1427 | heartbeatIncoming: this.heartbeatIncoming, 1428 | heartbeatGracePeriods: this.heartbeatToleranceMultiplier, 1429 | heartbeatOutgoing: this.heartbeatOutgoing, 1430 | heartbeatStrategy: this.heartbeatStrategy, 1431 | splitLargeFrames: this.splitLargeFrames, 1432 | maxWebSocketChunkSize: this.maxWebSocketChunkSize, 1433 | forceBinaryWSFrames: this.forceBinaryWSFrames, 1434 | logRawCommunication: this.logRawCommunication, 1435 | appendMissingNULLonIncoming: this.appendMissingNULLonIncoming, 1436 | discardWebsocketOnCommFailure: this.discardWebsocketOnCommFailure, 1437 | onConnect: frame => { 1438 | // Successfully connected, stop the connection watcher 1439 | if (this._connectionWatcher) { 1440 | clearTimeout(this._connectionWatcher); 1441 | this._connectionWatcher = undefined; 1442 | } 1443 | // Reset reconnect delay after successful connection 1444 | this._nextReconnectDelay = this.reconnectDelay; 1445 | if (!this.active) { 1446 | this.debug('STOMP got connected while deactivate was issued, will disconnect now'); 1447 | this._disposeStompHandler(); 1448 | return; 1449 | } 1450 | this.onConnect(frame); 1451 | }, 1452 | onDisconnect: frame => { 1453 | this.onDisconnect(frame); 1454 | }, 1455 | onStompError: frame => { 1456 | this.onStompError(frame); 1457 | }, 1458 | onWebSocketClose: evt => { 1459 | this._stompHandler = undefined; // a new one will be created in case of a reconnect 1460 | if (this.state === exports.ActivationState.DEACTIVATING) { 1461 | // Mark deactivation complete 1462 | this._changeState(exports.ActivationState.INACTIVE); 1463 | } 1464 | // The callback is called before attempting to reconnect, this would allow the client 1465 | // to be `deactivated` in the callback. 1466 | this.onWebSocketClose(evt); 1467 | if (this.active) { 1468 | this._schedule_reconnect(); 1469 | } 1470 | }, 1471 | onWebSocketError: evt => { 1472 | this.onWebSocketError(evt); 1473 | }, 1474 | onUnhandledMessage: message => { 1475 | this.onUnhandledMessage(message); 1476 | }, 1477 | onUnhandledReceipt: frame => { 1478 | this.onUnhandledReceipt(frame); 1479 | }, 1480 | onUnhandledFrame: frame => { 1481 | this.onUnhandledFrame(frame); 1482 | }, 1483 | onHeartbeatReceived: () => { 1484 | this.onHeartbeatReceived(); 1485 | }, 1486 | onHeartbeatLost: () => { 1487 | this.onHeartbeatLost(); 1488 | }, 1489 | }); 1490 | this._stompHandler.start(); 1491 | } 1492 | _createWebSocket() { 1493 | let webSocket; 1494 | if (this.webSocketFactory) { 1495 | webSocket = this.webSocketFactory(); 1496 | } 1497 | else if (this.brokerURL) { 1498 | webSocket = new WebSocket(this.brokerURL, this.stompVersions.protocolVersions()); 1499 | } 1500 | else { 1501 | throw new Error('Either brokerURL or webSocketFactory must be provided'); 1502 | } 1503 | webSocket.binaryType = 'arraybuffer'; 1504 | return webSocket; 1505 | } 1506 | _schedule_reconnect() { 1507 | if (this._nextReconnectDelay > 0) { 1508 | this.debug(`STOMP: scheduling reconnection in ${this._nextReconnectDelay}ms`); 1509 | this._reconnector = setTimeout(() => { 1510 | if (this.reconnectTimeMode === exports.ReconnectionTimeMode.EXPONENTIAL) { 1511 | this._nextReconnectDelay = this._nextReconnectDelay * 2; 1512 | // Truncated exponential backoff with a set limit unless disabled 1513 | if (this.maxReconnectDelay !== 0) { 1514 | this._nextReconnectDelay = Math.min(this._nextReconnectDelay, this.maxReconnectDelay); 1515 | } 1516 | } 1517 | this._connect(); 1518 | }, this._nextReconnectDelay); 1519 | } 1520 | } 1521 | /** 1522 | * Disconnects the client and stops the automatic reconnection loop. 1523 | * 1524 | * If there is an active STOMP connection at the time of invocation, the appropriate callbacks 1525 | * will be triggered during the shutdown sequence. Once deactivated, the client will enter the 1526 | * `INACTIVE` state, and no further reconnection attempts will be made. 1527 | * 1528 | * **Behavior**: 1529 | * - If there is no active WebSocket connection, this method resolves immediately. 1530 | * - If there is an active connection, the method waits for the underlying WebSocket 1531 | * to properly close before resolving. 1532 | * - Multiple calls to this method are safe. Each invocation resolves upon completion. 1533 | * - To reactivate, call [Client#activate]{@link Client#activate}. 1534 | * 1535 | * **Experimental Option:** 1536 | * - By specifying the `force: true` option, the WebSocket connection is discarded immediately, 1537 | * bypassing both the STOMP and WebSocket shutdown sequences. 1538 | * - **Caution:** Using `force: true` may leave the WebSocket in an inconsistent state, 1539 | * and brokers may not immediately detect the termination. 1540 | * 1541 | * Example: 1542 | * ```javascript 1543 | * // Graceful disconnect 1544 | * await client.deactivate(); 1545 | * 1546 | * // Forced disconnect to speed up shutdown when the connection is stale 1547 | * await client.deactivate({ force: true }); 1548 | * ``` 1549 | * 1550 | * @param options Configuration options for deactivation. Use `force: true` for immediate shutdown. 1551 | * @returns A Promise that resolves when the deactivation process completes. 1552 | */ 1553 | async deactivate(options = {}) { 1554 | const force = options.force || false; 1555 | const needToDispose = this.active; 1556 | let retPromise; 1557 | if (this.state === exports.ActivationState.INACTIVE) { 1558 | this.debug(`Already INACTIVE, nothing more to do`); 1559 | return Promise.resolve(); 1560 | } 1561 | this._changeState(exports.ActivationState.DEACTIVATING); 1562 | // Clear reconnection timer just to be safe 1563 | this._nextReconnectDelay = 0; 1564 | // Clear if a reconnection was scheduled 1565 | if (this._reconnector) { 1566 | clearTimeout(this._reconnector); 1567 | this._reconnector = undefined; 1568 | } 1569 | if (this._stompHandler && 1570 | // @ts-ignore - if there is a _stompHandler, there is the webSocket 1571 | this.webSocket.readyState !== exports.StompSocketState.CLOSED) { 1572 | const origOnWebSocketClose = this._stompHandler.onWebSocketClose; 1573 | // we need to wait for the underlying websocket to close 1574 | retPromise = new Promise((resolve, reject) => { 1575 | // @ts-ignore - there is a _stompHandler 1576 | this._stompHandler.onWebSocketClose = evt => { 1577 | origOnWebSocketClose(evt); 1578 | resolve(); 1579 | }; 1580 | }); 1581 | } 1582 | else { 1583 | // indicate that auto reconnect loop should terminate 1584 | this._changeState(exports.ActivationState.INACTIVE); 1585 | return Promise.resolve(); 1586 | } 1587 | if (force) { 1588 | this._stompHandler?.discardWebsocket(); 1589 | } 1590 | else if (needToDispose) { 1591 | this._disposeStompHandler(); 1592 | } 1593 | return retPromise; 1594 | } 1595 | /** 1596 | * Forces a disconnect by directly closing the WebSocket. 1597 | * 1598 | * Unlike a normal disconnect, this does not send a DISCONNECT sequence to the broker but 1599 | * instead closes the WebSocket connection directly. After forcing a disconnect, the client 1600 | * will automatically attempt to reconnect based on its `reconnectDelay` configuration. 1601 | * 1602 | * **Note:** To prevent further reconnect attempts, call [Client#deactivate]{@link Client#deactivate}. 1603 | * 1604 | * Example: 1605 | * ```javascript 1606 | * client.forceDisconnect(); 1607 | * ``` 1608 | */ 1609 | forceDisconnect() { 1610 | if (this._stompHandler) { 1611 | this._stompHandler.forceDisconnect(); 1612 | } 1613 | } 1614 | _disposeStompHandler() { 1615 | // Dispose STOMP Handler 1616 | if (this._stompHandler) { 1617 | this._stompHandler.dispose(); 1618 | } 1619 | } 1620 | /** 1621 | * Sends a message to the specified destination on the STOMP broker. 1622 | * 1623 | * The `body` must be a `string`. For non-string payloads (e.g., JSON), encode it as a string before sending. 1624 | * If sending binary data, use the `binaryBody` parameter as a [Uint8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array). 1625 | * 1626 | * **Content-Length Behavior**: 1627 | * - For non-binary messages, the `content-length` header is added by default. 1628 | * - The `content-length` header can be skipped for text frames by setting `skipContentLengthHeader: true` in the parameters. 1629 | * - For binary messages, the `content-length` header is always included. 1630 | * 1631 | * **Notes**: 1632 | * - Ensure that brokers support binary frames before using `binaryBody`. 1633 | * - Sending messages with NULL octets and missing `content-length` headers can cause brokers to disconnect and throw errors. 1634 | * 1635 | * Example: 1636 | * ```javascript 1637 | * // Basic text message 1638 | * client.publish({ destination: "/queue/test", body: "Hello, STOMP" }); 1639 | * 1640 | * // Text message with additional headers 1641 | * client.publish({ destination: "/queue/test", headers: { priority: 9 }, body: "Hello, STOMP" }); 1642 | * 1643 | * // Skip content-length header 1644 | * client.publish({ destination: "/queue/test", body: "Hello, STOMP", skipContentLengthHeader: true }); 1645 | * 1646 | * // Binary message 1647 | * const binaryData = new Uint8Array([1, 2, 3, 4]); 1648 | * client.publish({ 1649 | * destination: '/topic/special', 1650 | * binaryBody: binaryData, 1651 | * headers: { 'content-type': 'application/octet-stream' } 1652 | * }); 1653 | * ``` 1654 | */ 1655 | publish(params) { 1656 | this._checkConnection(); 1657 | // @ts-ignore - we already checked that there is a _stompHandler, and it is connected 1658 | this._stompHandler.publish(params); 1659 | } 1660 | _checkConnection() { 1661 | if (!this.connected) { 1662 | throw new TypeError('There is no underlying STOMP connection'); 1663 | } 1664 | } 1665 | /** 1666 | * Monitors for a receipt acknowledgment from the broker for specific operations. 1667 | * 1668 | * Add a `receipt` header to the operation (like subscribe or publish), and use this method with 1669 | * the same receipt ID to detect when the broker has acknowledged the operation's completion. 1670 | * 1671 | * The callback is invoked with the corresponding {@link IFrame} when the receipt is received. 1672 | * 1673 | * Example: 1674 | * ```javascript 1675 | * const receiptId = "unique-receipt-id"; 1676 | * 1677 | * client.watchForReceipt(receiptId, (frame) => { 1678 | * console.log("Operation acknowledged by the broker:", frame); 1679 | * }); 1680 | * 1681 | * // Attach the receipt header to an operation 1682 | * client.publish({ destination: "/queue/test", headers: { receipt: receiptId }, body: "Hello" }); 1683 | * ``` 1684 | * 1685 | * @param receiptId Unique identifier for the receipt. 1686 | * @param callback Callback function invoked on receiving the RECEIPT frame. 1687 | */ 1688 | watchForReceipt(receiptId, callback) { 1689 | this._checkConnection(); 1690 | // @ts-ignore - we already checked that there is a _stompHandler, and it is connected 1691 | this._stompHandler.watchForReceipt(receiptId, callback); 1692 | } 1693 | /** 1694 | * Subscribes to a destination on the STOMP broker. 1695 | * 1696 | * The callback is triggered for each message received from the subscribed destination. The message 1697 | * is passed as an {@link IMessage} instance. 1698 | * 1699 | * **Subscription ID**: 1700 | * - If no `id` is provided in `headers`, the library generates a unique subscription ID automatically. 1701 | * - Provide an explicit `id` in `headers` if you wish to manage the subscription ID manually. 1702 | * 1703 | * Example: 1704 | * ```javascript 1705 | * const callback = (message) => { 1706 | * console.log("Received message:", message.body); 1707 | * }; 1708 | * 1709 | * // Auto-generated subscription ID 1710 | * const subscription = client.subscribe("/queue/test", callback); 1711 | * 1712 | * // Explicit subscription ID 1713 | * const mySubId = "my-subscription-id"; 1714 | * const subscription = client.subscribe("/queue/test", callback, { id: mySubId }); 1715 | * ``` 1716 | * 1717 | * @param destination Destination to subscribe to. 1718 | * @param callback Function invoked for each received message. 1719 | * @param headers Optional headers for subscription, such as `id`. 1720 | * @returns A {@link StompSubscription} which can be used to manage the subscription. 1721 | */ 1722 | subscribe(destination, callback, headers = {}) { 1723 | this._checkConnection(); 1724 | // @ts-ignore - we already checked that there is a _stompHandler, and it is connected 1725 | return this._stompHandler.subscribe(destination, callback, headers); 1726 | } 1727 | /** 1728 | * Unsubscribes from a subscription on the STOMP broker. 1729 | * 1730 | * Prefer using the `unsubscribe` method directly on the {@link StompSubscription} returned from `subscribe` for cleaner management: 1731 | * ```javascript 1732 | * const subscription = client.subscribe("/queue/test", callback); 1733 | * // Unsubscribe using the subscription object 1734 | * subscription.unsubscribe(); 1735 | * ``` 1736 | * 1737 | * This method can also be used directly with the subscription ID. 1738 | * 1739 | * Example: 1740 | * ```javascript 1741 | * client.unsubscribe("my-subscription-id"); 1742 | * ``` 1743 | * 1744 | * @param id Subscription ID to unsubscribe. 1745 | * @param headers Optional headers to pass for the UNSUBSCRIBE frame. 1746 | */ 1747 | unsubscribe(id, headers = {}) { 1748 | this._checkConnection(); 1749 | // @ts-ignore - we already checked that there is a _stompHandler, and it is connected 1750 | this._stompHandler.unsubscribe(id, headers); 1751 | } 1752 | /** 1753 | * Starts a new transaction. The returned {@link ITransaction} object provides 1754 | * methods for [commit]{@link ITransaction#commit} and [abort]{@link ITransaction#abort}. 1755 | * 1756 | * If `transactionId` is not provided, the library generates a unique ID internally. 1757 | * 1758 | * Example: 1759 | * ```javascript 1760 | * const tx = client.begin(); // Auto-generated ID 1761 | * 1762 | * // Or explicitly specify a transaction ID 1763 | * const tx = client.begin("my-transaction-id"); 1764 | * ``` 1765 | * 1766 | * @param transactionId Optional transaction ID. 1767 | * @returns An instance of {@link ITransaction}. 1768 | */ 1769 | begin(transactionId) { 1770 | this._checkConnection(); 1771 | // @ts-ignore - we already checked that there is a _stompHandler, and it is connected 1772 | return this._stompHandler.begin(transactionId); 1773 | } 1774 | /** 1775 | * Commits a transaction. 1776 | * 1777 | * It is strongly recommended to call [commit]{@link ITransaction#commit} on 1778 | * the transaction object returned by [client#begin]{@link Client#begin}. 1779 | * 1780 | * Example: 1781 | * ```javascript 1782 | * const tx = client.begin(); 1783 | * // Perform operations under this transaction 1784 | * tx.commit(); 1785 | * ``` 1786 | * 1787 | * @param transactionId The ID of the transaction to commit. 1788 | */ 1789 | commit(transactionId) { 1790 | this._checkConnection(); 1791 | // @ts-ignore - we already checked that there is a _stompHandler, and it is connected 1792 | this._stompHandler.commit(transactionId); 1793 | } 1794 | /** 1795 | * Aborts a transaction. 1796 | * 1797 | * It is strongly recommended to call [abort]{@link ITransaction#abort} directly 1798 | * on the transaction object returned by [client#begin]{@link Client#begin}. 1799 | * 1800 | * Example: 1801 | * ```javascript 1802 | * const tx = client.begin(); 1803 | * // Perform operations under this transaction 1804 | * tx.abort(); // Abort the transaction 1805 | * ``` 1806 | * 1807 | * @param transactionId The ID of the transaction to abort. 1808 | */ 1809 | abort(transactionId) { 1810 | this._checkConnection(); 1811 | // @ts-ignore - we already checked that there is a _stompHandler, and it is connected 1812 | this._stompHandler.abort(transactionId); 1813 | } 1814 | /** 1815 | * Acknowledges receipt of a message. Typically, this should be done by calling 1816 | * [ack]{@link IMessage#ack} directly on the {@link IMessage} instance passed 1817 | * to the subscription callback. 1818 | * 1819 | * Example: 1820 | * ```javascript 1821 | * const callback = (message) => { 1822 | * // Process the message 1823 | * message.ack(); // Acknowledge the message 1824 | * }; 1825 | * 1826 | * client.subscribe("/queue/example", callback, { ack: "client" }); 1827 | * ``` 1828 | * 1829 | * @param messageId The ID of the message to acknowledge. 1830 | * @param subscriptionId The ID of the subscription. 1831 | * @param headers Optional headers for the acknowledgment frame. 1832 | */ 1833 | ack(messageId, subscriptionId, headers = {}) { 1834 | this._checkConnection(); 1835 | // @ts-ignore - we already checked that there is a _stompHandler, and it is connected 1836 | this._stompHandler.ack(messageId, subscriptionId, headers); 1837 | } 1838 | /** 1839 | * Rejects a message (negative acknowledgment). Like acknowledgments, this should 1840 | * typically be done by calling [nack]{@link IMessage#nack} directly on the {@link IMessage} 1841 | * instance passed to the subscription callback. 1842 | * 1843 | * Example: 1844 | * ```javascript 1845 | * const callback = (message) => { 1846 | * // Process the message 1847 | * if (isError(message)) { 1848 | * message.nack(); // Reject the message 1849 | * } 1850 | * }; 1851 | * 1852 | * client.subscribe("/queue/example", callback, { ack: "client" }); 1853 | * ``` 1854 | * 1855 | * @param messageId The ID of the message to negatively acknowledge. 1856 | * @param subscriptionId The ID of the subscription. 1857 | * @param headers Optional headers for the NACK frame. 1858 | */ 1859 | nack(messageId, subscriptionId, headers = {}) { 1860 | this._checkConnection(); 1861 | // @ts-ignore - we already checked that there is a _stompHandler, and it is connected 1862 | this._stompHandler.nack(messageId, subscriptionId, headers); 1863 | } 1864 | } 1865 | 1866 | /** 1867 | * Configuration options for STOMP Client, each key corresponds to 1868 | * field by the same name in {@link Client}. This can be passed to 1869 | * the constructor of {@link Client} or to [Client#configure]{@link Client#configure}. 1870 | * 1871 | * Part of `@stomp/stompjs`. 1872 | */ 1873 | class StompConfig { 1874 | } 1875 | 1876 | /** 1877 | * STOMP headers. Many function calls will accept headers as parameters. 1878 | * The headers sent by Broker will be available as [IFrame#headers]{@link IFrame#headers}. 1879 | * 1880 | * `key` and `value` must be valid strings. 1881 | * In addition, `key` must not contain `CR`, `LF`, or `:`. 1882 | * 1883 | * Part of `@stomp/stompjs`. 1884 | */ 1885 | class StompHeaders { 1886 | } 1887 | 1888 | /** 1889 | * Part of `@stomp/stompjs`. 1890 | * 1891 | * @internal 1892 | */ 1893 | class HeartbeatInfo { 1894 | constructor(client) { 1895 | this.client = client; 1896 | } 1897 | get outgoing() { 1898 | return this.client.heartbeatOutgoing; 1899 | } 1900 | set outgoing(value) { 1901 | this.client.heartbeatOutgoing = value; 1902 | } 1903 | get incoming() { 1904 | return this.client.heartbeatIncoming; 1905 | } 1906 | set incoming(value) { 1907 | this.client.heartbeatIncoming = value; 1908 | } 1909 | } 1910 | 1911 | /** 1912 | * Available for backward compatibility, please shift to using {@link Client}. 1913 | * 1914 | * **Deprecated** 1915 | * 1916 | * Part of `@stomp/stompjs`. 1917 | * 1918 | * To upgrade, please follow the [Upgrade Guide](https://stomp-js.github.io/guide/stompjs/upgrading-stompjs.html) 1919 | */ 1920 | class CompatClient extends Client { 1921 | /** 1922 | * Available for backward compatibility, please shift to using {@link Client} 1923 | * and [Client#webSocketFactory]{@link Client#webSocketFactory}. 1924 | * 1925 | * **Deprecated** 1926 | * 1927 | * @internal 1928 | */ 1929 | constructor(webSocketFactory) { 1930 | super(); 1931 | /** 1932 | * It is no op now. No longer needed. Large packets work out of the box. 1933 | */ 1934 | this.maxWebSocketFrameSize = 16 * 1024; 1935 | this._heartbeatInfo = new HeartbeatInfo(this); 1936 | this.reconnect_delay = 0; 1937 | this.webSocketFactory = webSocketFactory; 1938 | // Default from previous version 1939 | this.debug = (...message) => { 1940 | console.log(...message); 1941 | }; 1942 | } 1943 | _parseConnect(...args) { 1944 | let closeEventCallback; 1945 | let connectCallback; 1946 | let errorCallback; 1947 | let headers = {}; 1948 | if (args.length < 2) { 1949 | throw new Error('Connect requires at least 2 arguments'); 1950 | } 1951 | if (typeof args[1] === 'function') { 1952 | [headers, connectCallback, errorCallback, closeEventCallback] = args; 1953 | } 1954 | else { 1955 | switch (args.length) { 1956 | case 6: 1957 | [ 1958 | headers.login, 1959 | headers.passcode, 1960 | connectCallback, 1961 | errorCallback, 1962 | closeEventCallback, 1963 | headers.host, 1964 | ] = args; 1965 | break; 1966 | default: 1967 | [ 1968 | headers.login, 1969 | headers.passcode, 1970 | connectCallback, 1971 | errorCallback, 1972 | closeEventCallback, 1973 | ] = args; 1974 | } 1975 | } 1976 | return [headers, connectCallback, errorCallback, closeEventCallback]; 1977 | } 1978 | /** 1979 | * Available for backward compatibility, please shift to using [Client#activate]{@link Client#activate}. 1980 | * 1981 | * **Deprecated** 1982 | * 1983 | * The `connect` method accepts different number of arguments and types. See the Overloads list. Use the 1984 | * version with headers to pass your broker specific options. 1985 | * 1986 | * overloads: 1987 | * - connect(headers, connectCallback) 1988 | * - connect(headers, connectCallback, errorCallback) 1989 | * - connect(login, passcode, connectCallback) 1990 | * - connect(login, passcode, connectCallback, errorCallback) 1991 | * - connect(login, passcode, connectCallback, errorCallback, closeEventCallback) 1992 | * - connect(login, passcode, connectCallback, errorCallback, closeEventCallback, host) 1993 | * 1994 | * params: 1995 | * - headers, see [Client#connectHeaders]{@link Client#connectHeaders} 1996 | * - connectCallback, see [Client#onConnect]{@link Client#onConnect} 1997 | * - errorCallback, see [Client#onStompError]{@link Client#onStompError} 1998 | * - closeEventCallback, see [Client#onWebSocketClose]{@link Client#onWebSocketClose} 1999 | * - login [String], see [Client#connectHeaders](../classes/Client.html#connectHeaders) 2000 | * - passcode [String], [Client#connectHeaders](../classes/Client.html#connectHeaders) 2001 | * - host [String], see [Client#connectHeaders](../classes/Client.html#connectHeaders) 2002 | * 2003 | * To upgrade, please follow the [Upgrade Guide](../additional-documentation/upgrading.html) 2004 | */ 2005 | connect(...args) { 2006 | const out = this._parseConnect(...args); 2007 | if (out[0]) { 2008 | this.connectHeaders = out[0]; 2009 | } 2010 | if (out[1]) { 2011 | this.onConnect = out[1]; 2012 | } 2013 | if (out[2]) { 2014 | this.onStompError = out[2]; 2015 | } 2016 | if (out[3]) { 2017 | this.onWebSocketClose = out[3]; 2018 | } 2019 | super.activate(); 2020 | } 2021 | /** 2022 | * Available for backward compatibility, please shift to using [Client#deactivate]{@link Client#deactivate}. 2023 | * 2024 | * **Deprecated** 2025 | * 2026 | * See: 2027 | * [Client#onDisconnect]{@link Client#onDisconnect}, and 2028 | * [Client#disconnectHeaders]{@link Client#disconnectHeaders} 2029 | * 2030 | * To upgrade, please follow the [Upgrade Guide](../additional-documentation/upgrading.html) 2031 | */ 2032 | disconnect(disconnectCallback, headers = {}) { 2033 | if (disconnectCallback) { 2034 | this.onDisconnect = disconnectCallback; 2035 | } 2036 | this.disconnectHeaders = headers; 2037 | super.deactivate(); 2038 | } 2039 | /** 2040 | * Available for backward compatibility, use [Client#publish]{@link Client#publish}. 2041 | * 2042 | * Send a message to a named destination. Refer to your STOMP broker documentation for types 2043 | * and naming of destinations. The headers will, typically, be available to the subscriber. 2044 | * However, there may be special purpose headers corresponding to your STOMP broker. 2045 | * 2046 | * **Deprecated**, use [Client#publish]{@link Client#publish} 2047 | * 2048 | * Note: Body must be String. You will need to covert the payload to string in case it is not string (e.g. JSON) 2049 | * 2050 | * ```javascript 2051 | * client.send("/queue/test", {priority: 9}, "Hello, STOMP"); 2052 | * 2053 | * // If you want to send a message with a body, you must also pass the headers argument. 2054 | * client.send("/queue/test", {}, "Hello, STOMP"); 2055 | * ``` 2056 | * 2057 | * To upgrade, please follow the [Upgrade Guide](../additional-documentation/upgrading.html) 2058 | */ 2059 | send(destination, headers = {}, body = '') { 2060 | headers = Object.assign({}, headers); 2061 | const skipContentLengthHeader = headers['content-length'] === false; 2062 | if (skipContentLengthHeader) { 2063 | delete headers['content-length']; 2064 | } 2065 | this.publish({ 2066 | destination, 2067 | headers: headers, 2068 | body, 2069 | skipContentLengthHeader, 2070 | }); 2071 | } 2072 | /** 2073 | * Available for backward compatibility, renamed to [Client#reconnectDelay]{@link Client#reconnectDelay}. 2074 | * 2075 | * **Deprecated** 2076 | */ 2077 | set reconnect_delay(value) { 2078 | this.reconnectDelay = value; 2079 | } 2080 | /** 2081 | * Available for backward compatibility, renamed to [Client#webSocket]{@link Client#webSocket}. 2082 | * 2083 | * **Deprecated** 2084 | */ 2085 | get ws() { 2086 | return this.webSocket; 2087 | } 2088 | /** 2089 | * Available for backward compatibility, renamed to [Client#connectedVersion]{@link Client#connectedVersion}. 2090 | * 2091 | * **Deprecated** 2092 | */ 2093 | get version() { 2094 | return this.connectedVersion; 2095 | } 2096 | /** 2097 | * Available for backward compatibility, renamed to [Client#onUnhandledMessage]{@link Client#onUnhandledMessage}. 2098 | * 2099 | * **Deprecated** 2100 | */ 2101 | get onreceive() { 2102 | return this.onUnhandledMessage; 2103 | } 2104 | /** 2105 | * Available for backward compatibility, renamed to [Client#onUnhandledMessage]{@link Client#onUnhandledMessage}. 2106 | * 2107 | * **Deprecated** 2108 | */ 2109 | set onreceive(value) { 2110 | this.onUnhandledMessage = value; 2111 | } 2112 | /** 2113 | * Available for backward compatibility, renamed to [Client#onUnhandledReceipt]{@link Client#onUnhandledReceipt}. 2114 | * Prefer using [Client#watchForReceipt]{@link Client#watchForReceipt}. 2115 | * 2116 | * **Deprecated** 2117 | */ 2118 | get onreceipt() { 2119 | return this.onUnhandledReceipt; 2120 | } 2121 | /** 2122 | * Available for backward compatibility, renamed to [Client#onUnhandledReceipt]{@link Client#onUnhandledReceipt}. 2123 | * 2124 | * **Deprecated** 2125 | */ 2126 | set onreceipt(value) { 2127 | this.onUnhandledReceipt = value; 2128 | } 2129 | /** 2130 | * Available for backward compatibility, renamed to [Client#heartbeatIncoming]{@link Client#heartbeatIncoming} 2131 | * [Client#heartbeatOutgoing]{@link Client#heartbeatOutgoing}. 2132 | * 2133 | * **Deprecated** 2134 | */ 2135 | get heartbeat() { 2136 | return this._heartbeatInfo; 2137 | } 2138 | /** 2139 | * Available for backward compatibility, renamed to [Client#heartbeatIncoming]{@link Client#heartbeatIncoming} 2140 | * [Client#heartbeatOutgoing]{@link Client#heartbeatOutgoing}. 2141 | * 2142 | * **Deprecated** 2143 | */ 2144 | set heartbeat(value) { 2145 | this.heartbeatIncoming = value.incoming; 2146 | this.heartbeatOutgoing = value.outgoing; 2147 | } 2148 | } 2149 | 2150 | /** 2151 | * STOMP Class, acts like a factory to create {@link Client}. 2152 | * 2153 | * Part of `@stomp/stompjs`. 2154 | * 2155 | * **Deprecated** 2156 | * 2157 | * It will be removed in next major version. Please switch to {@link Client}. 2158 | */ 2159 | class Stomp { 2160 | /** 2161 | * This method creates a WebSocket client that is connected to 2162 | * the STOMP server located at the url. 2163 | * 2164 | * ```javascript 2165 | * var url = "ws://localhost:61614/stomp"; 2166 | * var client = Stomp.client(url); 2167 | * ``` 2168 | * 2169 | * **Deprecated** 2170 | * 2171 | * It will be removed in next major version. Please switch to {@link Client} 2172 | * using [Client#brokerURL]{@link Client#brokerURL}. 2173 | */ 2174 | static client(url, protocols) { 2175 | // This is a hack to allow another implementation than the standard 2176 | // HTML5 WebSocket class. 2177 | // 2178 | // It is possible to use another class by calling 2179 | // 2180 | // Stomp.WebSocketClass = MozWebSocket 2181 | // 2182 | // *prior* to call `Stomp.client()`. 2183 | // 2184 | // This hack is deprecated and `Stomp.over()` method should be used 2185 | // instead. 2186 | // See remarks on the function Stomp.over 2187 | if (protocols == null) { 2188 | protocols = Versions.default.protocolVersions(); 2189 | } 2190 | const wsFn = () => { 2191 | const klass = Stomp.WebSocketClass || WebSocket; 2192 | return new klass(url, protocols); 2193 | }; 2194 | return new CompatClient(wsFn); 2195 | } 2196 | /** 2197 | * This method is an alternative to [Stomp#client]{@link Stomp#client} to let the user 2198 | * specify the WebSocket to use (either a standard HTML5 WebSocket or 2199 | * a similar object). 2200 | * 2201 | * In order to support reconnection, the function Client._connect should be callable more than once. 2202 | * While reconnecting 2203 | * a new instance of underlying transport (TCP Socket, WebSocket or SockJS) will be needed. So, this function 2204 | * alternatively allows passing a function that should return a new instance of the underlying socket. 2205 | * 2206 | * ```javascript 2207 | * var client = Stomp.over(function(){ 2208 | * return new WebSocket('ws://localhost:15674/ws') 2209 | * }); 2210 | * ``` 2211 | * 2212 | * **Deprecated** 2213 | * 2214 | * It will be removed in next major version. Please switch to {@link Client} 2215 | * using [Client#webSocketFactory]{@link Client#webSocketFactory}. 2216 | */ 2217 | static over(ws) { 2218 | let wsFn; 2219 | if (typeof ws === 'function') { 2220 | wsFn = ws; 2221 | } 2222 | else { 2223 | console.warn('Stomp.over did not receive a factory, auto reconnect will not work. ' + 2224 | 'Please see https://stomp-js.github.io/api-docs/latest/classes/Stomp.html#over'); 2225 | wsFn = () => ws; 2226 | } 2227 | return new CompatClient(wsFn); 2228 | } 2229 | } 2230 | /** 2231 | * In case you need to use a non standard class for WebSocket. 2232 | * 2233 | * For example when using within NodeJS environment: 2234 | * 2235 | * ```javascript 2236 | * StompJs = require('../../esm5/'); 2237 | * Stomp = StompJs.Stomp; 2238 | * Stomp.WebSocketClass = require('websocket').w3cwebsocket; 2239 | * ``` 2240 | * 2241 | * **Deprecated** 2242 | * 2243 | * 2244 | * It will be removed in next major version. Please switch to {@link Client} 2245 | * using [Client#webSocketFactory]{@link Client#webSocketFactory}. 2246 | */ 2247 | // tslint:disable-next-line:variable-name 2248 | Stomp.WebSocketClass = null; 2249 | 2250 | exports.Client = Client; 2251 | exports.CompatClient = CompatClient; 2252 | exports.FrameImpl = FrameImpl; 2253 | exports.Parser = Parser; 2254 | exports.Stomp = Stomp; 2255 | exports.StompConfig = StompConfig; 2256 | exports.StompHeaders = StompHeaders; 2257 | exports.Versions = Versions; 2258 | 2259 | })); 2260 | //# sourceMappingURL=stomp.umd.js.map 2261 | --------------------------------------------------------------------------------