mqttjs keep disconnecting and reconnect in main thread, but stable in worker thread - websocket

I am using mqttjs to connect to the aws iot mqtt by wss presigned URL
In my application, there are 2 mqtt connection, the first one is from the main thread, the second one is from a web worker thread. Both are created from a same class (MqttServcie), they have the same logic, same everything.
But the one from the main thread keep disconnecting and reconnecting.
The one from the worker is very stable, the connection is never die, it never has to be reconnected.
Do you have any idea why the connection from the main thread keep disconnecting?
And what make a connection ends? (other than connection timeout and lost wifi connection)
In the image below, I end the connection and create a new one after 5 times retrying fails, so the number of request is several, nevermind.
The client id is always random, so it never be kicked off by another client.
The MqttService class
/* eslint-disable no-useless-constructor, no-empty-function */
import mqtt, { MqttClient } from 'mqtt';
import { NuxtAxiosInstance } from '#nuxtjs/axios';
import RestService from './RestService';
import { Mqtt } from '~/types/Mqtt';
import { MILISECS_PER_SEC } from '~/configs';
import { logWithTimestamp } from '~/utils';
export type MqttServiceEventHandlers = {
close?: Array<() => void>;
disconnectd?: Array<() => void>;
connected?: Array<() => void>;
reconnect?: Array<() => void>;
reconnected?: Array<() => void>;
beforeReconect?: Array<() => void>;
};
export type MqttServiceEvent = keyof MqttServiceEventHandlers;
export interface IMqttService {
httpClient?: NuxtAxiosInstance;
presignedUrl?: string;
}
export class MqttService {
public client: MqttClient | null = null;
public closing = false;
public reconnecting = false;
// Example: "wss://abcdef123-ats.iot.us-east-2.amazonaws.com/mqtt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=XXXXXX%2F20230206%2Fus-east-2%2Fiotdevicegateway%2Faws4_request&X-Amz-Date=20230206T104907Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=abcxyz123"
public presignedUrl = '';
public httpClient = null as null | NuxtAxiosInstance;
public retryCount = 0;
public retryLimits = 5;
public handlers: MqttServiceEventHandlers = {};
constructor(
{ httpClient, presignedUrl }: IMqttService,
public caller = 'main'
) {
if (httpClient) {
this.httpClient = httpClient;
} else if (presignedUrl) {
this.presignedUrl = presignedUrl;
} else {
throw new Error(
'[MqttService] a httpClient or presigned URL must be provided'
);
}
}
async connect() {
await this.updatePresignedUrl();
this.client = mqtt.connect(this.presignedUrl, {
clientId: this.generateClientId(),
reconnectPeriod: 5000,
connectTimeout: 30000,
resubscribe: true,
keepalive: 15,
transformWsUrl: (_url, _options, client) => {
// eslint-disable-next-line no-param-reassign
client.options.clientId = this.generateClientId();
logWithTimestamp(
`[MQTT] [${this.caller}] transformWsUrl()`,
client.options.clientId,
this.signature
);
return this.presignedUrl;
},
});
return this.setupHandlers();
}
protected setupHandlers() {
return new Promise<MqttClient>((resolve, reject) => {
this.client!.on('close', async () => {
if (this.closing) return;
if (this.retryCount >= this.retryLimits) {
(this.handlers.close || []).forEach((handler) => handler());
await this.disconnect();
logWithTimestamp(`[MQTT] [${this.caller}] connection has closed!`);
reject(new Error(`All retry attempts were failed (${this.caller})`));
return;
}
if (this.retryCount === 0) {
(this.handlers.beforeReconect || []).forEach((handler) => handler());
logWithTimestamp(
`[MQTT] [${this.caller}] connection lost`,
this.presignedUrl
);
}
// Re-generate new presigned URL at the 3rd attempt, or if the URL is expired
if (this.retryCount === 2 || this.isExpired) {
await this.updatePresignedUrl().catch(async () => {
await this.disconnect();
(this.handlers.close || []).forEach((handler) => handler());
logWithTimestamp(
`[MQTT] [${this.caller}] connection has closed! (Unable to get new presigned url)`
);
});
}
});
this.client!.on('reconnect', () => {
this.retryCount += 1;
this.reconnecting = true;
(this.handlers.reconnect || []).forEach((handler) => handler());
logWithTimestamp(
`[MQTT] [${this.caller}] reconnect (#${this.retryCount})`
);
});
this.client!.on('connect', () => {
if (this.reconnecting) {
(this.handlers.reconnected || []).forEach((handler) => handler());
}
this.retryCount = 0;
this.reconnecting = false;
(this.handlers.connected || []).forEach((handler) => handler());
logWithTimestamp(`[MQTT] [${this.caller}] connected`);
resolve(this.client!);
});
});
}
disconnect({ force = true, silent = false, ...options } = {}) {
this.closing = true;
return new Promise<void>((resolve) => {
if (this.client && this.isOnline()) {
this.client.end(force, options, () => {
this.reset(silent, '(fully)');
resolve();
});
} else {
this.client?.end(force);
this.reset(silent, '(client-side only)');
resolve();
}
});
}
reset(silent = false, debug?: any) {
this.client = null;
this.retryCount = 0;
this.reconnecting = false;
this.presignedUrl = '';
this.closing = false;
if (!silent) {
(this.handlers.disconnectd || []).forEach((handler) => handler());
}
logWithTimestamp(`[MQTT] [${this.caller}] disconnected!`, {
silent,
debug,
});
}
async destroy() {
await this.disconnect({ silent: true });
this.handlers = {};
}
// Get or assign a new wss url
async updatePresignedUrl(url?: string) {
if (this.httpClient) {
const service = new RestService<Mqtt>(this.httpClient, '/mqtts');
const { data } = await service.show('wss_link');
this.presignedUrl = data!.wss_link;
} else if (url) {
this.presignedUrl = url;
}
return this.presignedUrl;
}
on(event: MqttServiceEvent, handler: () => void) {
const { [event]: eventHanlders = [] } = this.handlers;
eventHanlders.push(handler);
this.handlers[event] = eventHanlders;
}
off(event: MqttServiceEvent, handler: () => void) {
const { [event]: eventHanlders = [] } = this.handlers;
const index = eventHanlders.findIndex((_handler) => _handler === handler);
eventHanlders.splice(index, 1);
}
get date() {
const matched = this.presignedUrl.match(/(X-Amz-Date=)(\w+)/);
if (!matched) return null;
return new Date(
String(matched[2]).replace(
/^(\d{4})(\d{2})(\d{2})(T\d{2})(\d{2})(\d{2}Z)$/,
(__, YYYY, MM, DD, HH, mm, ss) => `${YYYY}-${MM}-${DD}${HH}:${mm}:${ss}`
)
);
}
get expires() {
const matched = this.presignedUrl.match(/(X-Amz-Expires=)(\d+)/);
return matched ? Number(matched[2]) : null;
}
get signature() {
const matched = this.presignedUrl.match(/(X-Amz-Signature=)(\w+)/);
return matched ? matched[2] : null;
}
get expiresDate() {
if (!(this.date && this.expires)) return null;
return new Date(this.date.getTime() + this.expires * MILISECS_PER_SEC);
}
get isExpired() {
return !this.expiresDate || this.expiresDate <= new Date();
}
private generateClientId() {
return `mqttjs_[${this.caller}]_${Math.random()
.toString(16)
.substring(2, 10)}`.toUpperCase();
}
private isOnline() {
return typeof window !== 'undefined' && window?.$nuxt?.isOnline;
}
}

