is it somehow possible to listen to a WebSocket inside a service, and make that service "reactive" by listening to messages from sockets and then sending the changes to my other components, e.g by observables?
It should of course work over different tab/browser instances.
What is the correct pattern? And if I use the service in another angular component, do I need to somehow initialize it first, or is it enough to import the dependency inside my component, because there is no ngOnInit
method in services?
I would probably create my socket listener in the service constructor and then next new events with observables. But, not sure exactly how.!
CodePudding user response:
Yes, it is possible listen to a WebSocket inside a service and notify the messages received to Components via Observables.
In my opinion this is a very natural use case for Observables.
You can open the websocket connection when you launch the app and, if it is a service provided in root
, you can share the same service instance across different components.
This function returns an Observable that notifies and instance of WebSocket
when a websocket connection is opened
function openSocket$(url: string) {
return new Observable(
(subscriber: Subscriber<WebSocket>): TeardownLogic => {
const conn = new WebSocket(url);
conn.onopen = () => {
subscriber.next(conn);
subscriber.complete();
};
conn.onerror = (err) => {
console.error("Websocket errored while trying to connect", err);
subscriber.error(err);
};
conn.onclose = (ev) => {
console.log("Websocket closed before having emitted any message");
subscriber.complete();
};
}
);
}
This function requires an instance of WebSocket
as input and returns an Observable which notifies each message received on the websockets channel:
function messages$(socket: WebSocket) {
// if the onmessage property is defined, it means that this websocket is already in use by
// some component which is interested in its message stream
if (socket.onmessage) {
throw new Error(
"Websocket has already a function set to manage the message stream"
);
}
return new Observable(
(subscriber: Subscriber<MessageEvent>): TeardownLogic => {
socket.onmessage = (msg: MessageEvent) => {
subscriber.next(msg);
};
socket.onerror = (err) => {
console.error("Websocket errored while sgreaming messages");
subscriber.error(err);
};
socket.onclose = () => {
console.log("Websocket closed");
subscriber.complete();
};
return () => {
// clean the onmessage callback when the Observable is unsubscribed so that we can safely check
// whether the onmessage callback is null or undefined at the beginning of this function
socket.onmessage = null;
};
}
);
}
You can see an example of app that uses this mechanism here.
CodePudding user response:
For any library raising events outside angular's scope/zone the best bet is to bring it into the scope/zone.
This way you don't need to think about any further synchronization with angular lifecycle events.
I usually use a Subject or ReplaySubject in a service to make sure all components can listen to the events raised.
@Injectable()
export class YourService {
eventType1$ = new Subject()
constructor(private ngZone: NgZone ...
...
socketObject.onEventType1((eventType1) => {
this.ngZone.run(() => {
this.eventType1$.next(eventType1)
});
})
Then from your components you can subscribe to this service's public observable normally:
yourComponentFunction() {
this.yourService.eventType1$.subscribe((eventType1) => {
// do something with eventType1
})
}
Note: please remember to complete any subscriptions when the component is destroyed to avoid the common memory leak.