I have a angular 5 app with the rxjs WebsocketSubject sending jsonrpc messages.
This is my sendRequest function
sendRequest(request: Request): Promise<Response> {
console.log(request);
this.socket.next(JSON.stringify(request));
return this.onResponse().filter((response: Response) => {
return response.id === request.id;
}).first().toPromise().then((response) => {
console.log(response);
if (response.error) {
console.log('error');
throw new RpcError(response.error);
}
return response;
});
}
I am using the first() operator to complete this filter subscription. But onResponse() comes directly from my WebsocketSubject and this will then be completed.
Are there any methods for decoupling the original subject?
Or should I create a new Observale.create(...)?
What happens with the written .filter function. Does it last anywhere or do I have to remove it anywhere preventing ever lasting filter calls?
Edit 1
Also using this does not help.
sendRequest(request: Request): Promise<Response> {
console.log(request);
this.socket.next(JSON.stringify(request));
return new Promise<Response>((resolve, reject) => {
const responseSubscription = this.onResponse().filter((response: Response) => {
console.log('filter');
return response.id === request.id;
}).subscribe((response: Response) => {
// responseSubscription.unsubscribe();
resolve(response);
});
});
}
If I execute the unsubscribe the whole websocketSubject is closed. Not doing so logs 'filter' on time more per request !!
Edit 2
Here is the whole websocketService i have written
import {Injectable} from "#angular/core";
import {WebSocketSubject, WebSocketSubjectConfig} from "rxjs/observable/dom/WebSocketSubject";
import {MessageFactory, Notification, Request, Response, RpcError} from "../misc/jsonrpc";
import {ReplaySubject} from "rxjs/ReplaySubject";
import {Observable} from "rxjs/Observable";
import 'rxjs/add/operator/toPromise';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/first';
import 'rxjs/add/observable/from';
export enum ConnectionState {
CONNECTED = "Connected",
CONNECTING = "Connecting",
CLOSING = "Closing",
DISCONNECTED = "Disconnected"
}
#Injectable()
export class WebsocketService {
private connectionState = new ReplaySubject<ConnectionState>(1);
private socket: WebSocketSubject<ArrayBuffer | Object>;
private config: WebSocketSubjectConfig;
constructor() {
console.log('ctor');
const protocol = location.protocol === 'https' ? 'wss' : 'ws';
const host = location.hostname;
const port = 3000; // location.port;
this.config = {
binaryType: "arraybuffer",
url: `${protocol}://${host}:${port}`,
openObserver: {
next: () => this.connectionState.next(ConnectionState.CONNECTED)
},
closingObserver: {
next: () => this.connectionState.next(ConnectionState.CLOSING)
},
closeObserver: {
next: () => this.connectionState.next(ConnectionState.DISCONNECTED)
},
resultSelector: (e: MessageEvent) => {
try {
if (e.data instanceof ArrayBuffer) {
return e.data;
} else {
return JSON.parse(e.data);
}
} catch (e) {
console.error(e);
return null;
}
}
};
this.connectionState.next(ConnectionState.CONNECTING);
this.socket = new WebSocketSubject(this.config);
this.connectionState.subscribe((state) => {
console.log(`WS state ${state}`);
});
}
onBinaryData(): Observable<ArrayBuffer> {
return this.socket.filter((message: any) => {
return message instanceof ArrayBuffer;
});
}
onMessageData(): Observable<Object> {
return this.socket.filter((message: any) => {
return !(message instanceof ArrayBuffer);
});
}
onResponse(): Observable<Response> {
return this.onMessageData().filter((message) => {
return MessageFactory.from(message).isResponse();
}).map((message): Response => {
return MessageFactory.from(message).toResponse();
});
}
sendRequest(request: Request): Promise<Response> {
console.log(request);
this.socket.next(JSON.stringify(request));
return new Promise<Response>((resolve, reject) => {
const responseSubscription = this.onResponse().filter((response: Response) => {
console.log('filter');
return response.id === request.id;
}).subscribe((response: Response) => {
responseSubscription.unsubscribe();
resolve(response);
});
});
}
sendNotification(notification: Notification): void {
this.socket.next(JSON.stringify(notification));
}
}
And the result in my log
Using Angular 5.0.2
websocket.service.ts:27 ctor
websocket.service.ts:69 WS state Connecting
core.js:3565 Angular is running in the development mode. Call enableProdMode() to enable the production mode.
websocket.service.ts:96 Request {jsonrpc: "2.0", id: "b042005c-5fbf-5ffc-fbd1-df68fae5882e", method: "appointment_list_get", params: undefined}
websocket.service.ts:69 WS state Connected
websocket.service.ts:103 filter
websocket.service.ts:69 WS state Disconnected
I need to find a way decoupling my filter from the original stream somehow.
This is working.
The key was to decouple the message handling from the underlaying websocketSubject.
import {Injectable} from "#angular/core";
import {WebSocketSubject, WebSocketSubjectConfig} from "rxjs/observable/dom/WebSocketSubject";
import {MessageFactory, Notification, Request, Response, RpcError} from "../misc/jsonrpc";
import {ReplaySubject} from "rxjs/ReplaySubject";
import {Observable} from "rxjs/Observable";
import 'rxjs/add/operator/toPromise';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/first';
import 'rxjs/add/observable/from';
import {Subject} from "rxjs/Subject";
export enum ConnectionState {
CONNECTED = "Connected",
CONNECTING = "Connecting",
CLOSING = "Closing",
DISCONNECTED = "Disconnected"
}
#Injectable()
export class WebsocketService {
private connectionState = new ReplaySubject<ConnectionState>(1);
private socket: WebSocketSubject<ArrayBuffer | Object>;
private config: WebSocketSubjectConfig;
private messageObserver = new Subject<MessageFactory>();
private binaryObserver = new Subject<ArrayBuffer>();
constructor() {
const protocol = location.protocol === 'https' ? 'wss' : 'ws';
const host = location.hostname;
const port = 3000; // location.port;
this.config = {
binaryType: "arraybuffer",
url: `${protocol}://${host}:${port}`,
openObserver: {
next: () => this.connectionState.next(ConnectionState.CONNECTED)
},
closingObserver: {
next: () => this.connectionState.next(ConnectionState.CLOSING)
},
closeObserver: {
next: () => this.connectionState.next(ConnectionState.DISCONNECTED)
},
resultSelector: (e: MessageEvent) => {
try {
if (e.data instanceof ArrayBuffer) {
return e.data;
} else {
return JSON.parse(e.data);
}
} catch (e) {
console.error(e);
return null;
}
}
};
this.connectionState.next(ConnectionState.CONNECTING);
this.socket = new WebSocketSubject(this.config);
this.socket.filter((message: any) => {
return message instanceof ArrayBuffer;
}).subscribe((message: ArrayBuffer) => {
this.binaryObserver.next(message);
});
this.socket.filter((message: any) => {
return !(message instanceof ArrayBuffer);
}).subscribe((message: ArrayBuffer) => {
this.messageObserver.next(MessageFactory.from(message));
});
this.connectionState.subscribe((state) => {
console.log(`WS state ${state}`);
});
}
onResponse(): Observable<Response> {
return this.messageObserver.filter((message: MessageFactory) => {
return message.isResponse();
}).map((message: MessageFactory): Response => {
return message.toResponse();
});
}
sendRequest(request: Request): Promise<Response> {
console.log(request);
this.socket.next(JSON.stringify(request));
return this.onResponse().filter((response: Response) => {
return request.id === response.id;
}).first().toPromise().then((response) => {
console.log(response);
if (response.error) {
console.log('error');
throw new RpcError(response.error);
}
return response;
});
}
sendNotification(notification: Notification): void {
this.socket.next(JSON.stringify(notification));
}
}
Related
How to store socket object of socket.io in slice of redux toolkit?
I would like to do something like:
const initialState = {
socket: null
}
const socketSlice = createSlice({
name: socket,
initialState,
reducers:{
createSocket(state, action){
state.socket = io("localhost:5000")
},
removeSocket(state, action){
state.socket = null
}
// ...
}
})
However, this gives the following error:
serializableStateInvariantMiddleware.ts:222 A non-serializable value was detected in the state
Help me...
I had the exact same issue and solved it using the following steps:
Create a socket client in which I have a single instance of socket which I use to perform all socket related functions:
import { io } from 'socket.io-client';
class SocketClient {
socket;
connect() {
this.socket = io.connect(process.env.SOCKET_HOST, { transports: ['websocket'] });
return new Promise((resolve, reject) => {
this.socket.on('connect', () => resolve());
this.socket.on('connect_error', (error) => reject(error));
});
}
disconnect() {
return new Promise((resolve) => {
this.socket.disconnect(() => {
this.socket = null;
resolve();
});
});
}
emit(event, data) {
return new Promise((resolve, reject) => {
if (!this.socket) return reject('No socket connection.');
return this.socket.emit(event, data, (response) => {
// Response is the optional callback that you can use with socket.io in every request. See 1 above.
if (response.error) {
console.error(response.error);
return reject(response.error);
}
return resolve();
});
});
}
on(event, fun) {
// No promise is needed here, but we're expecting one in the middleware.
return new Promise((resolve, reject) => {
if (!this.socket) return reject('No socket connection.');
this.socket.on(event, fun);
resolve();
});
}
}
export default SocketClient;
Import it into my index.jsx file and initialize it:
import SocketClient from './js/services/SocketClient';
export const socketClient = new SocketClient();
Here's the whole code of my index.jsx file:
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
//import meta image
import '#/public/assets/images/metaImage.jpg';
//styles
import '#/scss/global.scss';
//store
import store from '#/js/store/store';
//app
import App from './App';
//socket client
import SocketClient from './js/services/SocketClient';
export const socketClient = new SocketClient();
const container = document.getElementById('root'),
root = createRoot(container);
root.render(
<Provider store={store}>
<App />
</Provider>
);
I used createAsyncThunk function from #reduxjs/toolkit, because it automatically generates types like pending, fulfilled and rejected.
Here's how I structure my reducer slice to connect and disconnect from web socket in redux:
import { createAsyncThunk, createSlice } from '#reduxjs/toolkit';
import { socketClient } from '../../../../index';
const initialState = {
connectionStatus: '',
};
export const connectToSocket = createAsyncThunk('connectToSocket', async function () {
return await socketClient.connect();
});
export const disconnectFromSocket = createAsyncThunk('disconnectFromSocket', async function () {
return await socketClient.disconnect();
});
const appSlice = createSlice({
name: 'app',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(connectToSocket.pending, (state) => {
state.connectionStatus = 'connecting';
});
builder.addCase(connectToSocket.fulfilled, (state) => {
state.connectionStatus = 'connected';
});
builder.addCase(connectToSocket.rejected, (state) => {
state.connectionStatus = 'connection failed';
});
builder.addCase(disconnectFromSocket.pending, (state) => {
state.connectionStatus = 'disconnecting';
});
builder.addCase(disconnectFromSocket.fulfilled, (state) => {
state.connectionStatus = 'disconnected';
});
builder.addCase(disconnectFromSocket.rejected, (state) => {
state.connectionStatus = 'disconnection failed';
});
},
});
export default appSlice.reducer;
Here how I connect and disconnect in App.jsx file:
useEffect(() => {
dispatch(connectToSocket());
return () => {
if (connectionStatus === 'connected') {
dispatch(disconnectFromSocket());
}
};
//eslint-disable-next-line
}, [dispatch]);
You can do the following if you want to emit to web socket:
import { createAsyncThunk, createSlice } from '#reduxjs/toolkit';
import { socketClient } from '../../../../index';
const initialState = {
messageStatus: '', //ideally it should come from the BE
messages: [],
typingUsername: '',
};
export const sendMessage = createAsyncThunk('sendMessage', async function ({ message, username }) {
return await socketClient.emit('chat', { message, handle: username });
});
const chatSlice = createSlice({
name: 'chat',
initialState,
reducers: {},
extraReducers: (builder) => {
builder.addCase(sendMessage.pending, (state) => {
state.messageStatus = 'Sending';
});
builder.addCase(sendMessage.fulfilled, (state) => {
state.messageStatus = 'Sent successfully';
});
builder.addCase(sendMessage.rejected, (state) => {
state.messageStatus = 'Send failed';
});
},
});
export default chatSlice.reducer;
You can do the following if you want to listen to an event from web socket:
import { createAsyncThunk, createSlice } from '#reduxjs/toolkit';
import { socketClient } from '../../../../index';
const initialState = {
messageStatus: '', //ideally it should come from the BE
messages: [],
typingUsername: '',
};
export const fetchMessages = createAsyncThunk(
'fetchMessages',
async function (_, { getState, dispatch }) {
console.log('state ', getState());
return await socketClient.on('chat', (receivedMessages) =>
dispatch({ type: 'chat/saveReceivedMessages', payload: { messages: receivedMessages } })
);
}
);
const chatSlice = createSlice({
name: 'chat',
initialState,
reducers: {
saveReceivedMessages: (state, action) => {
state.messages.push(action.payload.messages);
state.typingUsername = '';
},
},
extraReducers: (builder) => {
builder.addCase(fetchMessages.pending, () => {
// add a state if you would like to
});
builder.addCase(fetchMessages.fulfilled, () => {
// add a state if you would like to
});
builder.addCase(fetchMessages.rejected, () => {
// add a state if you would like to
});
},
});
export default chatSlice.reducer;
background:
whenever I open WebSocket page
I had a few XHR_SEND? 404 - error but finally XHR_SEND? got success response and connected to WebSocket.
So to avoid this 404 error, I decide to use WebSocket only. so I added this
: return new SockJS(connectionUrl,, null, { transports: ['websocket']});
then now..
XHR_SEND? are gone but it doesn't connect to server at all.
+FYI: I have 2 servers ..(i think because of this previously I got XHR_send error. )
The below screenshot is repeating. but never connected
JAVA
#Configuration
#EnableWebSocketMessageBroker
public class BatchSocketConfiguration extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/batch-socket");
registry.addEndpoint("/batch-socket").withSockJS();
}
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/socketio/");
}
ANGULAR7
import { Injectable, OnDestroy, Inject, Optional } from '#angular/core';
import * as SockJS from '../../../assets/lib/sockjs.min.js';
import { BehaviorSubject, Observable } from 'rxjs';
import { filter, first, switchMap } from 'rxjs/operators';
import { StompSubscription, Stomp, Client, Frame, Message, StompConfig, Versions } from '#stomp/stompjs';
#Injectable({
providedIn: 'root'
})
export class SocketService {
private client: Client;
private state: BehaviorSubject<any>;
private baseUrl: any = "/" + window.location.href.substr(0).split('/')[3] + "/";
constructor() {
}
init() {
let connectionUrl = this.baseUrl + "batch-socket";
console.log("MY URL is " + connectionUrl);
return new Promise((resolve, reject) => {
let config = new StompConfig();
config.heartbeatOutgoing = 10000;
config.heartbeatIncoming = 10000;
config.stompVersions = new Versions(['1.0', '1.1']);
config.webSocketFactory = function () {
return new SockJS(connectionUrl, null, { transports: ['websocket']});
//PREVIOUS : return new SockJS(connectionUrl)
}
config.debug = function (str) {
console.log("#socketDebug: " + str)
}
this.client = new Client();
this.client.configure(config);
console.log(this.client);
console.log("#socketSvc: starting connection...");
const _this = this;
this.client.onConnect = function (frame) {
console.log("#socketSvc: connection established.");
console.log(frame);
_this.state = new BehaviorSubject<any>(SocketClientState.ATTEMPTING);
_this.state.next(SocketClientState.CONNECTED);
resolve(frame.headers['user-name']);
}
this.client.onWebSocketClose = function (msg){
console.log("#socketSvc: connection closed.");
console.log(msg);
}
this.client.onWebSocketError = function(msg){
console.log("#socketSvc: connection error.");
console.log(msg);
}
this.client.onDisconnect = function(msg){
console.log("#socketSvc: socket disconnected.");
console.log(msg);
//this.init();
}
this.client.onStompError = function(msg){
console.log("#socketSvc: stomp error occurred.");
console.log(msg);
}
this.client.activate();
});
}
private connect(): Observable<Client> {
return new Observable<Client>(observer => {
this.state.pipe(filter(state => state === SocketClientState.CONNECTED)).subscribe(() => {
observer.next(this.client);
});
});
}
onPlainMessageReceived(topic: string): Observable<string> {
return this.onMessageReceived(topic, SocketService.textHandler);
}
onMessageReceived(topic: string, handler = SocketService.jsonHandler): Observable<any> {
return this.connect().pipe(first(), switchMap(client => {
return new Observable<any>(observer => {
const subscription: StompSubscription = client.subscribe(topic, message => {
observer.next(handler(message));
});
return () => client.unsubscribe(subscription.id);
});
}));
}
static jsonHandler(message: Message): any {
return JSON.parse(message.body);
}
static textHandler(message: Message): string {
return message.body;
}
disconnect() {
this.connect().pipe(first()).subscribe(client => client.deactivate());
this.client.deactivate();
}
}
export enum SocketClientState {
ATTEMPTING, CONNECTED
}
I found a reason for this issue.
I realized that I have 2 war files.
hence one has my code(socket connection) , the other one doesnt have a code (socket connection).
so it throws the error.
=> resolved by removing the war file that doesn't have socket connection.
In any case, if my application got disconnected from WebSocket I am not able to reconnect it. I am attaching the sample code please suggest me the idea to how I can reconnect WebSocket and initialize my identity on web socket server again.
I have made my application with the help of this tutorial.
https://tutorialedge.net/typescript/angular/angular-websockets-tutorial/
I have written the same code in my application except for my application requirement.
The tutorial that I have been followed does not have the feature to reconnect the WebSocket in any case like internet break or by some reason our WebSocket server got restart because I am running my WebSocket server with SupervisorD and it will automatically restart if WebSocket server get to stop in any case
My application is in production and many customers are using now so I can not change all flow and recreate the code for WebSocket in this application.
I am adding all code that I am using
websocket.service.ts
import { Injectable } from '#angular/core';
import * as Rx from 'rxjs/Rx';
#Injectable()
export class WebsocketService {
connected: boolean = false;
initialized: boolean= false;
constructor() { }
private subject: Rx.Subject<MessageEvent>;
public connect(url): Rx.Subject<MessageEvent> {
if (!this.subject) {
this.subject = this.create(url);
// console.log("Successfully connected: " + url);
}
return this.subject;
}
private create(url): Rx.Subject<MessageEvent> {
let ws = new WebSocket(url);
// here i am trying to reconnect my websocket
// setInterval (function () {
// if (ws.readyState !== 1) {
// ws = new WebSocket(url);
// this.initialized = false;
// }
// console.log(this.initialized);
// if (ws.readyState == 1 && this.initialized == false) {
// ws.send('{"type":"add",
"t":"14bfa6xxx", "from_numbers":
["xxxx","xxxxx"], "platform":"xxxx"}');
// this.initialized = true;
// }
// console.log(this.initialized);
// }, 4000);
let observable = Rx.Observable.create(
(obs: Rx.Observer<MessageEvent>) => {
ws.onmessage = obs.next.bind(obs);
ws.onerror = obs.error.bind(obs);
ws.onclose = obs.complete.bind(obs);
return ws.close.bind(ws);
})
let observer = {
next: (data: Object) => {
if (ws.readyState === WebSocket.OPEN) {
if (data['type'] == 'add') {
console.log("Connection Initialized");
}
ws.send(JSON.stringify(data));
}
}
}
return Rx.Subject.create(observer, observable);
}
}
Chat.service.ts
import { Injectable } from '#angular/core';
import { Observable, Subject } from 'rxjs/Rx';
import { WebsocketService } from './websocket.service';
#Injectable()
export class ChatService {
public messages: Subject<Message>;
constructor(wsService: WebsocketService, private authService: AuthService) {
this.messages = <Subject<Message>>wsService
.connect(socket_url)
.map((response: MessageEvent): Message => {
const data = JSON.parse(response.data);
console.log(data);
return data;
});
}
}
and finality I have used this in our component to subscribe message.
constructor(private chatService: ChatService,) {
this.socketMessages();
}
socketMessages() {
this.chatService.messages.subscribe(msg => {
console.log(msg)
});
}
I am trying to configure subscriptions with Apollo 2 and NEXT.js. I can get the client to connect to the server and they are working in the GraphQL playground, so the bad configuration must be in the withData file, or the component that handles the subscription.
When inspecting the socket connection on the network panel in chrome, the subscription payload does not get added as a frame, like it does in the GraphQL playground.
withData:
import { ApolloLink, Observable } from 'apollo-link';
import { GRAPHQL_ENDPOINT, WS_PATH } from '../config/env';
import { ApolloClient } from 'apollo-client';
import { BatchHttpLink } from 'apollo-link-batch-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import { WebSocketLink } from 'apollo-link-ws';
import { createPersistedQueryLink } from 'apollo-link-persisted-queries';
import { onError } from 'apollo-link-error';
import withApollo from 'next-with-apollo';
import { withClientState } from 'apollo-link-state';
function createClient({ headers }) {
const cache = new InMemoryCache();
const request = async (operation) => {
operation.setContext({
http: {
includeExtensions: true,
includeQuery: false
},
headers
});
};
const requestLink = new ApolloLink(
(operation, forward) => new Observable((observer) => {
let handle;
Promise.resolve(operation)
.then(oper => request(oper))
.then(() => {
handle = forward(operation).subscribe({
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: observer.complete.bind(observer)
});
})
.catch(observer.error.bind(observer));
return () => {
if (handle) handle.unsubscribe();
};
})
);
return new ApolloClient({
link: ApolloLink.from([
onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
console.log({ graphQLErrors });
}
if (networkError) {
console.log('Logout user');
}
}),
requestLink,
// link,
withClientState({
defaults: {
isConnected: true
},
resolvers: {
Mutation: {
updateNetworkStatus: (_, { isConnected }, { cache }) => {
cache.writeData({ data: { isConnected } });
return null;
}
}
},
cache
}),
createPersistedQueryLink().concat(
new BatchHttpLink({
uri: GRAPHQL_ENDPOINT,
credentials: 'include'
}),
process.browser
? new WebSocketLink({
uri: WS_PATH,
options: {
reconnect: true
}
})
: null
)
]),
cache
});
}
export default withApollo(createClient);
Subscription component:
import { CONVERSATION_QUERY } from '../../constants/queries';
import { CONVERSATION_SUBSCRIPTION } from '../../constants/subscriptions';
import PropTypes from 'prop-types';
import { Query } from 'react-apollo';
const Conversation = props => (
<Query
{...props}
query={CONVERSATION_QUERY}
variables={{ input: { _id: props._id } }}
>
{(payload) => {
const more = () => payload.subscribeToMore({
document: CONVERSATION_SUBSCRIPTION,
variables: { input: { conversation: props._id } },
updateQuery: (prev, { subscriptionData }) => {
console.log({ subscriptionData });
if (!subscriptionData.data.messageSent) return prev;
const data = subscriptionData;
console.log({ data });
return Object.assign({}, prev, {});
},
onError(error) {
console.log(error);
},
onSubscriptionData: (data) => {
console.log('onSubscriptionData ', data);
}
});
return props.children({ ...payload, more });
}}
</Query>
);
Conversation.propTypes = {
children: PropTypes.func.isRequired
};
export default Conversation;
The subscription that has been tested in the GraphQL playground:
import gql from 'graphql-tag';
export const CONVERSATION_SUBSCRIPTION = gql`
subscription messageSent($input: messageSentInput) {
messageSent(input: $input) {
_id
users {
_id
profile {
firstName
lastName
jobTitle
company
picture
}
}
messages {
_id
body
createdAt
read
sender {
_id
profile {
firstName
lastName
jobTitle
company
picture
}
}
}
}
}
`;
The more function is then executed in componentDidMount:
componentDidMount() {
this.props.subscribeToMore();
}
The result in the console from the log in updateQuery is:
{"data":{"messageSent":null}}
I hadn't configured my withData file properly. You need to use split from the apollo-link package to let Apollo determine if the request should be handled with http or ws. Here is my working configuration file.
import { ApolloLink, Observable } from 'apollo-link';
import { ApolloClient } from 'apollo-client';
import { BatchHttpLink } from 'apollo-link-batch-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { WebSocketLink } from 'apollo-link-ws';
import { createPersistedQueryLink } from 'apollo-link-persisted-queries';
import { getMainDefinition } from 'apollo-utilities';
import { onError } from 'apollo-link-error';
import { split } from 'apollo-link';
import withApollo from 'next-with-apollo';
import { withClientState } from 'apollo-link-state';
import { GRAPHQL_ENDPOINT, WS_PATH } from '../config/env';
function createClient({ headers }) {
const cache = new InMemoryCache();
const request = async (operation) => {
operation.setContext({
http: {
includeExtensions: true,
includeQuery: false
},
headers
});
};
const requestLink = new ApolloLink(
(operation, forward) => new Observable((observer) => {
let handle;
Promise.resolve(operation)
.then(oper => request(oper))
.then(() => {
handle = forward(operation).subscribe({
next: observer.next.bind(observer),
error: observer.error.bind(observer),
complete: observer.complete.bind(observer)
});
})
.catch(observer.error.bind(observer));
return () => {
if (handle) handle.unsubscribe();
};
})
);
const httpLink = new BatchHttpLink({
uri: GRAPHQL_ENDPOINT
});
// Make sure the wsLink is only created on the browser. The server doesn't have a native implemention for websockets
const wsLink = process.browser
? new WebSocketLink({
uri: WS_PATH,
options: {
reconnect: true
}
})
: () => {
console.log('SSR');
};
// Let Apollo figure out if the request is over ws or http
const terminatingLink = split(
({ query }) => {
const { kind, operation } = getMainDefinition(query);
return (
kind === 'OperationDefinition'
&& operation === 'subscription'
&& process.browser
);
},
wsLink,
httpLink
);
return new ApolloClient({
link: ApolloLink.from([
onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
console.error({ graphQLErrors });
}
if (networkError) {
console.error({ networkError});
}
}),
requestLink,
// link,
withClientState({
defaults: {
isConnected: true
},
resolvers: {
Mutation: {
updateNetworkStatus: (_, { isConnected }, { cache }) => {
cache.writeData({ data: { isConnected } });
return null;
}
}
},
cache
}),
// Push the links into the Apollo client
createPersistedQueryLink().concat(
// New config
terminatingLink
// Old config
// new BatchHttpLink({
// uri: GRAPHQL_ENDPOINT,
// credentials: 'include'
// })
)
]),
cache
});
}
export default withApollo(createClient);
I'm trying to get a sockjs + stomp connection to my spring boot websockets. This is my configuration:
#Configuration
#EnableWebSocketMessageBroker
public class WebsocketConfiguration extends AbstractWebSocketMessageBrokerConfigurer {
private final String MESSAGE_BROKER_PREFIX = "/topic";
private final String WEBSOCKET_PREFIX = "/sockjs-node";
private final String REQUEST_PREFIX = "/";
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint(WEBSOCKET_PREFIX)
.withSockJS();
}
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker(MESSAGE_BROKER_PREFIX);
config.setApplicationDestinationPrefixes(REQUEST_PREFIX);
}
}
And and my endpoint definition:
#Controller
public class Foo {
#SubscribeMapping("/{pipelineId}/{topic}")
private void subscribe(
HttpSession session,
#PathVariable String pipelineId,
#PathVariable String topic
) {
System.out.println(session.getId());
}
#EventListener
public void onApplicationEvent(SessionConnectEvent event) {
System.out.println(event.getSource());
}
#EventListener
public void onApplicationEvent(SessionDisconnectEvent event) {
System.out.println(event.getSessionId());
}
}
And from the javascript side:
var ws = new SockJS('/sockjs-node');
var client = Stomp.over(ws);
var subscription = client.subscribe("/topic/foo/bar", () => {
console.log("asdas");
});
but the connection does not happen and none of the methods get invoked. In the javascript console I can see:
>>> SUBSCRIBE
id:sub-0
destination:/topic/lala
stomp.js:199 Uncaught TypeError: Cannot read property 'send' of undefined
at Client._transmit (webpack:///./node_modules/#stomp/stompjs/lib/stomp.js?:199:26)
at Client.subscribe (webpack:///./node_modules/#stomp/stompjs/lib/stomp.js?:468:12)
at Object.eval (webpack:///./src/index.js?:128:27)
I am able to connect using wscat --connect ws://localhost:8080/sockjs-node/902/phebsu4o/websocket, but interestingly enough only the disconnect handler gets invoked and the connect handler doesn't. What am I missing here?
I found a js client which actually works on github.
import React from "react";
import SockJS from "sockjs-client";
import Stomp from "stompjs";
import PropTypes from "prop-types";
class SockJsClient extends React.Component {
static defaultProps = {
onConnect: () => {},
onDisconnect: () => {},
getRetryInterval: (count) => {return 1000 * count;},
headers: {},
autoReconnect: true,
debug: false
}
static propTypes = {
url: PropTypes.string.isRequired,
topics: PropTypes.array.isRequired,
onConnect: PropTypes.func,
onDisconnect: PropTypes.func,
getRetryInterval: PropTypes.func,
onMessage: PropTypes.func.isRequired,
headers: PropTypes.object,
autoReconnect: PropTypes.bool,
debug: PropTypes.bool
}
constructor(props) {
super(props);
this.state = {
connected: false
};
this.subscriptions = new Map();
this.retryCount = 0;
}
componentDidMount() {
this.connect();
}
componentWillUnmount() {
this.disconnect();
}
render() {
return (<div></div>);
}
_initStompClient = () => {
// Websocket held by stompjs can be opened only once
this.client = Stomp.over(new SockJS(this.props.url));
if (!this.props.debug) {
this.client.debug = () => {};
}
}
_cleanUp = () => {
this.setState({ connected: false });
this.retryCount = 0;
this.subscriptions.clear();
}
_log = (msg) => {
if (this.props.debug) {
console.log(msg);
}
}
connect = () => {
this._initStompClient();
this.client.connect(this.props.headers, () => {
this.setState({ connected: true });
this.props.topics.forEach((topic) => {
this.subscribe(topic);
});
this.props.onConnect();
}, (error) => {
if (this.state.connected) {
this._cleanUp();
// onDisconnect should be called only once per connect
this.props.onDisconnect();
}
if (this.props.autoReconnect) {
this._timeoutId = setTimeout(this.connect, this.props.getRetryInterval(this.retryCount++));
}
});
}
disconnect = () => {
// On calling disconnect explicitly no effort will be made to reconnect
// Clear timeoutId in case the component is trying to reconnect
if (this._timeoutId) {
clearTimeout(this._timeoutId);
}
if (this.state.connected) {
this.subscriptions.forEach((subid, topic) => {
this.unsubscribe(topic);
});
this.client.disconnect(() => {
this._cleanUp();
this.props.onDisconnect();
this._log("Stomp client is successfully disconnected!");
});
}
}
subscribe = (topic) => {
let sub = this.client.subscribe(topic, (msg) => {
this.props.onMessage(JSON.parse(msg.body));
});
this.subscriptions.set(topic, sub);
}
unsubscribe = (topic) => {
let sub = this.subscriptions.get(topic);
sub.unsubscribe();
this.subscriptions.delete(topic);
}
// Below methods can be accessed by ref attribute from the parent component
sendMessage = (topic, msg, opt_headers = {}) => {
if (this.state.connected) {
this.client.send(topic, opt_headers, msg);
} else {
console.error("Send error: SockJsClient is disconnected");
}
}
}
export default SockJsClient;