-
Notifications
You must be signed in to change notification settings - Fork 16
/
neffos.ts
1371 lines (1141 loc) · 44.1 KB
/
neffos.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Make it compatible to run with browser and inside nodejs
// the good thing is that the node's WebSocket module has the same API as the browser's one,
// so all works and minimum changes were required to achieve that result.
// See the `genWait()` too.
const isBrowser = (typeof window !== 'undefined');
var _fetch = (typeof fetch !== 'undefined') ? fetch : undefined;
if (!isBrowser) {
WebSocket = require('ws');
_fetch = require('node-fetch') as (input: RequestInfo, init?: RequestInit) => Promise<Response>;
} else {
WebSocket = window["WebSocket"];
}
/* The WSData is just a string type alias. */
type WSData = string;
/* The OnNamespaceConnect is the event name that it's fired on before namespace connect. */
const OnNamespaceConnect = "_OnNamespaceConnect";
/* The OnNamespaceConnected is the event name that it's fired on after namespace connect. */
const OnNamespaceConnected = "_OnNamespaceConnected";
/* The OnNamespaceDisconnect is the event name that it's fired on namespace disconnected. */
const OnNamespaceDisconnect = "_OnNamespaceDisconnect";
/* The OnRoomJoin is the event name that it's fired on before room join. */
const OnRoomJoin = "_OnRoomJoin";
/* The OnRoomJoined is the event name that it's fired on after room join. */
const OnRoomJoined = "_OnRoomJoined";
/* The OnRoomLeave is the event name that it's fired on before room leave. */
const OnRoomLeave = "_OnRoomLeave";
/* The OnRoomLeft is the event name that it's fired on after room leave. */
const OnRoomLeft = "_OnRoomLeft";
/* The OnAnyEvent is the event name that it's fired, if no incoming event was registered, it's a "wilcard". */
const OnAnyEvent = "_OnAnyEvent";
/* The OnNativeMessage is the event name, which if registered on empty ("") namespace
it accepts native messages(Message.Body and Message.IsNative is filled only). */
const OnNativeMessage = "_OnNativeMessage";
const ackBinary = 'M'; // see `onopen`, comes from client to server at startup.
// see `handleAck`.
const ackIDBinary = 'A';// comes from server to client after ackBinary and ready as a prefix, the rest message is the conn's ID.
const ackNotOKBinary = 'H'; // comes from server to client if `Server#OnConnected` errored as a prefix, the rest message is the error text.
const waitIsConfirmationPrefix = '#';
const waitComesFromClientPrefix = '$';
/* The isSystemEvent reports whether the "event" is a system event;
connect, connected, disconnect, room join, room joined, room leave, room left. */
function isSystemEvent(event: string): boolean {
switch (event) {
case OnNamespaceConnect:
case OnNamespaceConnected:
case OnNamespaceDisconnect:
case OnRoomJoin:
case OnRoomJoined:
case OnRoomLeave:
case OnRoomLeft:
return true;
default:
return false;
}
}
function isEmpty(s: any): boolean {
if (s === undefined) {
return true
}
if (s === null) {
return true
}
if (s == "" || typeof s === 'string' || s instanceof String) {
return s.length === 0 || s === "";
}
if (s instanceof Error) {
return isEmpty(s.message);
}
return false;
}
/* The Message is the structure which describes the icoming data (and if `Conn.Write` is manually used to write). */
class Message {
wait: string;
/* The Namespace that this message sent to. */
Namespace: string;
/* The Room that this message sent to. */
Room: string;
/* The Event that this message sent to. */
Event: string;
/* The actual body of the incoming data. */
Body: WSData;
/* The Err contains any message's error if defined and not empty.
server-side and client-side can return an error instead of a message from inside event callbacks. */
Err: string;
isError: boolean;
isNoOp: boolean;
isInvalid: boolean;
/* The IsForced if true then it means that this is not an incoming action but a force action.
For example when websocket connection lost from remote the OnNamespaceDisconnect `Message.IsForced` will be true */
IsForced: boolean;
/* The IsLocal reprots whether an event is sent by the client-side itself, i.e when `connect` call on `OnNamespaceConnect` event the `Message.IsLocal` will be true,
server-side can force-connect a client to connect to a namespace as well in this case the `IsLocal` will be false. */
IsLocal: boolean;
/* The IsNative reports whether the Message is websocket native messages, only Body is filled. */
IsNative: boolean;
/* The SetBinary can be filled to true if the client must send this message using the Binary format message.
This field is not filled on sending/receiving. */
// SetBinary: boolean;
isConnect(): boolean {
return this.Event == OnNamespaceConnect || false;
}
isDisconnect(): boolean {
return this.Event == OnNamespaceDisconnect || false;
}
isRoomJoin(): boolean {
return this.Event == OnRoomJoin || false;
}
isRoomLeft(): boolean {
return this.Event == OnRoomLeft || false;
}
isWait(): boolean {
if (isEmpty(this.wait)) {
return false;
}
if (this.wait[0] == waitIsConfirmationPrefix) {
return true;
}
return this.wait[0] == waitComesFromClientPrefix || false;
}
/* unmarshal method returns this Message's `Body` as an object,
equivalent to the Go's `neffos.Message.Unmarshal` method.
It can be used inside an event's callbacks.
See library-level `marshal` function too. */
unmarshal(): any {
return JSON.parse(this.Body);
}
}
/* marshal takes an object and returns its serialized to string form, equivalent to the Go's `neffos.Marshal`.
It can be used on `emit` methods.
See `Message.unmarshal` method too. */
function marshal(obj: any): string {
return JSON.stringify(obj);
}
/* Obsiously, the below should match the server's side. */
const messageSeparator = ';';
const messageFieldSeparatorReplacement = "@%!semicolon@%!";
const validMessageSepCount = 7;
const trueString = "1";
const falseString = "0";
const escapeRegExp = new RegExp(messageSeparator, "g");
function escapeMessageField(s: string): string {
if (isEmpty(s)) {
return "";
}
return s.replace(escapeRegExp, messageFieldSeparatorReplacement);
}
const unescapeRegExp = new RegExp(messageFieldSeparatorReplacement, "g");
function unescapeMessageField(s: string): string {
if (isEmpty(s)) {
return "";
}
return s.replace(unescapeRegExp, messageSeparator);
}
function serializeMessage(msg: Message): WSData {
if (msg.IsNative && isEmpty(msg.wait)) {
return msg.Body;
}
let isErrorString = falseString;
let isNoOpString = falseString;
let body = msg.Body || "";
if (msg.isError) {
body = msg.Err;
isErrorString = trueString;
}
if (msg.isNoOp) {
isNoOpString = trueString;
}
return [
msg.wait || "",
escapeMessageField(msg.Namespace),
escapeMessageField(msg.Room),
escapeMessageField(msg.Event),
isErrorString,
isNoOpString,
body].join(messageSeparator);
}
// behaves like Go's SplitN, default javascript's does not return the remainder and we need this for the dts[6]
function splitN(s: string, sep: string, limit: number): Array<string> {
if (limit == 0) return [s];
var arr = s.split(sep, limit);
if (arr.length == limit) {
let curr = arr.join(sep) + sep;
arr.push(s.substr(curr.length));
return arr;
} else {
return [s];
}
}
// <wait>;
// <namespace>;
// <room>;
// <event>;
// <isError(0-1)>;
// <isNoOp(0-1)>;
// <body||error_message>
function deserializeMessage(data: WSData, allowNativeMessages: boolean): Message {
var msg: Message = new Message();
if (data.length == 0) {
msg.isInvalid = true;
return msg;
}
let dts = splitN(data, messageSeparator, validMessageSepCount - 1);
if (dts.length != validMessageSepCount) {
if (!allowNativeMessages) {
msg.isInvalid = true;
} else {
msg.Event = OnNativeMessage;
msg.Body = data;
}
return msg;
}
msg.wait = dts[0];
msg.Namespace = unescapeMessageField(dts[1]);
msg.Room = unescapeMessageField(dts[2]);
msg.Event = unescapeMessageField(dts[3]);
msg.isError = dts[4] == trueString || false;
msg.isNoOp = dts[5] == trueString || false;
let body = dts[6];
if (!isEmpty(body)) {
if (msg.isError) {
msg.Err = body;
} else {
msg.Body = body;
}
} else {
msg.Body = "";
}
msg.isInvalid = false;
msg.IsForced = false;
msg.IsLocal = false;
msg.IsNative = (allowNativeMessages && msg.Event == OnNativeMessage) || false;
// msg.SetBinary = false;
return msg;
}
function genWait(): string {
if (!isBrowser) {
let hrTime = process.hrtime();
return waitComesFromClientPrefix + hrTime[0] * 1000000000 + hrTime[1];
} else {
let now = window.performance.now();
return waitComesFromClientPrefix + now.toString();
}
}
function genWaitConfirmation(wait: string): string {
return waitIsConfirmationPrefix + wait;
}
function genEmptyReplyToWait(wait: string): string {
return wait + messageSeparator.repeat(validMessageSepCount - 1);
}
/* The Room describes a connected connection to a room,
emits messages with the `Message.Room` filled to the specific room
and `Message.Namespace` to the underline `NSConn`'s namespace. */
class Room {
nsConn: NSConn;
name: string;
constructor(ns: NSConn, roomName: string) {
this.nsConn = ns;
this.name = roomName;
}
/* The emit method sends a message to the server with its `Message.Room` filled to this specific room
and `Message.Namespace` to the underline `NSConn`'s namespace. */
emit(event: string, body: WSData): boolean {
let msg = new Message();
msg.Namespace = this.nsConn.namespace;
msg.Room = this.name;
msg.Event = event;
msg.Body = body;
return this.nsConn.conn.write(msg);
}
/* The leave method sends a local and server room leave signal `OnRoomLeave`
and if succeed it fires the OnRoomLeft` event. */
leave(): Promise<Error> {
let msg = new Message();
msg.Namespace = this.nsConn.namespace;
msg.Room = this.name;
msg.Event = OnRoomLeave;
return this.nsConn.askRoomLeave(msg);
}
}
/* The NSConn describes a connected connection to a specific namespace,
it emits with the `Message.Namespace` filled and it can join to multiple rooms.
A single Conn can be connected to one or more namespaces,
each connected namespace is described by this class. */
class NSConn {
/* The conn property refers to the main `Conn` constructed by the `dial` function. */
conn: Conn;
namespace: string;
events: Events;
/* The rooms property its the map of the connected namespace's joined rooms. */
rooms: Map<string, Room>;
constructor(conn: Conn, namespace: string, events: Events) {
this.conn = conn;
this.namespace = namespace;
this.events = events;
this.rooms = new Map<string, Room>();
}
/* The emit method sends a message to the server with its `Message.Namespace` filled to this specific namespace. */
emit(event: string, body: WSData): boolean {
let msg = new Message();
msg.Namespace = this.namespace;
msg.Event = event;
msg.Body = body;
return this.conn.write(msg);
}
/* See `Conn.ask`. */
ask(event: string, body: WSData): Promise<Message> {
let msg = new Message();
msg.Namespace = this.namespace;
msg.Event = event;
msg.Body = body;
return this.conn.ask(msg);
}
/* The joinRoom method can be used to join to a specific room, rooms are dynamic.
Returns a `Room` or an error. */
async joinRoom(roomName: string): Promise<Room> {
return await this.askRoomJoin(roomName);
}
/* The room method returns a joined `Room`. */
room(roomName: string): Room {
return this.rooms.get(roomName);
}
// Rooms(): Room[] {
// let rooms = new Array<Room>(this.rooms.size);
// this.rooms.forEach((room) => {
// rooms.push(room);
// })
// return rooms;
// }
/* The leaveAll method sends a leave room signal to all rooms and fires the `OnRoomLeave` and `OnRoomLeft` (if no error occurred) events. */
async leaveAll(): Promise<Error> {
let leaveMsg = new Message();
leaveMsg.Namespace = this.namespace;
leaveMsg.Event = OnRoomLeft;
leaveMsg.IsLocal = true;
this.rooms.forEach(async (value, roomName) => {
leaveMsg.Room = roomName;
try {
await this.askRoomLeave(leaveMsg);
} catch (err) {
return err;
}
})
return null;
}
forceLeaveAll(isLocal: boolean) {
let leaveMsg = new Message();
leaveMsg.Namespace = this.namespace;
leaveMsg.Event = OnRoomLeave;
leaveMsg.IsForced = true;
leaveMsg.IsLocal = isLocal;
this.rooms.forEach((value, roomName) => {
leaveMsg.Room = roomName;
fireEvent(this, leaveMsg);
this.rooms.delete(roomName);
leaveMsg.Event = OnRoomLeft;
fireEvent(this, leaveMsg);
leaveMsg.Event = OnRoomLeave;
});
}
/* The disconnect method sends a disconnect signal to the server and fires the `OnNamespaceDisconnect` event. */
disconnect(): Promise<Error> {
let disconnectMsg = new Message();
disconnectMsg.Namespace = this.namespace;
disconnectMsg.Event = OnNamespaceDisconnect;
return this.conn.askDisconnect(disconnectMsg);
}
askRoomJoin(roomName: string): Promise<Room> {
return new Promise(async (resolve, reject) => {
let room = this.rooms.get(roomName);
if (room !== undefined) {
resolve(room);
return;
}
let joinMsg = new Message();
joinMsg.Namespace = this.namespace;
joinMsg.Room = roomName;
joinMsg.Event = OnRoomJoin;
joinMsg.IsLocal = true;
try {
await this.conn.ask(joinMsg);
} catch (err) {
reject(err);
return;
}
let err = fireEvent(this, joinMsg);
if (!isEmpty(err)) {
reject(err);
return;
}
room = new Room(this, roomName);
this.rooms.set(roomName, room);
joinMsg.Event = OnRoomJoined;
fireEvent(this, joinMsg);
resolve(room);
});
}
async askRoomLeave(msg: Message): Promise<Error> {
if (!this.rooms.has(msg.Room)) {
return ErrBadRoom;
}
try {
await this.conn.ask(msg)
} catch (err) {
return err;
}
let err = fireEvent(this, msg);
if (!isEmpty(err)) {
return err;
}
this.rooms.delete(msg.Room);
msg.Event = OnRoomLeft;
fireEvent(this, msg);
return null;
}
replyRoomJoin(msg: Message): void {
if (isEmpty(msg.wait) || msg.isNoOp) {
return;
}
if (!this.rooms.has(msg.Room)) {
let err = fireEvent(this, msg);
if (!isEmpty(err)) {
msg.Err = err.message;
this.conn.write(msg);
return;
}
this.rooms.set(msg.Room, new Room(this, msg.Room));
msg.Event = OnRoomJoined;
fireEvent(this, msg);
}
this.conn.writeEmptyReply(msg.wait);
}
replyRoomLeave(msg: Message): void {
if (isEmpty(msg.wait) || msg.isNoOp) {
return;
}
if (!this.rooms.has(msg.Room)) {
this.conn.writeEmptyReply(msg.wait);
return;
}
fireEvent(this, msg);
this.rooms.delete(msg.Room);
this.conn.writeEmptyReply(msg.wait);
msg.Event = OnRoomLeft;
fireEvent(this, msg);
}
}
/* The MessageHandlerFunc is the definition type of the events' callback.
Its error can be written to the other side on specific events,
i.e on `OnNamespaceConnect` it will abort a remote namespace connection.
See examples for more. */
type MessageHandlerFunc = (c: NSConn, msg: Message) => Error;
type Events = Map<string, MessageHandlerFunc>
type Namespaces = Map<string, Events>;
function fireEvent(ns: NSConn, msg: Message): Error {
if (ns.events.has(msg.Event)) {
return ns.events.get(msg.Event)(ns, msg);
}
if (ns.events.has(OnAnyEvent)) {
return ns.events.get(OnAnyEvent)(ns, msg);
}
return null;
}
function isNull(obj: any): boolean {
return (obj === null || obj === undefined || typeof obj === 'undefined')
}
function resolveNamespaces(obj: any, reject: (reason?: any) => void): Namespaces {
if (isNull(obj)) {
if (!isNull(reject)) {
reject("connHandler is empty.");
}
return null;
}
let namespaces = new Map<string, Map<string, MessageHandlerFunc>>();
// 1. if contains function instead of a string key then it's Events otherwise it's Namespaces.
// 2. if contains a mix of functions and keys then ~put those functions to the namespaces[""]~ it is NOT valid.
let events: Events = new Map<string, MessageHandlerFunc>();
// const isMessageHandlerFunc = (value: any): value is MessageHandlerFunc => true;
let totalKeys: number = 0;
Object.keys(obj).forEach(function (key, index) {
totalKeys++;
let value = obj[key];
// if (isMessageHandlerFunc(value)) {
if (value instanceof Function) {
// console.log(key + " event probably contains a message handler: ", value)
events.set(key, value)
} else if (value instanceof Map) {
// console.log(key + " is a namespace map which contains the following events: ", value)
namespaces.set(key, value);
} else {
// it's an object, convert it to a map, it's events.
// console.log(key + " is an object of: ", value);
let objEvents = new Map<string, MessageHandlerFunc>();
Object.keys(value).forEach(function (objKey, objIndex) {
// console.log("set event: " + objKey + " of value: ", value[objKey])
objEvents.set(objKey, value[objKey]);
});
namespaces.set(key, objEvents)
}
});
if (events.size > 0) {
if (totalKeys != events.size) {
if (!isNull(reject)) {
reject("all keys of connHandler should be events, mix of namespaces and event callbacks is not supported " + events.size + " vs total " + totalKeys);
}
return null;
}
namespaces.set("", events);
}
// console.log(namespaces);
return namespaces;
}
function getEvents(namespaces: Namespaces, namespace: string): Events {
if (namespaces.has(namespace)) {
return namespaces.get(namespace);
}
return null;
}
type waitingMessageFunc = (msg: Message) => void;
/* This is the prefix that Options.header function is set to a url parameter's key in order to serve to parse it as header.
The server's `URLParamAsHeaderPrefix` must match.
Note that on the Nodejs side this is entirely optional, nodejs and go client support custom headers without url parameters parsing. */
const URLParamAsHeaderPrefix = "X-Websocket-Header-"
interface Headers {
[key: string]: any;
}
/* Options contains optional fields. Can be passed on the `dial` function. */
interface Options {
// If nodejs then let it pass as it's;
// As CoreOptions (from nodejs request module):
// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/request/index.d.ts#L143
headers?: Headers;
//
protocols?: string[];
// if > 0 then it enables reconnection feature
// and it tries every "x" milliseconds of this `reconnect` field.
reconnect?: number;
}
function parseHeadersAsURLParameters(headers: Headers, url: string): string {
if (isNull(headers)) {
return url;
}
for (let key in headers) {
if (headers.hasOwnProperty(key)) {
let value = headers[key];
key = encodeURIComponent(URLParamAsHeaderPrefix + key); value = encodeURIComponent(value);
const part = key + "=" + value;
url = (url.indexOf("?") != -1 ?
url.split("?")[0] + "?" + part + "&" + url.split("?")[1] :
(url.indexOf("#") != -1 ? url.split("#")[0] + "?" + part + "#" + url.split("#")[1] : url + '?' + part));
}
}
return url;
}
/* The dial function returns a neffos client, a new `Conn` instance.
First parameter is the endpoint, i.e ws://localhost:8080/echo,
the second parameter can be any object of the form of:
namespace: {eventName: eventHandler, eventName2: ...} or {eventName: eventHandler}.
Example Code:
var conn = await neffos.dial("ws://localhost:8080/echo", {
default: { // "default" namespace.
_OnNamespaceConnected: function (ns, msg) {
console.log("connected to namespace: " + msg.Namespace);
},
_OnNamespaceDisconnect: function (ns, msg) {
console.log("disconnected from namespace: " + msg.Namespace);
},
_OnRoomJoined: function (ns, msg) {
console.log("joined to room: " + msg.Room);
},
_OnRoomLeft: function (ns, msg) {
console.log("left from room: " + msg.Room);
},
chat: function (ns, msg) { // "chat" event.
let prefix = "Server says: ";
if (msg.Room !== "") {
prefix = msg.Room + " >> ";
}
console.log(prefix + msg.Body);
}
}
});
var nsConn = await conn.connect("default");
nsConn.emit("chat", "Hello from client side!");
See https://github.com/kataras/neffos.js/tree/master/_examples for more.
*/
function dial(endpoint: string, connHandler: any, options?: Options | any): Promise<Conn> {
return _dial(endpoint, connHandler, 0, options);
}
// this header key should match the server.ServeHTTP's.
const websocketReconnectHeaderKey = 'X-Websocket-Reconnect';
function _dial(endpoint: string, connHandler: any, tries: number, options?: Options | any): Promise<Conn> {
if (endpoint.indexOf("ws") == -1) {
endpoint = "ws://" + endpoint;
}
return new Promise((resolve, reject) => {
if (!WebSocket) {
reject("WebSocket is not accessible through this browser.");
}
let namespaces = resolveNamespaces(connHandler, reject);
if (isNull(namespaces)) {
return;
}
if (isNull(options)) {
options = {};
}
if (isNull(options.headers)) {
options.headers = {};
}
const reconnectEvery: number = (options.reconnect) ? options.reconnect : 0;
if (tries > 0 && reconnectEvery > 0) {
// options.headers = {
// [websocketReconnectHeaderKey]: tries.toString()
// };
options.headers[websocketReconnectHeaderKey] = tries.toString();
} else if (!isNull(options.headers[websocketReconnectHeaderKey])) /* against tricks */ {
delete options.headers[websocketReconnectHeaderKey];
}
const ws = makeWebsocketConnection(endpoint, options)
let conn = new Conn(ws, namespaces);
conn.reconnectTries = tries;
ws.binaryType = "arraybuffer";
ws.onmessage = ((evt: MessageEvent) => {
let err = conn.handle(evt);
if (!isEmpty(err)) {
reject(err);
return;
}
if (conn.isAcknowledged()) {
resolve(conn);
}
});
ws.onopen = ((evt: Event) => {
// let b = new Uint8Array(1)
// b[0] = 1;
// this.conn.send(b.buffer);
ws.send(ackBinary);
});
ws.onerror = ((err: Event) => {
// if (err.type !== undefined && err.type == "error" && (ws.readyState == ws.CLOSED || ws.readyState == ws.CLOSING)) {
// // for any case, it should never happen.
// return;
// }
conn.close();
reject(err);
});
ws.onclose = ((evt: Event): any => {
if (conn.isClosed()) {
// reconnection is NOT allowed when:
// 1. server force-disconnect this client.
// 2. client disconnects itself manually.
// We check those two ^ with conn.isClosed().
// console.log("manual disconnect.")
} else {
// disable all previous event callbacks.
ws.onmessage = undefined;
ws.onopen = undefined;
ws.onerror = undefined;
ws.onclose = undefined;
if (reconnectEvery <= 0) {
conn.close();
return null;
}
// get the connected namespaces before .close clears.
let previouslyConnectedNamespacesNamesOnly = new Map<string, Array<string>>() // connected namespaces[key] -> [values]joined rooms;
conn.connectedNamespaces.forEach((nsConn: NSConn, name: string) => {
let previouslyJoinedRooms = new Array<string>();
if (!isNull(nsConn.rooms) && nsConn.rooms.size > 0) {
nsConn.rooms.forEach((roomConn, roomName) => {
previouslyJoinedRooms.push(roomName);
});
}
previouslyConnectedNamespacesNamesOnly.set(name, previouslyJoinedRooms);
})
conn.close();
whenResourceOnline(endpoint, reconnectEvery, (tries: number) => {
_dial(endpoint, connHandler, tries, options).then((newConn: Conn) => {
if (isNull(resolve) || resolve.toString() == "function () { [native code] }") {
// Idea behind the below:
// If the original promise was in try-catch statement instead of .then and .catch callbacks
// then this block will be called however, we don't have a way
// to guess the user's actions in a try block, so we at least,
// we will try to reconnect to the previous namespaces automatically here.
previouslyConnectedNamespacesNamesOnly.forEach((joinedRooms, name) => {
let whenConnected = (joinedRooms: string[]): ((newNSConn: NSConn) => void) => {
return (newNSConn: NSConn) => {
joinedRooms.forEach((roomName: string) => {
newNSConn.joinRoom(roomName);
});
};
};
newConn.connect(name).then(whenConnected(joinedRooms));
})
return;
}
resolve(newConn);
}).catch(reject);
});
}
return null;
});
});
}
function makeWebsocketConnection(endpoint: string, options?: Options | any) {
if (isBrowser) {
if (!isNull(options)) {
if (options.headers) {
endpoint = parseHeadersAsURLParameters(options.headers, endpoint);
}
if (options.protocols) {
return new WebSocket(endpoint, options.protocols);
} else {
return new WebSocket(endpoint)
}
}
}
return new WebSocket(endpoint, options)
}
function whenResourceOnline(endpoint: string, checkEvery: number, notifyOnline: (tries: number) => void) {
// Don't fire webscoket requests just yet.
// We check if the HTTP endpoint is alive with a simple fetch, if it is alive then we notify the caller
// to proceed with a websocket request. That way we can notify the server-side how many times
// this client was trying to reconnect as well.
// Note:
// Chrome itself is emitting net::ERR_CONNECTION_REFUSED and the final Bad Request messages to the console on network failures on fetch,
// there is no way to block them programmatically, we could do a console.clear but this will clear any custom logging the end-dev may has too.
let endpointHTTP = endpoint.replace(/(ws)(s)?\:\/\//, "http$2://");
// counts and sends as header the previous failures (if any) and the succeed last one.
let tries = 1;
const fetchOptions = { method: 'HEAD' };
let check = (): void => {
// Note:
// We do not fire a try immediately after the disconnection as most developers will expect.
_fetch(endpointHTTP, fetchOptions).then(() => {
notifyOnline(tries);
}).catch(() => { // on network failures.
// if (err !== undefined && err.toString() !== "TypeError: Failed to fetch") {
// console.log(err);
// }
tries++;
setTimeout(() => {
check();
}, checkEvery)
});
};
setTimeout(check, checkEvery)
}
const ErrInvalidPayload = new Error("invalid payload");
const ErrBadNamespace = new Error("bad namespace");
const ErrBadRoom = new Error("bad room");
const ErrClosed = new Error("use of closed connection");
const ErrWrite = new Error("write closed");
/* The Conn class contains the websocket connection and the neffos communication functionality.
Its `connect` will return a new `NSConn` instance, each connection can connect to one or more namespaces.
Each `NSConn` can join to multiple rooms. */
class Conn {
private conn: WebSocket;
/* If > 0 then this connection is the result of a reconnection,
see `wasReconnected()` too. */
reconnectTries: number;
private _isAcknowledged: boolean;
private allowNativeMessages: boolean;
/* ID is the generated connection ID from the server-side, all connected namespaces(`NSConn` instances)
that belong to that connection have the same ID. It is available immediately after the `dial`. */
ID: string;
private closed: boolean;
private waitServerConnectNotifiers: Map<string, () => void>;
private queue: WSData[];
private waitingMessages: Map<string, waitingMessageFunc>;
private namespaces: Namespaces;
connectedNamespaces: Map<string, NSConn>; // export it.
// private isConnectingProcesseses: string[]; // if elem exists then any receive of that namespace is locked until `askConnect` finished.
constructor(conn: WebSocket, namespaces: Namespaces) {
this.conn = conn;
this.reconnectTries = 0;
this._isAcknowledged = false;
this.namespaces = namespaces
let hasEmptyNS = namespaces.has("");
this.allowNativeMessages = hasEmptyNS && namespaces.get("").has(OnNativeMessage);
this.queue = new Array<string>();
this.waitingMessages = new Map<string, waitingMessageFunc>();
this.connectedNamespaces = new Map<string, NSConn>();
// this.isConnectingProcesseses = new Array<string>();
this.closed = false;
// this.conn.onclose = ((evt: Event): any => {
// this.close();
// return null;
// });
}
/* The wasReconnected method reports whether the current connection is the result of a reconnection.
To get the numbers of total retries see the `reconnectTries` field. */
wasReconnected(): boolean {
return this.reconnectTries > 0;
}
isAcknowledged(): boolean {
return this._isAcknowledged;
}
handle(evt: MessageEvent): Error {
if (!this._isAcknowledged) {
// if (evt.data instanceof ArrayBuffer) {
// new Uint8Array(evt.data)
let err = this.handleAck(evt.data);
if (err == undefined) {
this._isAcknowledged = true
this.handleQueue();
} else {
this.conn.close();
}
return err;
}
return this.handleMessage(evt.data);
}
private handleAck(data: WSData): Error {
let typ = data[0];
switch (typ) {
case ackIDBinary:
// let id = dec.decode(data.slice(1));
let id = data.slice(1);
this.ID = id;
break;
case ackNotOKBinary:
// let errorText = dec.decode(data.slice(1));
let errorText = data.slice(1);
return new Error(errorText);
default:
this.queue.push(data);
return null;
}
}
private handleQueue(): void {
if (this.queue == undefined || this.queue.length == 0) {
return;
}
this.queue.forEach((item, index) => {
this.queue.splice(index, 1);
this.handleMessage(item);
});