We have a stack: Rails / ActionCable and pure javascript client (Chrome extension) on Websocket.
When users visit same web pages extension should show them, that somebody else is currently here online. So the script sends the 'client' and 'page_id' parameters to the server in 'subscribe/unsubscribe'.
function connect(headers, pageId) {
let params = Object.keys(headers).map(key => `${key}=${headers[key]}`).join('&');
webSocket = new WebSocket(`${CABLE_URL}?${params}`);
return new Promise((resolve, reject) => {
webSocket.onopen = () => {
if (webSocket.readyState === WebSocket.OPEN) {
const params = {channel: "ExtensionConnectionChannel", client: headers.client, page_id: pageId};
const message = {command: "subscribe", identifier: JSON.stringify(params)};
webSocket.send(JSON.stringify(message));
}
let message = webSocket.readyState === WebSocket.OPEN ? 'Connection established' : 'Connection not established';
resolve(message);
};
});
function closeHandler(headers, pageId) {
if (webSocket.readyState === WebSocket.OPEN) {
const params = {channel: "ExtensionConnectionChannel", client: headers.client, page_id: pageId};
const message = {command: "unsubscribe", identifier: JSON.stringify(params)};
webSocket.send(JSON.stringify(message));
}}
When user closes the page client sent 'unsubscribe'. The problem is that unsubscription works fine only for the last signed identifier (established channel connection):
Unsubscribing from channel: {"channel":"ExtensionConnectionChannel","client":"fpShbwi2L","page_id":"123456789"}
But if several identifiers are sent, previous identifiers are not unsubscribed:
Unsubscribing from channel: {"channel":"ExtensionConnectionChannel","client":"fpShbwi2L","page_id":"123456789"}
Could not execute command from ({"command"=>"unsubscribe", "identifier"=>"{\"channel\":\"ExtensionConnectionChannel\",\"client\":\"fpShbwi2L\",\"page_id\":\"123456789\"}"})
[RuntimeError - Unable to find subscription with identifier: {"channel":"ExtensionConnectionChannel","client":"fpShbwi2L","page_id":"123456789"}]
It looks like every new subscription with new 'client' and 'page_id' overwrites previous one, but I can't figure out how and why.
The channel has a stream as a string:
class ExtensionConnectionChannel < ApplicationCable::Channel
def subscribed
stream_from 'extension_connection'
end
def unsubscribed
stop_stream_from 'extension_connection'
end
end
Please help. Thanks in advance.
Related
I am using the Node.js ws library, to listen to events in user accounts on a 3rd party API. For each user, I open a websocket to listen to the events in the user's account.
Turns out, the 3rd-party API doesn't provide a userID for each event, so if I have 10 websocket connections to user-accounts, I cannot determine which account an event came from.
I have access to a unique userId prior to starting each of my connections.
Is there a way to append or wrap the websocket connection with the userId identifier, to each connection I make, such that when I receive an event, I can access the custom identifier, and subsequently know which user's account the event came from?
The code below is a mix of real code, and pseudocode (i.e customSocket)
const ws = new WebSocket('wss://thirdparty-api.com/accounts', {
port: 8080,
});
ws.send(
JSON.stringify({
action: 'authenticate',
data: {
oauth_token: access_token,
},
})
);
// wrap and attach data here (pseudocode at top-level)
customSocket.add({userId,
ws.send(
JSON.stringify({
action: 'listen',
data: {
streams: ['action_updates'],
},
})
)
})
// listen for wrapper data here, pseudocode at top level
customSocket.emit((customData) {
ws.on('message', function incoming(data) {
console.log('incoming -> data', data.toString());
})
console.log('emit -> customData', customData);
})
Looking at the socket.io library, the namespace feature may solve for this, but I can't determine if that's true or not. Below is an example in their documentation:
// your application has multiple tenants so you want to dynamically create one namespace per tenant
const workspaces = io.of(/^\/\w+$/);
workspaces.on('connection', socket => {
const workspace = socket.nsp;
workspace.emit('hello');
});
// this middleware will be assigned to each namespace
workspaces.use((socket, next) => {
// ensure the user has access to the workspace
next();
});
I found a solution to this which is fairly simple. First create a message handler function:
const eventHandler = (uid, msg) => {
console.log(`${uid} did ${msg}`);
};
Then, when you create the websocket for the given user, wrap the .on event with the handler:
const createSocketForUser = (uid, eventHandler) => {
const socket = new WebSocket(/* ... */);
socket.onmessage = (msg) => {
eventHandler(uid, msg)
};
return socket;
}
I am trying to use mock-sockets with Cypress, setting up the mock in the onBeforeLoad hook for cy.visit() in my beforeEach block. I can get one test to work but when the mock setup runs on the next beforeEach I get an error that A mock server is already listening on this url.
code under test:
(called from my React app's componentDidiMount)
subscribeToSettings(url: string): W3CWebSocket {
let settingsSubscription = new W3CWebSocket(url);
settingsSubscription.onopen = () => console.log('WebSocket Client Connected (settings)');
settingsSubscription.onclose = () => console.log('WebSocket Client Disconnected (settings)');
settingsSubscription.onmessage = (message: MessageEvent) => this.handleSettingsMessage(message);
return settingsSubscription;
}
/**
* Handler for websocket settings messages, which updates the local settings values.
* #param message the websocket message
*/
handleSettingsMessage(message: MessageEvent) {
const updatedValues = JSON.parse(message.data);
console.log('A message was received on the settings channel.', updatedValues);
this.props.updateSettingsFromBackend(updatedValues);
}
cypress tests
import { Server } from 'mock-socket'
import { defaultSettingsState } from "../../src/reducers/settings.reducer";
import { _createSettingsApiPutPayload } from "../../src/actions/settings.actions";
describe('mock socket method 1', () => {
let mockSocket;
let mockServer;
beforeEach(() => {
cy.visit('/', {
onBeforeLoad(win: Window): void {
// #ts-ignore
cy.stub(win, 'WebSocket', url => {
mockServer = new Server(url)
mockServer.on('connection', socket => {
console.log('mock socket connected');
mockSocket = socket;
});
mockSocket = new WebSocket(url);
return mockSocket
});
},
});
});
afterEach(() => {
mockSocket.close()
mockServer.stop()
});
it('gets a message', () => {
cy.contains('SETTINGS').click()
const object = _createSettingsApiPutPayload(defaultSettingsState)
mockSocket.send(JSON.stringify(object));
cy.contains('Motion threshold')
});
it('gets another message', () => {
cy.contains('SETTINGS').click()
const object = _createSettingsApiPutPayload(defaultSettingsState)
mockSocket.send(JSON.stringify(object));
cy.contains('Motion threshold')
});
});
Here are the logs from my console:
WebSocket Client Connected (settings)
mock socket connected at url ws://localhost:8702/PM_Settings
A message was received on the settings channel. {…}
mock socket connected at url ws://localhost:3000/sockjs-node/949/mhuyekl3/websocket
The development server has disconnected.
Refresh the page if necessary.
Uncaught Error: A mock server is already listening on this url
I wonder if it has to do with that second call which is for some mystery url.
(Note: calling cy.contains('SETTINGS').click() at the end of beforeEach somehow doesn't work, even in that first test. Even when I have my app set to start on the settings page (instead of having to click to it from inside the tests), clicking on SETTINGS from beforeEach still doesn't work even though we're already there. So that's kind of weird)
These cypress logs may also be helpful:
It only worked for me, when I moved server stopping into WebSocket stub:
cy.stub(window, 'WebSocket', url => {
if (mockServer) {
mockServer.stop();
}
mockServer = new Server(url);
mockServer.on('connection', socket => {
mockSocket = socket;
});
mockSocket = new WebSocket(url);
return mockSocket;
});
Im probably wrong, but I guess afterEach or mockServer.stop(); is async thats why mock server fails to stop before new init
I am new to web-sockets. I was reading this article on medium and in a part of the client code, the code disconnects the socket every time a new message is emitted.
Why is it so? Does this have some design advantage, is this how it is done?
Why not leave the socket connected for more message streams?
-----EDIT------
Here is the code that I am talking about. The code is from this blog
newMessageReceived() {
const observable = new Observable<{ user: String, message: String}>(observer => {
this.socket.on('new message', (data) => {
observer.next(data);
});
return () => {
// Why is the coder disconnecting the socket here?
this.socket.disconnect();
};
});
return observable;
}
I'm trying to set up a server that can dynamically create many rooms for many namespaces. I'm currently just trying to broadcast to sockets of a room, when a new socket has joined that room.
So far I have been able to broadcast to a specific namespace and my event listeners on the client receives the message. However when I try to broadcast to a room, of a specific namespace, my event listener doesn't receive that message.
I've turned on the Debugger mode and see the socket.io-client:socket emitting the event with the right payload and event type. So I am not sure what I am missing since the documentation also seems fairly straightforward. Any help would be much appreciated. Below is my code.
Server
const colorNs = io.of('/color');
colorNs.on('connection', (socket) => {
const { id } = socket.handshake.query;
const { id:connId } = socket.conn;
if(id) {
socket.join(id);
socket.broadcast.to(id).emit('user:connect', { id: connId });
}
socket.on('disconnect', () => {
const { id } = socket.handshake.query;
const { id:connId } = socket.conn;
socket.broadcast.to(id).emit('user:disconnect', { id: connId });
});
});
Client
const socket = io('/color?id="123"');
socket.on('user:connect', () => console.log('data', data));
Client - Debug Trace
socket.io-parser decoded 2/color,["user:connect",{"id":"IZTTPidF121JCzf9AAAO"}] as {"type":2,"nsp":"/color","data":["user:connect",{"id":"IZTTPidF121JCzf9AAAO"}]} +1ms
browser.js:133
socket.io-client:socket emitting event ["user:connect",{"id":"IZTTPidF121JCzf9AAAO"}] +3ms
The following code will close the connection, event further observers exists on the myWebSocketSubject:
myWebSocketSubject.Observable.webSocket('ws://mysocket');
myWebSocketSubject.subscribe();
myWebSocketSubject.multiplex(..).subscribe().unsubscribe()
// the connection closed now
My expectation was, that the connection gets closed with the last unsubscribe() call (and not with the first one).
Use Case
If I get it right, with the multiplex(..) operator, on create and complete a message is send to the socket, which e.g. allows to un-/subscribe on server side to specific event.
My preferred Web Socket service could therefore look like as below. There exists only one connection, and this single connection provides several streams. On first subscription to the web socket the connection gets created; and with the last unsubscribe call the connection gets closed. For each data-stream a un-/subscribe message is sent once.
I haven't found a solution to use the WebSocketSubject.multiplex(..) method...
Preferred Example Web Socket Service
export class WebSocketService {
connection: WebSocketSubject<any>;
constructor() {
this.connection = Observable.webSocket<any>(_createConfig())
}
dataStream(eventType: string): Observable<WebSocketMessage> {
return connection.multiplex(
() => new WebSocketMessage("WebSocket.Subscribe." + eventType),
() => new WebSocketMessage("WebSocket.Unsubscribe." + eventType),
message => (message.type == eventType)
)
.retry() // reconnect on error and send subscription messages again
.share(); // send messages on last/fist un-/subscribe on this stream
}
// ...
}
export class WebSocketMessage {
type: string;
data: any;
constructor(command: string, data?:any) {
this.type = command;
this.data = data || undefined;
}
}
I have written the following test case which fails...
it('should able to handle multiple subscriptions', () => {
const subject = Observable.webSocket(<any>{url: 'ws://mysocket'});
const sub1 = subject.subscribe();
const sub2 = subject.subscribe();
const socket = MockWebSocket.lastSocket;
socket.open();
sinon.spy(socket, 'close');
sub1.unsubscribe();
// Fails, because the socket gets closed on first unsubscribe
expect(socket.close).have.not.been.called;
sub2.unsubscribe();
expect(socket.close).have.been.called;
});
If I get it right the share operator would do the trick. But after using the operator, the multiplex method is not available.
Thanks for any feedback, input, ...!