├── 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 | | Plugin version |
13 | Grails version |
14 |
15 |
16 | | io.github.zyro23:grails-spring-websocket:2.7.x |
17 | 7.0.0+ |
18 |
19 |
20 | | io.github.zyro23:grails-spring-websocket:2.6.x |
21 | 6.0.0+ |
22 |
23 |
24 | | org.grails.plugins:grails-spring-websocket:2.5.x |
25 | 4.0.0+ |
26 |
27 |
28 | | org.grails.plugins:grails-spring-websocket:2.4.x |
29 | 3.2.7+ |
30 |
31 |
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 |
--------------------------------------------------------------------------------