|
import { Subject, AnonymousSubject } from '../../Subject'; |
|
import { Subscriber } from '../../Subscriber'; |
|
import { Observable } from '../../Observable'; |
|
import { Subscription } from '../../Subscription'; |
|
import { Operator } from '../../Operator'; |
|
import { ReplaySubject } from '../../ReplaySubject'; |
|
import { Observer, NextObserver } from '../../types'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export interface WebSocketSubjectConfig<T> { |
|
|
|
url: string; |
|
|
|
protocol?: string | Array<string>; |
|
|
|
resultSelector?: (e: MessageEvent) => T; |
|
|
|
|
|
|
|
|
|
serializer?: (value: T) => WebSocketMessage; |
|
|
|
|
|
|
|
|
|
deserializer?: (e: MessageEvent) => T; |
|
|
|
|
|
|
|
openObserver?: NextObserver<Event>; |
|
|
|
|
|
|
|
closeObserver?: NextObserver<CloseEvent>; |
|
|
|
|
|
|
|
|
|
closingObserver?: NextObserver<void>; |
|
|
|
|
|
|
|
|
|
|
|
WebSocketCtor?: { new (url: string, protocols?: string | string[]): WebSocket }; |
|
|
|
binaryType?: 'blob' | 'arraybuffer'; |
|
} |
|
|
|
const DEFAULT_WEBSOCKET_CONFIG: WebSocketSubjectConfig<any> = { |
|
url: '', |
|
deserializer: (e: MessageEvent) => JSON.parse(e.data), |
|
serializer: (value: any) => JSON.stringify(value), |
|
}; |
|
|
|
const WEBSOCKETSUBJECT_INVALID_ERROR_OBJECT = |
|
'WebSocketSubject.error must be called with an object with an error code, and an optional reason: { code: number, reason: string }'; |
|
|
|
export type WebSocketMessage = string | ArrayBuffer | Blob | ArrayBufferView; |
|
|
|
export class WebSocketSubject<T> extends AnonymousSubject<T> { |
|
|
|
private _config: WebSocketSubjectConfig<T>; |
|
|
|
|
|
|
|
_output: Subject<T>; |
|
|
|
private _socket: WebSocket | null = null; |
|
|
|
constructor(urlConfigOrSource: string | WebSocketSubjectConfig<T> | Observable<T>, destination?: Observer<T>) { |
|
super(); |
|
if (urlConfigOrSource instanceof Observable) { |
|
this.destination = destination; |
|
this.source = urlConfigOrSource as Observable<T>; |
|
} else { |
|
const config = (this._config = { ...DEFAULT_WEBSOCKET_CONFIG }); |
|
this._output = new Subject<T>(); |
|
if (typeof urlConfigOrSource === 'string') { |
|
config.url = urlConfigOrSource; |
|
} else { |
|
for (const key in urlConfigOrSource) { |
|
if (urlConfigOrSource.hasOwnProperty(key)) { |
|
(config as any)[key] = (urlConfigOrSource as any)[key]; |
|
} |
|
} |
|
} |
|
|
|
if (!config.WebSocketCtor && WebSocket) { |
|
config.WebSocketCtor = WebSocket; |
|
} else if (!config.WebSocketCtor) { |
|
throw new Error('no WebSocket constructor can be found'); |
|
} |
|
this.destination = new ReplaySubject(); |
|
} |
|
} |
|
|
|
|
|
lift<R>(operator: Operator<T, R>): WebSocketSubject<R> { |
|
const sock = new WebSocketSubject<R>(this._config as WebSocketSubjectConfig<any>, this.destination as any); |
|
sock.operator = operator; |
|
sock.source = this; |
|
return sock; |
|
} |
|
|
|
private _resetState() { |
|
this._socket = null; |
|
if (!this.source) { |
|
this.destination = new ReplaySubject(); |
|
} |
|
this._output = new Subject<T>(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
multiplex(subMsg: () => any, unsubMsg: () => any, messageFilter: (value: T) => boolean) { |
|
const self = this; |
|
return new Observable((observer: Observer<T>) => { |
|
try { |
|
self.next(subMsg()); |
|
} catch (err) { |
|
observer.error(err); |
|
} |
|
|
|
const subscription = self.subscribe({ |
|
next: (x) => { |
|
try { |
|
if (messageFilter(x)) { |
|
observer.next(x); |
|
} |
|
} catch (err) { |
|
observer.error(err); |
|
} |
|
}, |
|
error: (err) => observer.error(err), |
|
complete: () => observer.complete(), |
|
}); |
|
|
|
return () => { |
|
try { |
|
self.next(unsubMsg()); |
|
} catch (err) { |
|
observer.error(err); |
|
} |
|
subscription.unsubscribe(); |
|
}; |
|
}); |
|
} |
|
|
|
private _connectSocket() { |
|
const { WebSocketCtor, protocol, url, binaryType } = this._config; |
|
const observer = this._output; |
|
|
|
let socket: WebSocket | null = null; |
|
try { |
|
socket = protocol ? new WebSocketCtor!(url, protocol) : new WebSocketCtor!(url); |
|
this._socket = socket; |
|
if (binaryType) { |
|
this._socket.binaryType = binaryType; |
|
} |
|
} catch (e) { |
|
observer.error(e); |
|
return; |
|
} |
|
|
|
const subscription = new Subscription(() => { |
|
this._socket = null; |
|
if (socket && socket.readyState === 1) { |
|
socket.close(); |
|
} |
|
}); |
|
|
|
socket.onopen = (evt: Event) => { |
|
const { _socket } = this; |
|
if (!_socket) { |
|
socket!.close(); |
|
this._resetState(); |
|
return; |
|
} |
|
const { openObserver } = this._config; |
|
if (openObserver) { |
|
openObserver.next(evt); |
|
} |
|
|
|
const queue = this.destination; |
|
|
|
this.destination = Subscriber.create<T>( |
|
(x) => { |
|
if (socket!.readyState === 1) { |
|
try { |
|
const { serializer } = this._config; |
|
socket!.send(serializer!(x!)); |
|
} catch (e) { |
|
this.destination!.error(e); |
|
} |
|
} |
|
}, |
|
(err) => { |
|
const { closingObserver } = this._config; |
|
if (closingObserver) { |
|
closingObserver.next(undefined); |
|
} |
|
if (err && err.code) { |
|
socket!.close(err.code, err.reason); |
|
} else { |
|
observer.error(new TypeError(WEBSOCKETSUBJECT_INVALID_ERROR_OBJECT)); |
|
} |
|
this._resetState(); |
|
}, |
|
() => { |
|
const { closingObserver } = this._config; |
|
if (closingObserver) { |
|
closingObserver.next(undefined); |
|
} |
|
socket!.close(); |
|
this._resetState(); |
|
} |
|
) as Subscriber<any>; |
|
|
|
if (queue && queue instanceof ReplaySubject) { |
|
subscription.add((queue as ReplaySubject<T>).subscribe(this.destination)); |
|
} |
|
}; |
|
|
|
socket.onerror = (e: Event) => { |
|
this._resetState(); |
|
observer.error(e); |
|
}; |
|
|
|
socket.onclose = (e: CloseEvent) => { |
|
if (socket === this._socket) { |
|
this._resetState(); |
|
} |
|
const { closeObserver } = this._config; |
|
if (closeObserver) { |
|
closeObserver.next(e); |
|
} |
|
if (e.wasClean) { |
|
observer.complete(); |
|
} else { |
|
observer.error(e); |
|
} |
|
}; |
|
|
|
socket.onmessage = (e: MessageEvent) => { |
|
try { |
|
const { deserializer } = this._config; |
|
observer.next(deserializer!(e)); |
|
} catch (err) { |
|
observer.error(err); |
|
} |
|
}; |
|
} |
|
|
|
|
|
protected _subscribe(subscriber: Subscriber<T>): Subscription { |
|
const { source } = this; |
|
if (source) { |
|
return source.subscribe(subscriber); |
|
} |
|
if (!this._socket) { |
|
this._connectSocket(); |
|
} |
|
this._output.subscribe(subscriber); |
|
subscriber.add(() => { |
|
const { _socket } = this; |
|
if (this._output.observers.length === 0) { |
|
if (_socket && (_socket.readyState === 1 || _socket.readyState === 0)) { |
|
_socket.close(); |
|
} |
|
this._resetState(); |
|
} |
|
}); |
|
return subscriber; |
|
} |
|
|
|
unsubscribe() { |
|
const { _socket } = this; |
|
if (_socket && (_socket.readyState === 1 || _socket.readyState === 0)) { |
|
_socket.close(); |
|
} |
|
this._resetState(); |
|
super.unsubscribe(); |
|
} |
|
} |
|
|