@emeryld/rrroutes-client 2.0.8 → 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
@@ -488,30 +488,33 @@ function buildSocketProvider(args) {
488
488
  }
489
489
  function SocketProvider(props) {
490
490
  const { events, baseOptions, children, fallback, providerDebug } = props;
491
- const [socket, setSocket] = React.useState(
492
- "socket" in props ? props.socket : null
493
- );
491
+ const [resolvedSocket, setResolvedSocket] = React.useState(null);
492
+ const socket = "socket" in props ? props.socket ?? null : resolvedSocket;
494
493
  React.useEffect(() => {
494
+ if (!("getSocket" in props)) return;
495
495
  let cancelled = false;
496
496
  dbg(providerDebug, { type: "resolve", phase: "start" });
497
- if (!socket && "getSocket" in props) {
497
+ if (!resolvedSocket) {
498
498
  Promise.resolve(props.getSocket()).then((s) => {
499
499
  if (cancelled) {
500
500
  dbg(providerDebug, { type: "resolve", phase: "cancelled" });
501
+ return;
502
+ }
503
+ if (!s) {
504
+ dbg(providerDebug, { type: "resolve", phase: "missing" });
505
+ return;
501
506
  }
502
- setSocket(s);
507
+ setResolvedSocket(s);
503
508
  dbg(providerDebug, { type: "resolve", phase: "ok" });
504
509
  }).catch((err) => {
505
- dbg(providerDebug, { type: "resolve", phase: "error", err: String(err) });
506
510
  if (cancelled) return;
511
+ dbg(providerDebug, { type: "resolve", phase: "error", err: String(err) });
507
512
  });
508
- } else {
509
- dbg(providerDebug, { type: "resolve", phase: "missing" });
510
513
  }
511
514
  return () => {
512
515
  cancelled = true;
513
516
  };
514
- }, [socket]);
517
+ }, [resolvedSocket]);
515
518
  const client = React.useMemo(() => {
516
519
  if (!socket) {
517
520
  dbg(providerDebug, { type: "client", phase: "missing" });
@@ -567,78 +570,165 @@ var SocketClient = class {
567
570
  this.roomCounts = /* @__PURE__ */ new Map();
568
571
  this.handlerMap = /* @__PURE__ */ new Map();
569
572
  this.events = events;
570
- this.socket = opts.socket;
571
- this.roomJoinEvent = opts.roomJoinEvent ?? "room:join";
572
- this.roomLeaveEvent = opts.roomLeaveEvent ?? "room:leave";
573
+ this.socket = opts.socket ?? null;
573
574
  this.environment = opts.environment ?? "development";
574
575
  this.debug = opts.debug ?? {};
575
- const hb = opts.heartbeat;
576
+ this.sysEvents = opts.sys ?? {};
577
+ const hb = opts.heartbeat ?? {};
576
578
  this.hb = {
577
- pingEvent: hb.pingEvent ?? "sys:ping",
578
- pongEvent: hb.pongEvent ?? "sys:pong",
579
579
  intervalMs: hb.intervalMs ?? 15e3,
580
580
  timeoutMs: hb.timeoutMs ?? 7500,
581
- pingSchema: hb.pingSchema,
582
- makePingPayload: hb.makePingPayload,
583
- pongSchema: hb.pongSchema,
584
581
  onPong: hb.onPong
585
582
  };
586
583
  this.onConnect = () => {
587
- this.dbg({
588
- type: "connection",
589
- phase: "connect",
590
- id: this.socket.id ?? ""
591
- });
592
- 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
+ }
593
605
  };
594
606
  this.onReconnect = (attempt) => {
595
- this.dbg({
596
- type: "connection",
597
- phase: "reconnect",
598
- attempt
599
- });
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
+ }
600
628
  };
601
629
  this.onDisconnect = (reason) => {
602
- this.dbg({
603
- type: "connection",
604
- phase: "disconnect",
605
- reason: String(reason)
606
- });
607
- 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
+ }
608
652
  };
609
653
  this.onConnectError = (err) => {
610
- this.dbg({
611
- type: "connection",
612
- phase: "connect_error",
613
- err: String(err)
614
- });
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
+ }
615
675
  };
616
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
+ }
617
685
  const receivedAt = Date.now();
618
- const clientSentIso = raw?.clientEcho?.__clientSentAt;
686
+ const clientSentIso = validated?.clientEcho?.__clientSentAt;
619
687
  let latencyMs;
620
688
  if (clientSentIso) {
621
689
  const sent = Date.parse(clientSentIso);
622
690
  if (!Number.isNaN(sent)) latencyMs = Math.max(0, receivedAt - sent);
623
691
  }