Related

Angular unit testing for the promises

I am trying to write the test cases for below method :-
The below is the component code for adding the product
COMPONENT:
import { HttpClient } from '#angular/common/http';
import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '#angular/forms';
import { Observable } from 'rxjs';
import { Constants } from '../../../../../../utils/constants';
import { FileUploadService } from '../../../services/file-upload.service';
import { Component, EventEmitter, Input, OnInit, Output } from '#angular/core';
import { ManagePackageService } from '../../../services/manage-package.service';
import { DomSanitizer } from '#angular/platform-browser';
import { Package } from '../../../models/package.model';
import { ProductPackageMapping } from '../../../models/product-package-mapping.model';
import { Product } from '../../../models/product.model';
import { NotificationAlertService } from '../../../../shared/services/notification-alert.service';
import { ManageProductService } from '../../../services/manage-product.service';
import { DocumentDTO } from '../../../models/document-dto';
import { saveAs } from 'file-saver';
import { ConfirmationDialogService } from '../../../../shared/services/confirmation-dialog.service';
#Component({
selector: 'app-add-product',
templateUrl: './add-product.component.html',
styleUrls: ['./add-product.component.scss']
})
export class AddProductComponent implements OnInit {
productDetailForm: FormGroup;
addTitle: boolean = true;
isView: boolean = true;
submitted: boolean = false;
status: boolean = true;
check: boolean = false;
changevar: string;
#Input() id: Number;
#Output() changeIndicator = new EventEmitter<string>();
#Input() list: ProductPackageMapping;
#Output() dataLoaded = new EventEmitter<string>();
fileToUpload: File | null = null;
fileArray: Array<any> = [];
productData: any = [];
productDataTemp: any[];
fData = new FormData();
packagePlan: Array<any> = [];
productDto = new Product();
packageDto = new Package();
productPackageMappingDto = new ProductPackageMapping();
documentDto: Array<any> = [];
isDocId: boolean = false;
productForm: FormGroup;
activeStatus: boolean = true;
requiredFileType: string;
public NAME_MAX_LENGTH = 50;
isValidFormSubmitted: boolean;
#Input() allRolesFromParent: Product[] = [];
selectedFiles?: FileList;
message: string[] = [];
fileInfos?: Observable<any>;
documentList: Array<any> = [];
ppm: any;
fileUrl: any;
constructor(private http: HttpClient,
private formBuilder: FormBuilder,
private uploadService: FileUploadService,
private manageProduct: ManageProductService,
private managePackageService: ManagePackageService,
private sanitizer: DomSanitizer,
private notifyService: NotificationAlertService,
private confirmationDialogService: ConfirmationDialogService
) { }
ngOnInit(): void {
this.createForm();
this.fileInfos = this.uploadService.getFiles();
this.fData = new FormData();
this.subscriptionPackages()
}
closeModal() {
this.status = true;
this.check = false;
this.changevar = 'cancel';
this.changeIndicator.emit(this.changevar);
this.productForm.reset();
this.fData.delete('ppm');
this.fData.delete('file');
this.emit();
}
closeEditModel() {
this.status = true;
this.documentList = [];
this.changevar = "cancel";
this.changeIndicator.emit(this.changevar);
this.addTitle = true;
this.productForm.reset();
this.fData.delete("ppm");
this.fData.delete("file");
this.emit();
}
onChangeSwitch(event) {
if (event.target.checked == true) {
this.activeStatus = true;
} else {
this.activeStatus = false;
}
}
createForm() {
this.productForm = new FormGroup({
name: new FormControl(this.productDto.name, [
Validators.required,
Validators.maxLength(this.NAME_MAX_LENGTH),
Validators.pattern(Constants.NO_WHITE_SPACE_PATTERN),
]),
url: new FormControl(this.productDto.demoURL, Validators.required),
version: new FormControl(this.productDto.productVersion, Validators.required),
projectType: new FormControl(this.productDto.projectType, Validators.required),
plansName: new FormControl(this.packageDto.id, Validators.required),
desc: new FormControl(this.productDto.productDescription, Validators.required),
active: new FormControl(this.productDto.active, Validators.required),
documents: this.formBuilder.array([]),
});
}
ngOnChanges() {
this.productData = this.list;
if (this.id) {
this.addTitle = false;
this.productDataTemp = this.productData.filter((vl) => (this.id == vl.id));
this.productDataTemp.forEach(obj => {
this.productDto.ppmId = obj.ppmId;
this.productDto.id = obj.id;
this.productDto.name = obj.name;
this.productDto.demoURL = obj.demoURL;
this.productDto.productDescription = obj.productDescription;
this.productDto.projectType = obj.type;
this.productDto.active = obj.active;
this.productDto.productVersion = obj.version;
this.packageDto.id = obj.pkgInfo.id;
this.documentList = obj.listDocuments
})
this.createForm();
} else {
this.productDto = new Product();
this.packageDto = new Package();
this.documentDto = [];
this.documentList = [];
}
this.documentList.length == 0 ? this.isDocId = false : this.isDocId = true;
}
get f() {
return this.productForm.controls;
}
choosePackages(e) {
}
subscriptionPackages() {
this.managePackageService.getPackages().subscribe(
(response) => {
this.packagePlan = response;
},
(httpErrorRes) => {
}
);
}
get documentsFormArray(): FormArray {
return this.f['documents'] as FormArray;
}
addDocument() {
if (this.documentsFormArray.length < 5) {
this.documentsFormArray.push(this.formBuilder.group({
id: new FormControl(''),
docName: new FormControl('', Validators.required),
docType: new FormControl('', Validators.required),
attachment: new FormControl('', Validators.required)
}))
}
}
deleteRow(i: number) {
if (this.id) {
delete this.fileArray[i];
this.documentsFormArray.removeAt(i);
this.fileArray.splice(i, 1);
}
else {
delete this.documentsFormArray[i];
this.documentsFormArray.removeAt(i);
this.fileArray.splice(i, 1);
}
}
getFiles(fileArray) {
this.fileArray.forEach(files => {
this.fData.append("file", files);
})
}
selectFiles(event): void {
this.selectedFiles = event.target.files;
}
onFormSubmit() {
this.submitted = true;
if (this.fileArray.length > 0) {
this.getFiles(this.fileArray);
}
if (this.productForm.value.id === null) {
this.productDto.id = null;
}
this.productDto.name = this.productForm.value.name;
this.productDto.demoURL = this.productForm.value.url;
this.productDto.productVersion = this.productForm.value.version;
this.productDto.projectType = this.productForm.value.projectType;
this.productDto.productDescription = this.productForm.value.desc;
this.productDto.active = this.productForm.value.active;
this.packageDto.id = this.productForm.value.plansName;
if (this.documentsFormArray && this.documentsFormArray.value && this.documentsFormArray.value.length > 0) {
this.documentsFormArray.value.forEach(element => {
this.documentDto.push(element);
});
}
this.productPackageMappingDto.id = this.productDto.ppmId ? this.productDto.ppmId : null;
this.productPackageMappingDto.products = this.productDto;
this.productPackageMappingDto.packages = this.packageDto;
this.productPackageMappingDto.document = this.documentDto;
this.ppm = new Blob([JSON.stringify(this.productPackageMappingDto)], { type: "application/json" });
this.fData.has("file") ? true : this.fData.append("file", '');
this.fData.append("ppm", this.ppm);
this.confirmationDialogService.confirm('Are you sure ?', 'Do you really want to Activate/ Deactivate Package ?')
.then((confirmed) => {
if (confirmed) {
this.manageProduct.createProduct(this.fData).subscribe(
(response) => {
this.notifyService.showSuccess("Data saved successfully !!", "Success")
this.closeModal();
this.fData.delete("ppm");
this.fData.delete("file");
this.productDto = new Product();
this.packageDto = new Package();
this.documentDto = [];
this.documentList = [];
this.fileArray = [];
this.emit();
},
(httpErrorRes) => {
this.fData.delete("ppm");
this.fData.delete("file");
this.notifyService.showError(httpErrorRes.error, "Error");
}
);
}
else {
this.fData.delete("ppm");
this.fData.delete("file");
}
})
}
upload(event: any) {
let files = event.target.files[0];
this.fileArray.push(files);
}
downloadFile(fileData: DocumentDTO): void {
this.manageProduct.download(fileData.fileName)
.subscribe(blob => saveAs(blob, fileData.fileName));
}
emit() {
this.dataLoaded.emit('AddProductComponent')
}
}
The below is the spec file for the above component
COMPONENT.SPEC.TS
import { HttpClientTestingModule } from '#angular/common/http/testing';
import { ComponentFixture, TestBed } from '#angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '#angular/forms';
import { NgSelectModule } from '#ng-select/ng-select';
import { TranslateModule } from '#ngx-translate/core';
import { ToastrModule } from 'ngx-toastr';
import { Observable, of, throwError } from 'rxjs';
import { AppConfig } from '../../../../../app.config';
import { ConfirmationDialogService } from '../../../../configurations/services/confirmation-dialog.service';
import { NotificationAlertService } from '../../../../shared/services/notification-alert.service';
import { DocumentDTO } from '../../../models/document-dto';
import { ManagePackageService } from '../../../services/manage-package.service';
import { ManageProductService } from '../../../services/manage-product.service';
import { AddProductComponent } from './add-product.component';
describe('AddProductComponent', () => {
let component: AddProductComponent;
let fixture: ComponentFixture<AddProductComponent>;
let packageServiceStub = jasmine.createSpyObj('ManagePackageService', ['getPackages']);
let confirmationDialougeStub = jasmine.createSpyObj('ConfirmationDialogService', ['confirm']);
let manageProductStub = jasmine.createSpyObj('ManageProductService', ['createProduct','download']);
let notificationStub = jasmine.createSpyObj('NotificationAlertService', ['showSuccess', 'showError']);
let fileData = new DocumentDTO()
class MockUserService {
getPackages() {
return [{'name':'basic', 'id':1}, {'name':'advance', 'id':2}, {'name':'premium', 'id':3}];
}
}
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HttpClientTestingModule,ReactiveFormsModule,FormsModule,ToastrModule.forRoot(),
TranslateModule.forRoot(), NgSelectModule],
declarations: [ AddProductComponent ],
providers :[AppConfig,
{ provide: ManageProductService, useValue: manageProductStub },
{ provide: ManagePackageService, useValue: packageServiceStub }]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AddProductComponent);
component = fixture.componentInstance;
const mockpackageResponse = [];
packageServiceStub.getPackages.and.returnValue(of(mockpackageResponse));
component.activeStatus = true;
component.productDataTemp = [{'ppmId': 2, 'id': 2, 'name': 'ttvrtv', 'productDescription': 'vttvt', 'projectType': null}]
component.productData = [{'ppmId': 1, 'id': 1, 'name': 'tvrtv', 'productDescription': 'vtvt', 'projectType': null},
{'ppmId': 2, 'id': 2, 'name': 'ttvrtv', 'productDescription': 'vttvt', 'projectType': null}]
component.id = 2;
component.fileArray = ['dfdfdf','dfdsdf']
component.packagePlan = [{'name':'basic', 'id':1}, {'name':'advance', 'id':2}, {'name':'premium', 'id':3}];
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should create closeModal()', () => {
const eventTrue = {
target: {
checked: true,
}
};
const eventFalse = {
target: {
checked: false,
}
};
component.closeModal();
component.closeEditModel();
component.onChangeSwitch(eventTrue)
component.onChangeSwitch(eventFalse)
});
// it('should create ngOnChanges', () => {
// component.ngOnChanges();
// });
it('should create choosePackages', () => {
const event = {};
component.choosePackages(event);
});
it('should create subscriptionPackages()', () => {
component.subscriptionPackages();
expect(component.packagePlan).toBeTruthy();
});
// it('should handle error subscriptionPackages', () => {
// spyOn(window, 'alert');
// packageServiceStub.getPackages.and.returnValue(throwError({ error: 'some error'}));
// component.subscriptionPackages();
// });
it('should create addDocument()', () => {
component.addDocument();
});
it('should create deleteRow(i: number)', () => {
component.id = 1;
component.deleteRow(1);
});
it('should create deleteRow(i: number) nullity check', () => {
component.id = null;
component.deleteRow(null);
});
it('should create selectFiles', () => {
const eventSelectFiles = {
target: {
files: 'ss',
}
};
component.selectFiles(eventSelectFiles);
});
it('should create onFormSubmit length>0', () => {
component.fileArray.length = 2;
component.onFormSubmit();
});
it('should create onFormSubmit nullity check', () => {
component.productForm.value.id = null
component.onFormSubmit();
});
// it('should create onFormSubmit else part', (done) => {
// const confirmationResponse = {}; // Keep is as your response
// let spy = spyOn( confirmationDialougeStub,'confirm').and.returnValue(Promise.resolve(true));
// // fixture.whenStable().then(confirmed => {
// // fixture.detectChanges();
// // component.onFormSubmit();
// // })
// spy.calls.mostRecent().returnValue.then(() => {
// fixture.detectChanges();
// //component.onFormSubmit();
// done();
// });
// });
// it('should create onFormSubmit else part', () => {
// confirmationDialougeStub.confirm.and.returnValue({
// closePromise : {
// then : function(callback) {
// callback({value: true});
// }
// }
// });
// });
it('should create upload download', () => {
const eventFile = {
target: {
files: true,
}
};
component.upload(eventFile);
});
});
PROBLEM :- I have some below pointers regarding Jasmine and trying to figure it out the way to achieve the same.
I am not able to figured out the way to cover or test what the promises return
this.confirmationDialogService.confirm('Are you sure ?', 'Do you really want to Activate/ Deactivate Package ?')
.then((confirmed) => {
In the code coverage, the above lines are not covered and I don't know what went wrong, I have tried so many ways and referred many of the posts from stackoverflow itself but that weren't work for my use case.
how to write test cases for a method having foreach(), filter(), map()
how to write test cases for a method having observables, subscribe and promises
Please suggest the best practices.
Your component is quite long, I will show you how to you can cover the following lines with Promises and Observables.
this.confirmationDialogService.confirm('Are you sure ?', 'Do you really want to Activate/ Deactivate Package ?')
.then((confirmed) => {
if (confirmed) {
this.manageProduct.createProduct(this.fData).subscribe(
Make the following changes:
// dialog is spelt wrong here
let confirmationDialogStub = jasmine.createSpyObj('ConfirmationDialogService', ['confirm']);
....
providers: [
...
// provide the mock for confiramtion dialog service
{ provide: ConfirmationDialogService, usevalue: confirmationDialogStub },
{ provide: ManageProductService, useValue: manageProductStub },
{ provide: ManagePackageService, useValue: packageServiceStub }
],
// fakeAsync because we need tick (wait for promises to resolve before asserting)
it('does abc', fakeAsync(() => {
// mock the value for confirmation with a promise
confirmationDialogStub.confirm.and.returnValue(Promise.resovle(true));
manageProductStub.createProduct.and.returnValue(of({/* mock however you wish */}));
// call the method
component.onFormSubmit();
// wait for all promises to resolve with tick
tick();
// do your assertions
}));
For forEach, filter, map, make sure you mock an array. That the array is there.
That being said, I highly recommend you check out the following resources, they will help you a lot.
https://testing-angular.com/
^ A well written e-book.
https://www.pluralsight.com/courses/unit-testing-angular
^ A really good class on unit testing in Angular.

Virtual assistant created using Typscript- running in Bot Framework Emulator is not responding

I am trying to develop virtual assistant using typescript . i have followed this below document
https://microsoft.github.io/botframework-solutions/tutorials/typescript/create-assistant/1_intro/
When i run npm start and test it in Botframework emulator , the bot is not responding any message.
But the Bot is opening with new user greeting adaptive card message
I have tried to edit the adaptive greeting card following this document page
https://microsoft.github.io/botframework-solutions/tutorials/typescript/customize-assistant/2_edit_your_greeting/
but eventhough the bot is not replying any message
`[11:53:22]Emulator listening on http://localhost:49963`
[11:53:22]ngrok listening on https://b2915c2d.ngrok.io
[11:53:22]ngrok traffic inspector:http://127.0.0.1:4040
[11:53:22]Will bypass ngrok for local addresses
[11:53:23]<- messageapplication/vnd.microsoft.card.adaptive
[11:53:23]POST200conversations.replyToActivity
[11:53:23]POST200directline.conversationUpdate
[11:53:23]POST200directline.conversationUpdate
expected and actual results: it should ask what is your name once connected and start conversing
================================================================================mainDialog.ts
import {
BotFrameworkAdapter,
BotTelemetryClient,
RecognizerResult,
StatePropertyAccessor } from 'botbuilder';
import { LuisRecognizer, LuisRecognizerTelemetryClient, QnAMakerResult, QnAMakerTelemetryClient } from 'botbuilder-ai';
import {
DialogContext,
DialogTurnResult,
DialogTurnStatus } from 'botbuilder-dialogs';
import {
ISkillManifest,
SkillContext,
SkillDialog,
SkillRouter } from 'botbuilder-skills';
import {
ICognitiveModelSet,
InterruptionAction,
RouterDialog,
TokenEvents } from 'botbuilder-solutions';
import { TokenStatus } from 'botframework-connector';
import {
Activity,
ActivityTypes } from 'botframework-schema';
import i18next from 'i18next';
import { IOnboardingState } from '../models/onboardingState';
import { CancelResponses } from '../responses/cancelResponses';
import { MainResponses } from '../responses/mainResponses';
import { BotServices } from '../services/botServices';
import { IBotSettings } from '../services/botSettings';
import { CancelDialog } from './cancelDialog';
import { EscalateDialog } from './escalateDialog';
import { OnboardingDialog } from './onboardingDialog';
enum Events {
timeZoneEvent = 'va.timeZone',
locationEvent = 'va.location'
}
export class MainDialog extends RouterDialog {
// Fields
private readonly luisServiceGeneral: string = 'general';
private readonly luisServiceFaq: string = 'faq';
private readonly luisServiceChitchat: string = 'chitchat';
private readonly settings: Partial<IBotSettings>;
private readonly services: BotServices;
private readonly skillContextAccessor: StatePropertyAccessor<SkillContext>;
private readonly onboardingAccessor: StatePropertyAccessor<IOnboardingState>;
private readonly responder: MainResponses = new MainResponses();
// Constructor
public constructor(
settings: Partial<IBotSettings>,
services: BotServices,
onboardingDialog: OnboardingDialog,
escalateDialog: EscalateDialog,
cancelDialog: CancelDialog,
skillDialogs: SkillDialog[],
skillContextAccessor: StatePropertyAccessor<SkillContext>,
onboardingAccessor: StatePropertyAccessor<IOnboardingState>,
telemetryClient: BotTelemetryClient
) {
super(MainDialog.name, telemetryClient);
this.settings = settings;
this.services = services;
this.onboardingAccessor = onboardingAccessor;
this.skillContextAccessor = skillContextAccessor;
this.telemetryClient = telemetryClient;
this.addDialog(onboardingDialog);
this.addDialog(escalateDialog);
this.addDialog(cancelDialog);
skillDialogs.forEach((skillDialog: SkillDialog): void => {
this.addDialog(skillDialog);
});
}
protected async onStart(dc: DialogContext): Promise<void> {
const view: MainResponses = new MainResponses();
const onboardingState: IOnboardingState|undefined = await this.onboardingAccessor.get(dc.context);
if (onboardingState === undefined || onboardingState.name === undefined || onboardingState.name === '') {
await view.replyWith(dc.context, MainResponses.responseIds.newUserGreeting);
} else {
await view.replyWith(dc.context, MainResponses.responseIds.returningUserGreeting);
}
}
protected async route(dc: DialogContext): Promise<void> {
// Get cognitive models for locale
const locale: string = i18next.language.substring(0, 2);
const cognitiveModels: ICognitiveModelSet | undefined = this.services.cognitiveModelSets.get(locale);
if (cognitiveModels === undefined) {
throw new Error('There is no value in cognitiveModels');
}
// Check dispatch result
const dispatchResult: RecognizerResult = await cognitiveModels.dispatchService.recognize(dc.context);
const intent: string = LuisRecognizer.topIntent(dispatchResult);
if (this.settings.skills === undefined) {
throw new Error('There is no skills in settings value');
}
// Identify if the dispatch intent matches any Action within a Skill if so, we pass to the appropriate SkillDialog to hand-off
const identifiedSkill: ISkillManifest | undefined = SkillRouter.isSkill(this.settings.skills, intent);
if (identifiedSkill !== undefined) {
// We have identified a skill so initialize the skill connection with the target skill
const result: DialogTurnResult = await dc.beginDialog(identifiedSkill.id);
if (result.status === DialogTurnStatus.complete) {
await this.complete(dc);
}
} else if (intent === 'l_general') {
// If dispatch result is general luis model
const luisService: LuisRecognizerTelemetryClient | undefined = cognitiveModels.luisServices.get(this.luisServiceGeneral);
if (luisService === undefined) {
throw new Error('The specified LUIS Model could not be found in your Bot Services configuration.');
} else {
const result: RecognizerResult = await luisService.recognize(dc.context);
if (result !== undefined) {
const generalIntent: string = LuisRecognizer.topIntent(result);
// switch on general intents
switch (generalIntent) {
case 'Escalate': {
// start escalate dialog
await dc.beginDialog(EscalateDialog.name);
break;
}
case 'None':
default: {
// No intent was identified, send confused message
await this.responder.replyWith(dc.context, MainResponses.responseIds.confused);
}
}
}
}
} else if (intent === 'q_faq') {
const qnaService: QnAMakerTelemetryClient | undefined = cognitiveModels.qnaServices.get(this.luisServiceFaq);
if (qnaService === undefined) {
throw new Error('The specified QnA Maker Service could not be found in your Bot Services configuration.');
} else {
const answers: QnAMakerResult[] = await qnaService.getAnswers(dc.context);
if (answers !== undefined && answers.length > 0) {
await dc.context.sendActivity(answers[0].answer, answers[0].answer);
} else {
await this.responder.replyWith(dc.context, MainResponses.responseIds.confused);
}
}
} else if (intent === 'q_chitchat') {
const qnaService: QnAMakerTelemetryClient | undefined = cognitiveModels.qnaServices.get(this.luisServiceChitchat);
if (qnaService === undefined) {
throw new Error('The specified QnA Maker Service could not be found in your Bot Services configuration.');
} else {
const answers: QnAMakerResult[] = await qnaService.getAnswers(dc.context);
if (answers !== undefined && answers.length > 0) {
await dc.context.sendActivity(answers[0].answer, answers[0].answer);
} else {
await this.responder.replyWith(dc.context, MainResponses.responseIds.confused);
}
}
} else {
// If dispatch intent does not map to configured models, send 'confused' response.
await this.responder.replyWith(dc.context, MainResponses.responseIds.confused);
}
}
protected async onEvent(dc: DialogContext): Promise<void> {
// Check if there was an action submitted from intro card
if (dc.context.activity.value) {
// tslint:disable-next-line: no-unsafe-any
if (dc.context.activity.value.action === 'startOnboarding') {
await dc.beginDialog(OnboardingDialog.name);
return;
}
}
let forward: boolean = true;
const ev: Activity = dc.context.activity;
if (ev.name !== undefined && ev.name.trim().length > 0) {
switch (ev.name) {
case Events.timeZoneEvent: {
try {
const timezone: string = <string> ev.value;
const tz: string = new Date().toLocaleString(timezone);
const timeZoneObj: {
timezone: string;
} = {
timezone: tz
};
const skillContext: SkillContext = await this.skillContextAccessor.get(dc.context, new SkillContext());
skillContext.setObj(timezone, timeZoneObj);
await this.skillContextAccessor.set(dc.context, skillContext);
} catch {
await dc.context.sendActivity(
{
type: ActivityTypes.Trace,
text: `"Timezone passed could not be mapped to a valid Timezone. Property not set."`
}
);
}
forward = false;
break;
}
case Events.locationEvent: {
const location: string = <string> ev.value;
const locationObj: {
location: string;
} = {
location: location
};
const skillContext: SkillContext = await this.skillContextAccessor.get(dc.context, new SkillContext());
skillContext.setObj(location, locationObj);
await this.skillContextAccessor.set(dc.context, skillContext);
forward = true;
break;
}
case TokenEvents.tokenResponseEventName: {
forward = true;
break;
}
default: {
await dc.context.sendActivity(
{
type: ActivityTypes.Trace,
text: `"Unknown Event ${ ev.name } was received but not processed."`
}
);
forward = false;
}
}
}
if (forward) {
const result: DialogTurnResult = await dc.continueDialog();
if (result.status === DialogTurnStatus.complete) {
await this.complete(dc);
}
}
}
protected async complete(dc: DialogContext, result?: DialogTurnResult): Promise<void> {
// The active dialog's stack ended with a complete status
await this.responder.replyWith(dc.context, MainResponses.responseIds.completed);
}
protected async onInterruptDialog(dc: DialogContext): Promise<InterruptionAction> {
if (dc.context.activity.type === ActivityTypes.Message) {
const locale: string = i18next.language.substring(0, 2);
const cognitiveModels: ICognitiveModelSet | undefined = this.services.cognitiveModelSets.get(locale);
if (cognitiveModels === undefined) {
throw new Error('There is no cognitiveModels value');
}
// check luis intent
const luisService: LuisRecognizerTelemetryClient | undefined = cognitiveModels.luisServices.get(this.luisServiceGeneral);
if (luisService === undefined) {
throw new Error('The general LUIS Model could not be found in your Bot Services configuration.');
} else {
const luisResult: RecognizerResult = await luisService.recognize(dc.context);
const intent: string = LuisRecognizer.topIntent(luisResult);
// Only triggers interruption if confidence level is high
if (luisResult.intents[intent] !== undefined && luisResult.intents[intent].score > 0.5) {
switch (intent) {
case 'Cancel': {
return this.onCancel(dc);
}
case 'Help': {
return this.onHelp(dc);
}
case 'Logout': {
return this.onLogout(dc);
}
default:
}
}
}
}
return InterruptionAction.NoAction;
}
private async onCancel(dc: DialogContext): Promise<InterruptionAction> {
if (dc.activeDialog !== undefined && dc.activeDialog.id !== CancelDialog.name) {
// Don't start restart cancel dialog
await dc.beginDialog(CancelDialog.name);
// Signal that the dialog is waiting on user response
return InterruptionAction.StartedDialog;
}
const view: CancelResponses = new CancelResponses();
await view.replyWith(dc.context, CancelResponses.responseIds.nothingToCancelMessage);
return InterruptionAction.StartedDialog;
}
private async onHelp(dc: DialogContext): Promise<InterruptionAction> {
await this.responder.replyWith(dc.context, MainResponses.responseIds.help);
// Signal the conversation was interrupted and should immediately continue
return InterruptionAction.MessageSentToUser;
}
private async onLogout(dc: DialogContext): Promise<InterruptionAction> {
let adapter: BotFrameworkAdapter;
const supported: boolean = dc.context.adapter instanceof BotFrameworkAdapter;
if (!supported) {
throw new Error('OAuthPrompt.SignOutUser(): not supported by the current adapter');
} else {
adapter = <BotFrameworkAdapter> dc.context.adapter;
}
await dc.cancelAllDialogs();
// Sign out user
// PENDING check adapter.getTokenStatusAsync
const tokens: TokenStatus[] = [];
tokens.forEach(async (token: TokenStatus): Promise<void> => {
if (token.connectionName !== undefined) {
await adapter.signOutUser(dc.context, token.connectionName);
}
});
await dc.context.sendActivity(i18next.t('main.logOut'));
return InterruptionAction.StartedDialog;
}
}
=================================================================================onboardingDialog.ts
import {
BotTelemetryClient,
StatePropertyAccessor,
TurnContext } from 'botbuilder';
import {
ComponentDialog,
DialogTurnResult,
TextPrompt,
WaterfallDialog,
WaterfallStepContext } from 'botbuilder-dialogs';
import { IOnboardingState } from '../models/onboardingState';
import { OnboardingResponses } from '../responses/onboardingResponses';
import { BotServices } from '../services/botServices';
enum DialogIds {
namePrompt = 'namePrompt',
emailPrompt = 'emailPrompt',
locationPrompt = 'locationPrompt'
}
export class OnboardingDialog extends ComponentDialog {
// Fields
private static readonly responder: OnboardingResponses = new OnboardingResponses();
private readonly accessor: StatePropertyAccessor<IOnboardingState>;
private state!: IOnboardingState;
// Constructor
public constructor(botServices: BotServices, accessor: StatePropertyAccessor<IOnboardingState>, telemetryClient: BotTelemetryClient) {
super(OnboardingDialog.name);
this.accessor = accessor;
this.initialDialogId = OnboardingDialog.name;
const onboarding: ((sc: WaterfallStepContext<IOnboardingState>) => Promise<DialogTurnResult>)[] = [
this.askForName.bind(this),
this.finishOnboardingDialog.bind(this)
];
// To capture built-in waterfall dialog telemetry, set the telemetry client
// to the new waterfall dialog and add it to the component dialog
this.telemetryClient = telemetryClient;
this.addDialog(new WaterfallDialog(this.initialDialogId, onboarding));
this.addDialog(new TextPrompt(DialogIds.namePrompt));
}
public async askForName(sc: WaterfallStepContext<IOnboardingState>): Promise<DialogTurnResult> {
this.state = await this.getStateFromAccessor(sc.context);
if (this.state.name !== undefined && this.state.name.trim().length > 0) {
return sc.next(this.state.name);
}
return sc.prompt(DialogIds.namePrompt, {
prompt: await OnboardingDialog.responder.renderTemplate(
sc.context,
OnboardingResponses.responseIds.namePrompt,
<string> sc.context.activity.locale)
});
}
public async finishOnboardingDialog(sc: WaterfallStepContext<IOnboardingState>): Promise<DialogTurnResult> {
this.state = await this.getStateFromAccessor(sc.context);
this.state.name = <string> sc.result;
await this.accessor.set(sc.context, this.state);
await OnboardingDialog.responder.replyWith(
sc.context,
OnboardingResponses.responseIds.haveNameMessage,
{
name: this.state.name
});
return sc.endDialog();
}
private async getStateFromAccessor(context: TurnContext): Promise<IOnboardingState> {
const state: IOnboardingState | undefined = await this.accessor.get(context);
if (state === undefined) {
const newState: IOnboardingState = {
email: '',
location: '',
name: ''
};
await this.accessor.set(context, newState);
return newState;
}
return state;
}
}
=================================================================================dialogBot.ts
import {
ActivityHandler,
BotTelemetryClient,
ConversationState,
EndOfConversationCodes,
Severity,
TurnContext } from 'botbuilder';
import {
Dialog,
DialogContext,
DialogSet,
DialogState } from 'botbuilder-dialogs';
export class DialogBot<T extends Dialog> extends ActivityHandler {
private readonly telemetryClient: BotTelemetryClient;
private readonly solutionName: string = 'samplevirtualassistant';
private readonly rootDialogId: string;
private readonly dialogs: DialogSet;
public constructor(
conversationState: ConversationState,
telemetryClient: BotTelemetryClient,
dialog: T) {
super();
this.rootDialogId = dialog.id;
this.telemetryClient = telemetryClient;
this.dialogs = new DialogSet(conversationState.createProperty<DialogState>(this.solutionName));
this.dialogs.add(dialog);
this.onTurn(this.turn.bind(this));
}
// eslint-disable-next-line #typescript-eslint/no-explicit-any, #typescript-eslint/tslint/config
public async turn(turnContext: TurnContext, next: () => Promise<void>): Promise<any> {
// Client notifying this bot took to long to respond (timed out)
if (turnContext.activity.code === EndOfConversationCodes.BotTimedOut) {
this.telemetryClient.trackTrace({
message: `Timeout in ${ turnContext.activity.channelId } channel: Bot took too long to respond`,
severityLevel: Severity.Information
});
return;
}
const dc: DialogContext = await this.dialogs.createContext(turnContext);
if (dc.activeDialog !== undefined) {
await dc.continueDialog();
} else {
await dc.beginDialog(this.rootDialogId);
}
await next();
}
}

reconnect web socket if it is closed

My web socket connection code :
public connect(): Subject<MessageEvent> {
if (!this.subject) {
this.subject = this.create(this.url);
}
this.ws.onerror = () => {
this.close();
let refresh = setInterval(() => {
this.subject = null;
this.connect();
this.ws.onopen = () => {
clearInterval(refresh)
}
}, 5000);
}
return this.subject;
}
private create(url: string){
this.ws = new WebSocket(url);
const observable = Observable.create((obs: Subject<MessageEvent>) => {
this.ws.onmessage = obs.next.bind(obs);
this.ws.onerror = obs.error.bind(obs);
this.ws.onclose = obs.complete.bind(obs);
this.ws.onclose = function () {
console.log("trying to reconnect");
this.connect();
}
return this.ws.close.bind(this.ws);
});
const observer = {
next: (data: any) => {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
};
return Subject.create(observer, observable);
}
I want to reconnect web socket if connection closes. At the moment the function gets truggered when i stop the web socket. BUt is not connecting again .I See error "this.connect is not a function" .How to work with angular recursive functions?
Don't use function keyword to create your callback when using this inside of it if you aren't aware of how it changes the this reference depending on the execution context, use arrow function instead
To make it reconnect, change this
this.ws.onclose = function () {
console.log("trying to reconnect");
this.connect();
}
To this
this.ws.onclose = () => {
console.log("trying to reconnect");
this.subject = null;
this.connect();
}

rxjs first completes whole stream chain

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));
}
}

Rx.Observable.webSocket() immediately complete after reconnect?

Having a bit of trouble working with the Subject exposed by Rx.Observable.webSocket. While the WebSocket does become reconnected after complete, subsequent subscriptions to the Subject are immediately completed as well, instead of pushing the next messages that come over the socket.
I think I'm missing something fundamental about how this is supposed to work.
Here's a requirebin/paste that I hope illustrates a bit better what I mean, and the behavior I was expecting. Thinking it'll be something super simple I overlooked.
Requirebin
var Rx = require('rxjs')
var subject = Rx.Observable.webSocket('wss://echo.websocket.org')
subject.next(JSON.stringify('one'))
subject.subscribe(
function (msg) {
console.log('a', msg)
},
null,
function () {
console.log('a complete')
}
)
setTimeout(function () {
subject.complete()
}, 1000)
setTimeout(function () {
subject.next(JSON.stringify('two'))
}, 3000)
setTimeout(function () {
subject.next(JSON.stringify('three'))
subject.subscribe(
function (msg) {
// Was hoping to get 'two' and 'three'
console.log('b', msg)
},
null,
function () {
// Instead, we immediately get here.
console.log('b complete')
}
)
}, 5000)
Another neat solution would be to use a wrapper over WebSocketSubject.
class RxWebsocketSubject<T> extends Subject<T> {
private reconnectionObservable: Observable<number>;
private wsSubjectConfig: WebSocketSubjectConfig;
private socket: WebSocketSubject<any>;
private connectionObserver: Observer<boolean>;
public connectionStatus: Observable<boolean>;
defaultResultSelector = (e: MessageEvent) => {
return JSON.parse(e.data);
}
defaultSerializer = (data: any): string => {
return JSON.stringify(data);
}
constructor(
private url: string,
private reconnectInterval: number = 5000,
private reconnectAttempts: number = 10,
private resultSelector?: (e: MessageEvent) => any,
private serializer?: (data: any) => string,
) {
super();
this.connectionStatus = new Observable((observer) => {
this.connectionObserver = observer;
}).share().distinctUntilChanged();
if (!resultSelector) {
this.resultSelector = this.defaultResultSelector;
}
if (!this.serializer) {
this.serializer = this.defaultSerializer;
}
this.wsSubjectConfig = {
url: url,
closeObserver: {
next: (e: CloseEvent) => {
this.socket = null;
this.connectionObserver.next(false);
}
},
openObserver: {
next: (e: Event) => {
this.connectionObserver.next(true);
}
}
};
this.connect();
this.connectionStatus.subscribe((isConnected) => {
if (!this.reconnectionObservable && typeof(isConnected) == "boolean" && !isConnected) {
this.reconnect();
}
});
}
connect(): void {
this.socket = new WebSocketSubject(this.wsSubjectConfig);
this.socket.subscribe(
(m) => {
this.next(m);
},
(error: Event) => {
if (!this.socket) {
this.reconnect();
}
});
}
reconnect(): void {
this.reconnectionObservable = Observable.interval(this.reconnectInterval)
.takeWhile((v, index) => {
return index < this.reconnectAttempts && !this.socket
});
this.reconnectionObservable.subscribe(
() => {
this.connect();
},
null,
() => {
this.reconnectionObservable = null;
if (!this.socket) {
this.complete();
this.connectionObserver.complete();
}
});
}
send(data: any): void {
this.socket.next(this.serializer(data));
}
}
for more information refer to the following article and source code:
Auto WebSocket reconnection with RxJS
GitHub - Full working rxjs websocket example
I ended up not using Rx.Observable.webSocket, instead opting for observable-socket and a bit of code to make reconnections once sockets are closed:
requirebin
const observableSocket = require('observable-socket')
const Rx = require('rxjs')
const EventEmitter = require('events')
function makeObservableLoop (socketEmitter, send, receive) {
socketEmitter.once('open', function onSocketEmit (wSocket) {
const oSocket = observableSocket(wSocket)
const sendSubscription = send.subscribe(msg => oSocket.next(msg))
oSocket.subscribe(
function onNext (msg) {
receive.next(msg)
},
function onError (err) {
error(err)
sendSubscription.unsubscribe()
makeObservableLoop(socketEmitter, send, receive)
},
function onComplete () {
sendSubscription.unsubscribe()
makeObservableLoop(socketEmitter, send, receive)
}
)
})
}
function makeSocketLoop (emitter) {
const websocket = new WebSocket('wss://echo.websocket.org')
function onOpen () {
emitter.emit('open', websocket)
setTimeout(function () {
websocket.close()
}, 5000)
}
function onClose () {
makeSocketLoop(emitter)
}
websocket.onopen = onOpen
websocket.onclose = onClose
}
function init (socketEmitter) {
const _send = new Rx.Subject()
const _receive = new Rx.Subject()
makeObservableLoop(socketEmitter, _send, _receive)
const send = msg => _send.next(JSON.stringify(msg))
const receive = _receive.asObservable()
return {
send: send,
read: receive,
}
}
const emitter = new EventEmitter()
makeSocketLoop(emitter)
const theSubjectz = init(emitter)
setInterval(function () {
theSubjectz.send('echo, you there?')
}, 1000)
theSubjectz.read.subscribe(function (el) {
console.log(el)
})

Resources