@adaskothebeast/ngxs-signalr-plugin
Version:
Bind server SignalR events to Ngxs store actions.
391 lines (371 loc) • 13.2 kB
JavaScript
import { CommonModule } from '@angular/common';
import * as i0 from '@angular/core';
import { InjectionToken, Injectable, Inject, provideAppInitializer, inject, NgModule } from '@angular/core';
import { HubConnectionBuilder, HubConnectionState, HttpTransportType, JsonHubProtocol } from '@microsoft/signalr';
import * as i1 from '@ngxs/store';
import { ofActionDispatched } from '@ngxs/store';
import { Subscription } from 'rxjs/internal/Subscription';
/**
* Action to connect to the websocket. Optionally pass a URL.
*/
class ConnectSignalR {
static { this.type = '[SignalR] Connect'; }
constructor(payload) {
this.payload = payload;
}
}
/**
* Action to disconnect the SignalR.
*/
class DisconnectSignalR {
static { this.type = '[SignalR] Disconnect'; }
}
/**
* Action to invoke to the server.
*/
class InvokeSignalRMessage {
static { this.type = '[SignalR] Invoke Message'; }
constructor(name, payload) {
this.name = name;
this.payload = payload;
}
}
/**
* Action to send to the server.
*/
class SendSignalRMessage {
static { this.type = '[SignalR] Send Message'; }
constructor(name, payload) {
this.name = name;
this.payload = payload;
}
}
/**
* Action triggered when SignalR is connected
*/
class SignalRConnected {
static { this.type = '[SignalR] Connected'; }
}
/**
* Action dispatched when the user tries to connect if the connection already exists.
*/
class SignalRConnectionUpdated {
static { this.type = '[SignalR] Connection Updated'; }
}
/**
* Action triggered when SignalR is disconnected
*/
class SignalRDisconnected {
static { this.type = '[SignalR] Disconnected'; }
constructor(payload) {
this.payload = payload;
}
}
/**
* Action triggered when a error ocurrs
*/
class SignalRMessageError {
static { this.type = '[SignalR] Message Error'; }
constructor(payload) {
this.payload = payload;
}
}
/**
* Action triggered when SignalR is connected
*/
class SignalRReconnected {
static { this.type = '[SignalR] Reconnected'; }
constructor(connectionId) {
this.connectionId = connectionId;
}
}
/**
* Action triggered when SignalR is connected
*/
class SignalRReconnecting {
static { this.type = '[SignalR] Reconnecting'; }
constructor(payload) {
this.payload = payload;
}
}
/**
* Action triggered when SignalR is connected
*/
class SignalRStreamCompleted {
static { this.type = '[SignalR] Stream Completed'; }
}
/**
* Action triggered when a error ocurrs
*/
class SignalRStreamError {
static { this.type = '[SignalR] Stream Error'; }
constructor(payload) {
this.payload = payload;
}
}
/**
* Action to stream from the server.
*/
class StreamSignalRMessage {
static { this.type = '[SignalR] Stream Message'; }
constructor(name, payload) {
this.name = name;
this.payload = payload;
}
}
const NGXS_SIGNALR_OPTIONS = new InjectionToken('NGXS_SIGNALR_OPTIONS');
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function noop(..._args) {
return function () {
// noop
};
}
/**
* This error is thrown where there is no `type` (or custom `typeKey`) property
* on the message that came from the server side socket
*/
class TypeKeyPropertyMissingError extends Error {
constructor(typeKey) {
super(`Property ${typeKey} is missing on the socket message`);
}
}
class SignalRHandler {
constructor(store, actions$, options) {
this.store = store;
this.actions$ = actions$;
this.options = options;
this.connection = null;
this.subscription = new Subscription();
this.streamSubscriptions = [];
this.typeKey = 'type';
this.setupActionsListeners();
if (this.options.typeKey) {
this.typeKey = this.options.typeKey;
}
}
ngOnDestroy() {
this.closeConnection();
this.subscription.unsubscribe();
}
setupActionsListeners() {
this.subscription.add(this.actions$
.pipe(ofActionDispatched(ConnectSignalR))
.subscribe(async ({ payload }) => {
await this.connect(payload);
}));
this.subscription.add(this.actions$
.pipe(ofActionDispatched(DisconnectSignalR))
.subscribe(() => {
this.disconnect();
}));
this.subscription.add(this.actions$
.pipe(ofActionDispatched(SendSignalRMessage))
.subscribe(async ({ name, payload }) => {
await this.send(name, payload);
}));
this.subscription.add(this.actions$
.pipe(ofActionDispatched(InvokeSignalRMessage))
.subscribe(async ({ name, payload }) => {
await this.invoke(name, payload);
}));
this.subscription.add(this.actions$
.pipe(ofActionDispatched(StreamSignalRMessage))
.subscribe(async ({ name, payload }) => {
const resultStream = await this.stream(name, payload);
this.streamSubscriptions.push(resultStream.subscribe({
next: (item) => {
const type = item[this.typeKey];
if (!type) {
throw new TypeKeyPropertyMissingError(this.typeKey);
}
this.store.dispatch({ ...item, type });
},
complete: () => {
this.store.dispatch(new SignalRStreamCompleted());
},
error: (err) => {
this.store.dispatch(new SignalRStreamError(err));
},
}));
}));
}
setupConnection() {
let builder = new HubConnectionBuilder();
if (this.options.url) {
if (this.options.httpConnectionOptions) {
builder = builder.withUrl(this.options.url, this.options.httpConnectionOptions);
}
else if (this.options.transportType) {
builder = builder.withUrl(this.options.url, this.options.transportType);
}
else {
builder = builder.withUrl(this.options.url);
}
}
if (this.options.reconnectPolicy) {
builder = builder.withAutomaticReconnect(this.options.reconnectPolicy);
}
else if (this.options.retryDelays) {
builder = builder.withAutomaticReconnect(this.options.retryDelays);
}
else if (this.options.automaticReconnect) {
builder = builder.withAutomaticReconnect();
}
if (this.options.protocol) {
builder = builder.withHubProtocol(this.options.protocol);
}
if (this.options.logging) {
builder = builder.configureLogging(this.options.logging);
}
this.connection = builder.build();
if (this.options.baseUrl) {
this.connection.baseUrl = this.options.baseUrl;
}
if (this.options.keepAliveIntervalInMilliseconds) {
this.connection.keepAliveIntervalInMilliseconds =
this.options.keepAliveIntervalInMilliseconds;
}
if (this.options.serverTimeoutInMilliseconds) {
this.connection.serverTimeoutInMilliseconds =
this.options.serverTimeoutInMilliseconds;
}
this.connection.onclose((error) => {
this.store.dispatch(new SignalRDisconnected(error));
});
this.connection.onreconnecting((error) => {
this.store.dispatch(new SignalRReconnecting(error));
});
this.connection.onreconnected((connectionId) => {
this.store.dispatch(new SignalRReconnected(connectionId));
});
}
setupHandlers() {
this.connection?.on('send', (message) => {
const type = message[this.typeKey];
if (!type) {
throw new TypeKeyPropertyMissingError(this.typeKey);
}
this.store.dispatch({ ...message, type });
});
}
async connect(options) {
this.updateConnection();
// Users can pass the options in the connect method so
// if options aren't available at DI bootstrap they have access
// to pass them here
if (options) {
this.options = { ...this.options, ...options };
}
this.setupConnection();
this.setupHandlers();
try {
await this.connection?.start();
this.store.dispatch(new SignalRConnected());
}
catch (err) {
this.store.dispatch(new SignalRMessageError(err));
}
}
disconnect() {
if (this.connection) {
this.closeConnection();
}
}
async send(name, data) {
if (this.connection?.state !== HubConnectionState.Connected) {
throw new Error('You must connect to the SignalR before sending any data');
}
await this.connection.send(name, data);
}
async invoke(name, data) {
if (this.connection?.state !== HubConnectionState.Connected) {
throw new Error('You must connect to the SignalR before sending any data');
}
await this.connection.invoke(name, data);
}
async stream(name, data) {
if (this.connection?.state !== HubConnectionState.Connected) {
throw new Error('You must connect to the SignalR before sending any data');
}
return this.connection.stream(name, data);
}
/**
* To ensure we don't have any memory leaks
* e.g. if the user occasionally dispatched `ConnectWebSocket` twice
* then the previous subscription will still live in the memory
* to prevent such behavior - we close the previous connection if it exists
*/
updateConnection() {
if (this.connection) {
this.closeConnection();
this.store.dispatch(new SignalRConnectionUpdated());
}
}
async closeConnection() {
this.streamSubscriptions.forEach((s) => {
s.dispose();
});
this.streamSubscriptions = [];
if (this.connection !== null) {
await this.connection.stop();
this.connection = null;
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: SignalRHandler, deps: [{ token: i1.Store }, { token: i1.Actions }, { token: NGXS_SIGNALR_OPTIONS }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: SignalRHandler }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: SignalRHandler, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: i1.Store }, { type: i1.Actions }, { type: undefined, decorators: [{
type: Inject,
args: [NGXS_SIGNALR_OPTIONS]
}] }] });
function signalrOptionsFactory(options) {
return {
keepAliveIntervalInMilliseconds: 15000,
serverTimeoutInMilliseconds: 30000,
typeKey: 'type',
automaticReconnect: true,
transportType: HttpTransportType.WebSockets,
protocol: new JsonHubProtocol(),
...options,
};
}
const NGXS_SIGNALR_USER_OPTIONS = new InjectionToken('NGXS_SIGNALR_USER_OPTIONS');
class NgxsSignalrPluginModule {
static forRoot(options) {
return {
ngModule: NgxsSignalrPluginModule,
providers: [
SignalRHandler,
{
provide: NGXS_SIGNALR_USER_OPTIONS,
useValue: options,
},
{
provide: NGXS_SIGNALR_OPTIONS,
useFactory: signalrOptionsFactory,
deps: [NGXS_SIGNALR_USER_OPTIONS],
},
provideAppInitializer(() => {
const initializerFn = (noop)(inject(SignalRHandler));
return initializerFn();
}),
],
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: NgxsSignalrPluginModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.0.5", ngImport: i0, type: NgxsSignalrPluginModule, imports: [CommonModule] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: NgxsSignalrPluginModule, imports: [CommonModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.5", ngImport: i0, type: NgxsSignalrPluginModule, decorators: [{
type: NgModule,
args: [{
imports: [CommonModule],
}]
}] });
/**
* Generated bundle index. Do not edit.
*/
export { NGXS_SIGNALR_USER_OPTIONS, NgxsSignalrPluginModule, signalrOptionsFactory };
//# sourceMappingURL=adaskothebeast-ngxs-signalr-plugin.mjs.map