624
- if (this.hb.pongSchema) {
625
- const ok = this.hb.pongSchema.safeParse(raw);
626
- 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();
627
720
  }
628
- const latency = latencyMs ?? raw?.sinceMs ?? 0;
629
- this.dbg({
630
- type: "heartbeat",
631
- action: "pong_recv",
632
- latencyMs: latency,
633
- payload: raw
634
- });
635
- this.hb.onPong?.({ latencyMs: latency, payload: raw, socket: this.socket });
636
721
  };
637
- this.socket.on("connect", this.onConnect);
638
- this.socket.on("reconnect", this.onReconnect);
639
- this.socket.on("disconnect", this.onDisconnect);
640
- this.socket.on("connect_error", this.onConnectError);
641
- this.socket.on(this.hb.pongEvent, this.onPong);
722
+ if (this.socket) {
723
+ this.socket.on("connect", this.onConnect);
724
+ this.socket.on("reconnect", this.onReconnect);
725
+ this.socket.on("disconnect", this.onDisconnect);
726
+ this.socket.on("connect_error", this.onConnectError);
727
+ this.socket.on("sys:pong", this.onPong);
728
+ }
729
+ }
730
+ getSysEvent(name) {
731
+ return this.sysEvents[name];
642
732
  }
643
733
  dbg(e) {
644
734
  const d = this.debug;
@@ -664,31 +754,73 @@ var SocketClient = class {
664
754
  toArray(rooms) {
665
755
  return rooms == null ? [] : Array.isArray(rooms) ? rooms : [rooms];
666
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
+ */
667
763
  startHeartbeat() {
668
764
  this.stopHeartbeat();
765
+ if (!this.socket) return;
766
+ const socket = this.socket;
669
767
  const tick = () => {
670
- const basePayload = this.hb.makePingPayload({ socket: this.socket }) ?? {};
671
- const candidate = { ...basePayload, __clientSentAt: (/* @__PURE__ */ new Date()).toISOString() };
672
- const check = this.hb.pingSchema.safeParse(candidate);
673
- if (!check.success) {
674
- if (this.environment === "development")
675
- console.warn("[socket] ping schema validation failed", check.error.issues);
676
- return;
677
- }
678
- const timer = setTimeout(() => {
679
- }, this.hb.timeoutMs);
680
- this.socket.timeout(this.hb.timeoutMs).emit(this.hb.pingEvent, { payload: check.data }, () => {
681
- clearTimeout(timer);
682
- });
683
- this.dbg({
684
- type: "heartbeat",
685
- action: "ping_emit",
686
- 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() }
687
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
+ }
688
814
  };
689
815
  this.hbTimer = setInterval(tick, this.hb.intervalMs);
690
816
  tick();
691
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
+ */
692
824
  stopHeartbeat() {
693
825
  if (this.hbTimer) {
694
826
  clearInterval(this.hbTimer);
@@ -699,6 +831,12 @@ var SocketClient = class {
699
831
  const schema = this.events[event].message;
700
832
  const parsed = schema.safeParse(payload);
701
833
  if (!parsed.success) throw new Error(`Invalid payload for "${event}": ${parsed.error.message}`);
834
+ if (!this.socket) {
835
+ if (this.environment === "development") {
836
+ console.warn(`[socket] emit("${String(event)}") skipped because socket is null`);
837
+ }
838
+ return;
839
+ }
702
840
  if (onAck) {
703
841
  this.socket.timeout(timeoutMs ?? this.hb.timeoutMs).emit(String(event), parsed.data, (ack) => {
704
842
  try {
@@ -723,9 +861,25 @@ var SocketClient = class {
723
861
  this.roomCounts.set(r, next);
724
862
  if (next === 1) toJoin.push(r);
725
863
  }
726
- if (toJoin.length > 0) {
727
- this.socket.emit(this.roomJoinEvent, { rooms: toJoin });
728
- this.dbg({ type: "room", action: "join", rooms: toJoin });
864
+ if (toJoin.length > 0 && this.socket) {
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
+ }
729
883
  }
730
884
  }
731
885
  leaveRooms(rooms) {
@@ -738,14 +892,38 @@ var SocketClient = class {
738
892
  if (next === 0) this.roomCounts.delete(r);
739
893
  else this.roomCounts.set(r, next);
740
894
  }
741
- if (toLeave.length > 0) {
742
- this.socket.emit(this.roomLeaveEvent, { rooms: toLeave });
743
- this.dbg({ type: "room", action: "leave", rooms: toLeave });
895
+ if (toLeave.length > 0 && this.socket) {
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
+ }
744
914
  }
745
915
  }
746
916
  on(event, handler) {
747
917
  const schema = this.events[event].message;
748
918
  this.dbg({ type: "register", action: "register", event });
919
+ if (!this.socket) {
920
+ if (this.environment === "development") {
921
+ console.warn(`[socket] on("${String(event)}") skipped because socket is null`);
922
+ }
923
+ return () => {
924
+ };
925
+ }
926
+ const socket = this.socket;
749
927
  const wrapped = (envelopeOrRaw, maybeAck) => {
750
928
  const maybeEnvelope = envelopeOrRaw;
751
929
  const rawData = maybeEnvelope?.data ?? maybeEnvelope;
@@ -764,9 +942,9 @@ var SocketClient = class {
764
942
  ctx: {
765
943
  receivedAt,
766
944
  latencyMs: sentAt ? Math.max(0, receivedAt.getTime() - sentAt.getTime()) : void 0,
767
- nsp: this.socket.nsp,
768
- socketId: this.socket.id,
769
- socket: this.socket,
945
+ nsp: socket.nsp,
946
+ socketId: socket.id,
947
+ socket,
770
948
  reply: typeof maybeAck === "function" ? (d) => {
771
949
  try {
772
950
  maybeAck(d);
@@ -792,8 +970,8 @@ var SocketClient = class {
792
970
  console.warn(`[socket] ${String(event)}:error`, e);
793
971
  }
794
972
  };
795
- this.socket.on(String(event), wrapped);
796
- this.socket.on(`${String(event)}:error`, errorWrapped);
973
+ socket.on(String(event), wrapped);
974
+ socket.on(`${String(event)}:error`, errorWrapped);
797
975
  let set = this.handlerMap.get(String(event));
798
976
  if (!set) {
799
977
  set = /* @__PURE__ */ new Set();
@@ -802,8 +980,8 @@ var SocketClient = class {
802
980
  const entry = { orig: handler, wrapped, errorWrapped };
803
981
  set.add(entry);
804
982
  return () => {
805
- this.socket.off(String(event), wrapped);
806
- this.socket.off(`${String(event)}:error`, errorWrapped);
983
+ socket.off(String(event), wrapped);
984
+ socket.off(`${String(event)}:error`, errorWrapped);
807
985
  const s = this.handlerMap.get(String(event));
808
986
  if (s) {
809
987
  s.delete(entry);
@@ -818,32 +996,52 @@ var SocketClient = class {
818
996
  */
819
997
  destroy() {
820
998
  this.stopHeartbeat();
821
- this.socket.off("connect", this.onConnect);
822
- this.socket.off("reconnect", this.onReconnect);
823
- this.socket.off("disconnect", this.onDisconnect);
824
- this.socket.off("connect_error", this.onConnectError);
825
- this.socket.off(this.hb.pongEvent, this.onPong);
826
- for (const [event, set] of this.handlerMap.entries()) {
827
- for (const entry of set) {
828
- this.socket.off(String(event), entry.wrapped);
829
- this.socket.off(`${String(event)}:error`, entry.errorWrapped);
999
+ const socket = this.socket;
1000
+ if (socket) {
1001
+ socket.off("connect", this.onConnect);
1002
+ socket.off("reconnect", this.onReconnect);
1003
+ socket.off("disconnect", this.onDisconnect);
1004
+ socket.off("connect_error", this.onConnectError);
1005
+ socket.off("sys:pong", this.onPong);
1006
+ for (const [event, set] of this.handlerMap.entries()) {
1007
+ for (const entry of set) {
1008
+ socket.off(String(event), entry.wrapped);
1009
+ socket.off(`${String(event)}:error`, entry.errorWrapped);
1010
+ }
830
1011
  }
831
1012
  }
832
1013
  this.handlerMap.clear();
833
1014
  const toLeave = Array.from(this.roomCounts.entries()).filter(([, count]) => count > 0).map(([room]) => room);
834
- if (toLeave.length > 0) {
835
- this.socket.emit(this.roomLeaveEvent, { rooms: toLeave });
836
- this.dbg({ type: "room", action: "leave", rooms: toLeave });
1015
+ if (toLeave.length > 0 && socket) {
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
+ }
837
1033
  }
838
1034
  this.roomCounts.clear();
839
1035
  }
840
1036
  /** Pass-throughs. Managing connection is the caller’s responsibility. */
841
1037
  disconnect() {
1038
+ if (!this.socket) return;
842
1039
  this.stopHeartbeat();
843
1040
  this.socket.disconnect();
844
1041
  this.dbg({ type: "connection", phase: "disconnect", reason: "client_disconnect" });
845
1042
  }
846
1043
  connect() {
1044
+ if (!this.socket) return;
847
1045
  this.socket.connect();
848
1046
  this.dbg({ type: "connection", phase: "connect", id: this.socket.id ?? "" });
849
1047
  }