@emeryld/rrroutes-client 2.0.9 → 2.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -571,81 +571,165 @@ var SocketClient = class {
571
571
  this.handlerMap = /* @__PURE__ */ new Map();
572
572
  this.events = events;
573
573
  this.socket = opts.socket ?? null;
574
- this.roomJoinEvent = opts.roomJoinEvent ?? "room:join";
575
- this.roomLeaveEvent = opts.roomLeaveEvent ?? "room:leave";
576
574
  this.environment = opts.environment ?? "development";
577
575
  this.debug = opts.debug ?? {};
578
- const hb = opts.heartbeat;
576
+ this.sysEvents = opts.sys ?? {};
577
+ const hb = opts.heartbeat ?? {};
579
578
  this.hb = {
580
- pingEvent: hb.pingEvent ?? "sys:ping",
581
- pongEvent: hb.pongEvent ?? "sys:pong",
582
579
  intervalMs: hb.intervalMs ?? 15e3,
583
580
  timeoutMs: hb.timeoutMs ?? 7500,
584
- pingSchema: hb.pingSchema,
585
- makePingPayload: hb.makePingPayload,
586
- pongSchema: hb.pongSchema,
587
581
  onPong: hb.onPong
588
582
  };
589
583
  this.onConnect = () => {
590
- this.dbg({
591
- type: "connection",
592
- phase: "connect",
593
- id: this.socket?.id ?? ""
594
- // CHANGED
595
- });
596
- this.startHeartbeat();
584
+ const sys = this.getSysEvent("sys:connect");
585
+ const defaultHandler = () => {
586
+ this.dbg({
587
+ type: "connection",
588
+ phase: "connect",
589
+ id: this.socket?.id ?? ""
590
+ });
591
+ this.startHeartbeat();
592
+ };
593
+ if (sys?.sysHandler) {
594
+ sys.sysHandler({
595
+ name: "sys:connect",
596
+ phase: "connect",
597
+ socket: this.socket,
598
+ next: defaultHandler,
599
+ startHeartbeat: () => this.startHeartbeat(),
600
+ stopHeartbeat: () => this.stopHeartbeat()
601
+ });
602
+ } else {
603
+ defaultHandler();
604
+ }
597
605
  };
598
606
  this.onReconnect = (attempt) => {
599
- this.dbg({
600
- type: "connection",
601
- phase: "reconnect",
602
- attempt
603
- });
607
+ const sys = this.getSysEvent("sys:reconnect");
608
+ const defaultHandler = () => {
609
+ this.dbg({
610
+ type: "connection",
611
+ phase: "reconnect",
612
+ attempt
613
+ });
614
+ };
615
+ if (sys?.sysHandler) {
616
+ sys.sysHandler({
617
+ name: "sys:reconnect",
618
+ phase: "reconnect",
619
+ attempt,
620
+ socket: this.socket,
621
+ next: defaultHandler,
622
+ startHeartbeat: () => this.startHeartbeat(),
623
+ stopHeartbeat: () => this.stopHeartbeat()
624
+ });
625
+ } else {
626
+ defaultHandler();
627
+ }
604
628
  };
605
629
  this.onDisconnect = (reason) => {
606
- this.dbg({
607
- type: "connection",
608
- phase: "disconnect",
609
- reason: String(reason)
610
- });
611
- this.stopHeartbeat();
630
+ const sys = this.getSysEvent("sys:disconnect");
631
+ const defaultHandler = () => {
632
+ this.dbg({
633
+ type: "connection",
634
+ phase: "disconnect",
635
+ reason: String(reason)
636
+ });
637
+ this.stopHeartbeat();
638
+ };
639
+ if (sys?.sysHandler) {
640
+ sys.sysHandler({
641
+ name: "sys:disconnect",
642
+ phase: "disconnect",
643
+ reason: String(reason),
644
+ socket: this.socket,
645
+ next: defaultHandler,
646
+ startHeartbeat: () => this.startHeartbeat(),
647
+ stopHeartbeat: () => this.stopHeartbeat()
648
+ });
649
+ } else {
650
+ defaultHandler();
651
+ }
612
652
  };
613
653
  this.onConnectError = (err) => {
614
- this.dbg({
615
- type: "connection",
616
- phase: "connect_error",
617
- err: String(err)
618
- });
654
+ const sys = this.getSysEvent("sys:connect_error");
655
+ const defaultHandler = () => {
656
+ this.dbg({
657
+ type: "connection",
658
+ phase: "connect_error",
659
+ err: String(err)
660
+ });
661
+ };
662
+ if (sys?.sysHandler) {
663
+ sys.sysHandler({
664
+ name: "sys:connect_error",
665
+ phase: "connect_error",
666
+ error: err,
667
+ socket: this.socket,
668
+ next: defaultHandler,
669
+ startHeartbeat: () => this.startHeartbeat(),
670
+ stopHeartbeat: () => this.stopHeartbeat()
671
+ });
672
+ } else {
673
+ defaultHandler();
674
+ }
619
675
  };
620
676
  this.onPong = (raw) => {
677
+ const sys = this.getSysEvent("sys:pong");
678
+ const schema = sys?.message;
679
+ let validated = raw;
680
+ if (schema) {
681
+ const ok = schema.safeParse(raw);
682
+ if (!ok.success) return;
683
+ validated = ok.data;
684
+ }
621
685
  const receivedAt = Date.now();
622
- const clientSentIso = raw?.clientEcho?.__clientSentAt;
686
+ const clientSentIso = validated?.clientEcho?.__clientSentAt;
623
687
  let latencyMs;
624
688
  if (clientSentIso) {
625
689
  const sent = Date.parse(clientSentIso);
626
690
  if (!Number.isNaN(sent)) latencyMs = Math.max(0, receivedAt - sent);
627
691
  }
628
- if (this.hb.pongSchema) {
629
- const ok = this.hb.pongSchema.safeParse(raw);
630
- if (!ok.success) return;
692
+ const latency = latencyMs ?? validated?.sinceMs ?? 0;
693
+ const defaultHandler = () => {
694
+ this.dbg({
695
+ type: "heartbeat",
696
+ action: "pong_recv",
697
+ latencyMs: latency,
698
+ payload: validated
699
+ });
700
+ if (this.hb.onPong) {
701
+ this.hb.onPong({
702
+ latencyMs: latency,
703
+ payload: validated,
704
+ socket: this.socket
705
+ });
706
+ }
707
+ };
708
+ if (sys?.sysHandler) {
709
+ sys.sysHandler({
710
+ name: "sys:pong",
711
+ socket: this.socket,
712
+ raw: validated,
713
+ latencyMs: latency,
714
+ next: defaultHandler,
715
+ startHeartbeat: () => this.startHeartbeat(),
716
+ stopHeartbeat: () => this.stopHeartbeat()
717
+ });
718
+ } else {
719
+ defaultHandler();
631
720
  }
632
- const latency = latencyMs ?? raw?.sinceMs ?? 0;
633
- this.dbg({
634
- type: "heartbeat",
635
- action: "pong_recv",
636
- latencyMs: latency,
637
- payload: raw
638
- });
639
- this.hb.onPong?.({ latencyMs: latency, payload: raw, socket: this.socket });
640
721
  };
641
722
  if (this.socket) {
642
723
  this.socket.on("connect", this.onConnect);
643
724
  this.socket.on("reconnect", this.onReconnect);
644
725
  this.socket.on("disconnect", this.onDisconnect);
645
726
  this.socket.on("connect_error", this.onConnectError);
646
- this.socket.on(this.hb.pongEvent, this.onPong);
727
+ this.socket.on("sys:pong", this.onPong);
647
728
  }
648
729
  }
730
+ getSysEvent(name) {
731
+ return this.sysEvents[name];
732
+ }
649
733
  dbg(e) {
650
734
  const d = this.debug;
651
735
  if (!d.logger) return;
@@ -670,33 +754,73 @@ var SocketClient = class {
670
754
  toArray(rooms) {
671
755
  return rooms == null ? [] : Array.isArray(rooms) ? rooms : [rooms];
672
756
  }
757
+ /**
758
+ * Public: start the heartbeat loop manually.
759
+ *
760
+ * This is called by the default 'sys:connect' handler, but you can also
761
+ * call it yourself from any sysHandler to change when heartbeats start.
762
+ */
673
763
  startHeartbeat() {
674
764
  this.stopHeartbeat();
675
765
  if (!this.socket) return;
766
+ const socket = this.socket;
676
767
  const tick = () => {
677
- if (!this.socket) return;
678
- const basePayload = this.hb.makePingPayload({ socket: this.socket }) ?? {};
679
- const candidate = { ...basePayload, __clientSentAt: (/* @__PURE__ */ new Date()).toISOString() };
680
- const check = this.hb.pingSchema.safeParse(candidate);
681
- if (!check.success) {
682
- if (this.environment === "development")
683
- console.warn("[socket] ping schema validation failed", check.error.issues);
684
- return;
685
- }
686
- const timer = setTimeout(() => {
687
- }, this.hb.timeoutMs);
688
- this.socket.timeout(this.hb.timeoutMs).emit(this.hb.pingEvent, { payload: check.data }, () => {
689
- clearTimeout(timer);
690
- });
691
- this.dbg({
692
- type: "heartbeat",
693
- action: "ping_emit",
694
- payload: check.data
768
+ if (!socket) return;
769
+ const sysPing = this.getSysEvent("sys:ping");
770
+ const schema = sysPing?.message;
771
+ const buildDefaultPayload = () => ({
772
+ clientEcho: { __clientSentAt: (/* @__PURE__ */ new Date()).toISOString() }
695
773
  });
774
+ const send = (payload) => {
775
+ let dataToSend = payload;
776
+ if (schema) {
777
+ const check = schema.safeParse(payload);
778
+ if (!check.success) {
779
+ if (this.environment === "development") {
780
+ console.warn("[socket] ping schema validation failed", check.error.issues);
781
+ }
782
+ return;
783
+ }
784
+ dataToSend = check.data;
785
+ }
786
+ const timer = setTimeout(() => {
787
+ }, this.hb.timeoutMs);
788
+ socket.timeout(this.hb.timeoutMs).emit("sys:ping", dataToSend, () => {
789
+ clearTimeout(timer);
790
+ });
791
+ this.dbg({
792
+ type: "heartbeat",
793
+ action: "ping_emit",
794
+ payload: dataToSend
795
+ });
796
+ };
797
+ const defaultHandler = () => {
798
+ const payload = buildDefaultPayload();
799
+ send(payload);
800
+ };
801
+ if (sysPing?.sysHandler) {
802
+ sysPing.sysHandler({
803
+ name: "sys:ping",
804
+ socket,
805
+ buildDefaultPayload,
806
+ send,
807
+ next: defaultHandler,
808
+ startHeartbeat: () => this.startHeartbeat(),
809
+ stopHeartbeat: () => this.stopHeartbeat()
810
+ });
811
+ } else {
812
+ defaultHandler();
813
+ }
696
814
  };
697
815
  this.hbTimer = setInterval(tick, this.hb.intervalMs);
698
816
  tick();
699
817
  }
818
+ /**
819
+ * Public: stop the heartbeat loop.
820
+ *
821
+ * This is called by the default 'sys:disconnect' handler, but you can also
822
+ * call it yourself from any sysHandler to fully control heartbeat lifecycle.
823
+ */
700
824
  stopHeartbeat() {
701
825
  if (this.hbTimer) {
702
826
  clearInterval(this.hbTimer);
@@ -738,8 +862,24 @@ var SocketClient = class {
738
862
  if (next === 1) toJoin.push(r);
739
863
  }
740
864
  if (toJoin.length > 0 && this.socket) {
741
- this.socket.emit(this.roomJoinEvent, { rooms: toJoin });
742
- this.dbg({ type: "room", action: "join", rooms: toJoin });
865
+ const socket = this.socket;
866
+ const sys = this.getSysEvent("sys:room_join");
867
+ const defaultHandler = () => {
868
+ socket.emit("sys:room_join", { rooms: toJoin });
869
+ this.dbg({ type: "room", action: "join", rooms: toJoin });
870
+ };
871
+ if (sys?.sysHandler) {
872
+ sys.sysHandler({
873
+ name: "sys:room_join",
874
+ rooms: toJoin,
875
+ socket,
876
+ next: defaultHandler,
877
+ startHeartbeat: () => this.startHeartbeat(),
878
+ stopHeartbeat: () => this.stopHeartbeat()
879
+ });
880
+ } else {
881
+ defaultHandler();
882
+ }
743
883
  }
744
884
  }
745
885
  leaveRooms(rooms) {
@@ -753,8 +893,24 @@ var SocketClient = class {
753
893
  else this.roomCounts.set(r, next);
754
894
  }
755
895
  if (toLeave.length > 0 && this.socket) {
756
- this.socket.emit(this.roomLeaveEvent, { rooms: toLeave });
757
- this.dbg({ type: "room", action: "leave", rooms: toLeave });
896
+ const socket = this.socket;
897
+ const sys = this.getSysEvent("sys:room_leave");
898
+ const defaultHandler = () => {
899
+ socket.emit("sys:room_leave", { rooms: toLeave });
900
+ this.dbg({ type: "room", action: "leave", rooms: toLeave });
901
+ };
902
+ if (sys?.sysHandler) {
903
+ sys.sysHandler({
904
+ name: "sys:room_leave",
905
+ rooms: toLeave,
906
+ socket,
907
+ next: defaultHandler,
908
+ startHeartbeat: () => this.startHeartbeat(),
909
+ stopHeartbeat: () => this.stopHeartbeat()
910
+ });
911
+ } else {
912
+ defaultHandler();
913
+ }
758
914
  }
759
915
  }
760
916
  on(event, handler) {
@@ -846,7 +1002,7 @@ var SocketClient = class {
846
1002
  socket.off("reconnect", this.onReconnect);
847
1003
  socket.off("disconnect", this.onDisconnect);
848
1004
  socket.off("connect_error", this.onConnectError);
849
- socket.off(this.hb.pongEvent, this.onPong);
1005
+ socket.off("sys:pong", this.onPong);
850
1006
  for (const [event, set] of this.handlerMap.entries()) {
851
1007
  for (const entry of set) {
852
1008
  socket.off(String(event), entry.wrapped);
@@ -857,8 +1013,23 @@ var SocketClient = class {
857
1013
  this.handlerMap.clear();
858
1014
  const toLeave = Array.from(this.roomCounts.entries()).filter(([, count]) => count > 0).map(([room]) => room);
859
1015
  if (toLeave.length > 0 && socket) {
860
- socket.emit(this.roomLeaveEvent, { rooms: toLeave });
861
- this.dbg({ type: "room", action: "leave", rooms: toLeave });
1016
+ const sys = this.getSysEvent("sys:room_leave");
1017
+ const defaultHandler = () => {
1018
+ socket.emit("sys:room_leave", { rooms: toLeave });
1019
+ this.dbg({ type: "room", action: "leave", rooms: toLeave });
1020
+ };
1021
+ if (sys?.sysHandler) {
1022
+ sys.sysHandler({
1023
+ name: "sys:room_leave",
1024
+ rooms: toLeave,
1025
+ socket,
1026
+ next: defaultHandler,
1027
+ startHeartbeat: () => this.startHeartbeat(),
1028
+ stopHeartbeat: () => this.stopHeartbeat()
1029
+ });
1030
+ } else {
1031
+ defaultHandler();
1032
+ }
862
1033
  }
863
1034
  this.roomCounts.clear();
864
1035
  }