@emeryld/rrroutes-client 2.0.10 → 2.0.12

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
@@ -32,6 +32,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
32
32
  var index_exports = {};
33
33
  __export(index_exports, {
34
34
  SocketClient: () => SocketClient,
35
+ buildRoomPayloadSchema: () => buildRoomPayloadSchema,
35
36
  buildSocketProvider: () => buildSocketProvider,
36
37
  createRouteClient: () => createRouteClient,
37
38
  defaultFetcher: () => defaultFetcher
@@ -462,6 +463,14 @@ function toFormData(body) {
462
463
  return fd;
463
464
  }
464
465
 
466
+ // src/sockets/socket.client.sys.ts
467
+ var import_zod = require("zod");
468
+ var roomValueSchema = import_zod.z.union([import_zod.z.array(import_zod.z.string()), import_zod.z.string()]);
469
+ var buildRoomPayloadSchema = (metaSchema) => import_zod.z.object({
470
+ rooms: roomValueSchema,
471
+ meta: metaSchema
472
+ });
473
+
465
474
  // src/sockets/socket.client.context.tsx
466
475
  var React = __toESM(require("react"), 1);
467
476
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -487,7 +496,7 @@ function buildSocketProvider(args) {
487
496
  };
488
497
  }
489
498
  function SocketProvider(props) {
490
- const { events, baseOptions, children, fallback, providerDebug } = props;
499
+ const { events, baseOptions, children, fallback, providerDebug, destroyLeaveMeta } = props;
491
500
  const [resolvedSocket, setResolvedSocket] = React.useState(null);
492
501
  const socket = "socket" in props ? props.socket ?? null : resolvedSocket;
493
502
  React.useEffect(() => {
@@ -527,7 +536,7 @@ function SocketProvider(props) {
527
536
  React.useEffect(() => {
528
537
  return () => {
529
538
  if (client) {
530
- client.destroy();
539
+ client.destroy(destroyLeaveMeta);
531
540
  dbg(providerDebug, { type: "client", phase: "destroy" });
532
541
  }
533
542
  };
@@ -552,11 +561,11 @@ function useSocketConnection(args) {
552
561
  [rooms]
553
562
  );
554
563
  React.useEffect(() => {
555
- if (autoJoin && normalizedRooms.length > 0) client.joinRooms(normalizedRooms);
564
+ if (autoJoin && normalizedRooms.length > 0) client.joinRooms(normalizedRooms, args.joinMeta);
556
565
  const unsubscribe = client.on(event, onMessage);
557
566
  return () => {
558
567
  unsubscribe();
559
- if (autoLeave && normalizedRooms.length > 0) client.leaveRooms(normalizedRooms);
568
+ if (autoLeave && normalizedRooms.length > 0) client.leaveRooms(normalizedRooms, args.leaveMeta);
560
569
  if (onCleanup) onCleanup();
561
570
  };
562
571
  }, args.deps ?? [client, event, onMessage, autoJoin, autoLeave, ...normalizedRooms]);
@@ -573,151 +582,145 @@ var SocketClient = class {
573
582
  this.socket = opts.socket ?? null;
574
583
  this.environment = opts.environment ?? "development";
575
584
  this.debug = opts.debug ?? {};
576
- this.sysEvents = opts.sys ?? {};
585
+ this.config = opts.config;
586
+ this.sysEvents = opts.sys;
587
+ this.roomJoinSchema = buildRoomPayloadSchema(this.config.joinMetaMessage);
588
+ this.roomLeaveSchema = buildRoomPayloadSchema(this.config.leaveMetaMessage);
577
589
  const hb = opts.heartbeat ?? {};
578
590
  this.hb = {
579
591
  intervalMs: hb.intervalMs ?? 15e3,
580
- timeoutMs: hb.timeoutMs ?? 7500,
581
- onPong: hb.onPong
592
+ timeoutMs: hb.timeoutMs ?? 7500
582
593
  };
594
+ this.dbg({
595
+ type: "lifecycle",
596
+ phase: "init_start",
597
+ socketId: this.socket?.id ?? void 0,
598
+ details: {
599
+ environment: this.environment
600
+ }
601
+ });
602
+ if (!this.socket) {
603
+ this.dbg({
604
+ type: "lifecycle",
605
+ phase: "init_socket_missing",
606
+ err: "Socket reference is null during initialization"
607
+ });
608
+ } else {
609
+ this.dbg({
610
+ type: "lifecycle",
611
+ phase: "init_ready",
612
+ socketId: this.socket.id,
613
+ details: {
614
+ heartbeatIntervalMs: this.hb.intervalMs,
615
+ heartbeatTimeoutMs: this.hb.timeoutMs
616
+ }
617
+ });
618
+ this.logSocketConfigSnapshot("init", "constructor");
619
+ }
583
620
  this.onConnect = () => {
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();
621
+ if (!this.socket) {
622
+ this.dbg({ type: "connection", phase: "connect", err: "Socket is null" });
623
+ throw new Error("Socket is null in onConnect handler");
604
624
  }
625
+ this.dbg({
626
+ type: "connection",
627
+ phase: "connect",
628
+ id: this.socket.id,
629
+ details: {
630
+ nsp: this.getNamespace(this.socket)
631
+ }
632
+ });
633
+ this.logSocketConfigSnapshot("update", "connect_event");
634
+ this.getSysEvent("sys:connect")({
635
+ socket: this.socket,
636
+ client: this
637
+ });
605
638
  };
606
639
  this.onReconnect = (attempt) => {
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();
640
+ if (!this.socket) {
641
+ this.dbg({ type: "connection", phase: "reconnect", err: "Socket is null" });
642
+ throw new Error("Socket is null in onReconnect handler");
627
643
  }
644
+ this.dbg({
645
+ type: "connection",
646
+ phase: "reconnect",
647
+ attempt,
648
+ id: this.socket.id,
649
+ details: {
650
+ nsp: this.getNamespace(this.socket)
651
+ }
652
+ });
653
+ this.logSocketConfigSnapshot("update", "reconnect_event");
654
+ this.getSysEvent("sys:reconnect")({
655
+ attempt,
656
+ socket: this.socket,
657
+ client: this
658
+ });
628
659
  };
629
660
  this.onDisconnect = (reason) => {
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();
661
+ if (!this.socket) {
662
+ this.dbg({ type: "connection", phase: "disconnect", err: "Socket is null" });
663
+ throw new Error("Socket is null in onDisconnect handler");
651
664
  }
665
+ this.dbg({
666
+ type: "connection",
667
+ phase: "disconnect",
668
+ reason: String(reason),
669
+ details: {
670
+ roomsTracked: this.roomCounts.size
671
+ }
672
+ });
673
+ this.logSocketConfigSnapshot("update", "disconnect_event");
674
+ this.getSysEvent("sys:disconnect")({
675
+ reason: String(reason),
676
+ socket: this.socket,
677
+ client: this
678
+ });
652
679
  };
653
680
  this.onConnectError = (err) => {
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();
681
+ if (!this.socket) {
682
+ this.dbg({ type: "connection", phase: "connect_error", err: "Socket is null" });
683
+ throw new Error("Socket is null in onConnectError handler");
674
684
  }
685
+ this.dbg({
686
+ type: "connection",
687
+ phase: "connect_error",
688
+ err: String(err),
689
+ details: this.getVerboseDetails({ rawError: err })
690
+ });
691
+ this.logSocketConfigSnapshot("update", "connect_error_event");
692
+ this.getSysEvent("sys:connect_error")({
693
+ error: String(err),
694
+ socket: this.socket,
695
+ client: this
696
+ });
675
697
  };
676
698
  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;
699
+ if (!this.socket) {
700
+ this.dbg({ type: "heartbeat", action: "pong_recv", err: "Socket is null" });
701
+ throw new Error("Socket is null in onPong handler");
684
702
  }
685
- const receivedAt = Date.now();
686
- const clientSentIso = validated?.clientEcho?.__clientSentAt;
687
- let latencyMs;
688
- if (clientSentIso) {
689
- const sent = Date.parse(clientSentIso);
690
- if (!Number.isNaN(sent)) latencyMs = Math.max(0, receivedAt - sent);
691
- }
692
- const latency = latencyMs ?? validated?.sinceMs ?? 0;
693
- const defaultHandler = () => {
703
+ const parsed = this.config.pongPayload.safeParse(raw);
704
+ if (!parsed.success) {
694
705
  this.dbg({
695
706
  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()
707
+ action: "validation_failed",
708
+ err: `pong payload validation failed: ${parsed.error.message}`,
709
+ details: this.getValidationDetails(parsed.error)
717
710
  });
718
- } else {
719
- defaultHandler();
711
+ return;
720
712
  }
713
+ const validated = parsed.data;
714
+ this.dbg({
715
+ type: "heartbeat",
716
+ action: "pong_recv",
717
+ payload: validated
718
+ });
719
+ this.getSysEvent("sys:pong")({
720
+ socket: this.socket,
721
+ payload: validated,
722
+ client: this
723
+ });
721
724
  };
722
725
  if (this.socket) {
723
726
  this.socket.on("connect", this.onConnect);
@@ -727,6 +730,61 @@ var SocketClient = class {
727
730
  this.socket.on("sys:pong", this.onPong);
728
731
  }
729
732
  }
733
+ snapshotSocketConfig(socket) {
734
+ if (!socket) return null;
735
+ const manager = socket.io ?? null;
736
+ const base = {
737
+ nsp: this.getNamespace(socket)
738
+ };
739
+ if (!manager) return base;
740
+ const opts = manager.opts ?? {};
741
+ const transports = Array.isArray(opts.transports) ? opts.transports.filter((t) => typeof t === "string") : void 0;
742
+ const transportName = typeof manager.engine?.transport?.name === "string" ? manager.engine.transport?.name : void 0;
743
+ const readyState = typeof manager._readyState === "string" ? manager._readyState : void 0;
744
+ const uri = typeof manager.uri === "string" ? manager.uri : void 0;
745
+ return {
746
+ ...base,
747
+ url: uri,
748
+ path: typeof opts.path === "string" ? opts.path : void 0,
749
+ transport: transportName ?? transports?.[0],
750
+ transports,
751
+ readyState,
752
+ autoConnect: typeof opts.autoConnect === "boolean" ? opts.autoConnect : void 0,
753
+ reconnection: typeof opts.reconnection === "boolean" ? opts.reconnection : void 0,
754
+ reconnectionAttempts: typeof opts.reconnectionAttempts === "number" ? opts.reconnectionAttempts : void 0,
755
+ reconnectionDelay: typeof opts.reconnectionDelay === "number" ? opts.reconnectionDelay : void 0,
756
+ reconnectionDelayMax: typeof opts.reconnectionDelayMax === "number" ? opts.reconnectionDelayMax : void 0,
757
+ timeout: typeof opts.timeout === "number" ? opts.timeout : void 0,
758
+ hostname: typeof opts.hostname === "string" ? opts.hostname : void 0,
759
+ port: typeof opts.port === "number" || typeof opts.port === "string" ? opts.port : void 0,
760
+ secure: typeof opts.secure === "boolean" ? opts.secure : void 0
761
+ };
762
+ }
763
+ logSocketConfigSnapshot(phase, reason) {
764
+ if (!this.debug.logger || !this.debug.config) return;
765
+ const snapshot = this.snapshotSocketConfig(this.socket);
766
+ this.dbg({
767
+ type: "config",
768
+ phase,
769
+ reason,
770
+ socketId: this.socket?.id,
771
+ snapshot: snapshot ?? void 0,
772
+ err: snapshot ? void 0 : "Socket unavailable for config snapshot"
773
+ });
774
+ }
775
+ getValidationDetails(error) {
776
+ if (!this.debug.verbose) return void 0;
777
+ return { issues: error.issues };
778
+ }
779
+ getVerboseDetails(details) {
780
+ if (!this.debug.verbose) return void 0;
781
+ return details;
782
+ }
783
+ getNamespace(socket) {
784
+ if (!socket) return void 0;
785
+ const nsp = socket.nsp;
786
+ return typeof nsp === "string" ? nsp : void 0;
787
+ }
730
788
  getSysEvent(name) {
731
789
  return this.sysEvents[name];
732
790
  }
@@ -754,6 +812,20 @@ var SocketClient = class {
754
812
  toArray(rooms) {
755
813
  return rooms == null ? [] : Array.isArray(rooms) ? rooms : [rooms];
756
814
  }
815
+ rollbackJoinIncrement(rooms) {
816
+ for (const room of rooms) {
817
+ const curr = this.roomCounts.get(room) ?? 0;
818
+ const next = Math.max(0, curr - 1);
819
+ if (next === 0) this.roomCounts.delete(room);
820
+ else this.roomCounts.set(room, next);
821
+ }
822
+ }
823
+ rollbackLeaveDecrement(rooms) {
824
+ for (const room of rooms) {
825
+ const curr = this.roomCounts.get(room) ?? 0;
826
+ this.roomCounts.set(room, curr + 1);
827
+ }
828
+ }
757
829
  /**
758
830
  * Public: start the heartbeat loop manually.
759
831
  *
@@ -762,55 +834,56 @@ var SocketClient = class {
762
834
  */
763
835
  startHeartbeat() {
764
836
  this.stopHeartbeat();
765
- if (!this.socket) return;
837
+ if (!this.socket) {
838
+ this.dbg({
839
+ type: "heartbeat",
840
+ action: "start",
841
+ err: "Socket is null"
842
+ });
843
+ return;
844
+ }
766
845
  const socket = this.socket;
846
+ this.dbg({
847
+ type: "heartbeat",
848
+ action: "start",
849
+ details: {
850
+ intervalMs: this.hb.intervalMs,
851
+ timeoutMs: this.hb.timeoutMs
852
+ }
853
+ });
767
854
  const tick = () => {
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() }
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
- });
855
+ if (!socket) {
791
856
  this.dbg({
792
857
  type: "heartbeat",
793
- action: "ping_emit",
794
- payload: dataToSend
858
+ action: "tick_skip",
859
+ err: "Socket missing during heartbeat tick"
795
860
  });
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()
861
+ return;
862
+ }
863
+ const payload = this.getSysEvent("sys:ping")({
864
+ socket,
865
+ client: this
866
+ });
867
+ const check = this.config.pingPayload.safeParse(payload);
868
+ if (!check.success) {
869
+ this.dbg({
870
+ type: "heartbeat",
871
+ action: "validation_failed",
872
+ err: "ping payload validation failed",
873
+ details: this.getValidationDetails(check.error)
810
874
  });
811
- } else {
812
- defaultHandler();
875
+ if (this.environment === "development") {
876
+ console.warn("[socket] ping schema validation failed", check.error.issues);
877
+ }
878
+ return;
813
879
  }
880
+ const dataToSend = check.data;
881
+ socket.emit("sys:ping", dataToSend);
882
+ this.dbg({
883
+ type: "heartbeat",
884
+ action: "ping_emit",
885
+ payload: dataToSend
886
+ });
814
887
  };
815
888
  this.hbTimer = setInterval(tick, this.hb.intervalMs);
816
889
  tick();
@@ -822,38 +895,56 @@ var SocketClient = class {
822
895
  * call it yourself from any sysHandler to fully control heartbeat lifecycle.
823
896
  */
824
897
  stopHeartbeat() {
898
+ const hadTimer = Boolean(this.hbTimer);
825
899
  if (this.hbTimer) {
826
900
  clearInterval(this.hbTimer);
827
901
  this.hbTimer = null;
828
902
  }
903
+ this.dbg({
904
+ type: "heartbeat",
905
+ action: "stop",
906
+ details: {
907
+ hadTimer
908
+ }
909
+ });
829
910
  }
830
- emit(event, payload, metadata, onAck, timeoutMs) {
831
- const schema = this.events[event].message;
832
- const parsed = schema.safeParse(payload);
833
- if (!parsed.success) throw new Error(`Invalid payload for "${event}": ${parsed.error.message}`);
911
+ emit(event, payload, metadata) {
834
912
  if (!this.socket) {
835
- if (this.environment === "development") {
836
- console.warn(`[socket] emit("${String(event)}") skipped because socket is null`);
837
- }
913
+ this.dbg({ type: "emit", event, err: "Socket is null" });
838
914
  return;
839
915
  }
840
- if (onAck) {
841
- this.socket.timeout(timeoutMs ?? this.hb.timeoutMs).emit(String(event), parsed.data, (ack) => {
842
- try {
843
- onAck(ack);
844
- } catch {
845
- }
916
+ const schema = this.events[event].message;
917
+ const parsed = schema.safeParse(payload);
918
+ if (!parsed.success) {
919
+ this.dbg({
920
+ type: "emit",
921
+ event,
922
+ err: "payload_validation_failed",
923
+ details: this.getValidationDetails(parsed.error)
846
924
  });
847
- } else {
848
- this.socket.emit(String(event), parsed.data);
925
+ throw new Error(`Invalid payload for "${event}": ${parsed.error.message}`);
849
926
  }
927
+ this.socket.emit(String(event), parsed.data);
850
928
  this.dbg({
851
929
  type: "emit",
852
930
  event,
853
- metadata: this.debug.verbose ? metadata : void 0
931
+ metadata
854
932
  });
855
933
  }
856
- joinRooms(rooms) {
934
+ joinRooms(rooms, meta) {
935
+ if (!this.socket) {
936
+ this.dbg({ type: "room", action: "join", rooms: this.toArray(rooms), err: "Socket is null" });
937
+ throw new Error("Socket is null in joinRooms method");
938
+ }
939
+ if (!this.getSysEvent("sys:room_join")({
940
+ rooms,
941
+ meta,
942
+ socket: this.socket,
943
+ client: this
944
+ })) {
945
+ this.dbg({ type: "room", action: "join", rooms: this.toArray(rooms), err: "sys:room_join handler aborted join" });
946
+ return;
947
+ }
857
948
  const list = this.toArray(rooms);
858
949
  const toJoin = [];
859
950
  for (const r of list) {
@@ -862,27 +953,41 @@ var SocketClient = class {
862
953
  if (next === 1) toJoin.push(r);
863
954
  }
864
955
  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",
956
+ const payloadResult = this.roomJoinSchema.safeParse({
957
+ rooms: toJoin,
958
+ meta
959
+ });
960
+ if (!payloadResult.success) {
961
+ this.rollbackJoinIncrement(toJoin);
962
+ this.dbg({
963
+ type: "room",
964
+ action: "join",
874
965
  rooms: toJoin,
875
- socket,
876
- next: defaultHandler,
877
- startHeartbeat: () => this.startHeartbeat(),
878
- stopHeartbeat: () => this.stopHeartbeat()
966
+ err: "payload validation failed",
967
+ details: this.getValidationDetails(payloadResult.error)
879
968
  });
880
- } else {
881
- defaultHandler();
969
+ return;
882
970
  }
971
+ const payload = payloadResult.data;
972
+ const normalizedRooms = this.toArray(payload.rooms);
973
+ this.socket.emit("sys:room_join", payload);
974
+ this.dbg({ type: "room", action: "join", rooms: normalizedRooms });
883
975
  }
884
976
  }
885
- leaveRooms(rooms) {
977
+ leaveRooms(rooms, meta) {
978
+ if (!this.socket) {
979
+ this.dbg({ type: "room", action: "leave", rooms: this.toArray(rooms), err: "Socket is null" });
980
+ throw new Error("Socket is null in leaveRooms method");
981
+ }
982
+ if (!this.getSysEvent("sys:room_leave")({
983
+ rooms,
984
+ meta,
985
+ socket: this.socket,
986
+ client: this
987
+ })) {
988
+ this.dbg({ type: "room", action: "leave", rooms: this.toArray(rooms), err: "sys:room_leave handler aborted leave" });
989
+ return;
990
+ }
886
991
  const list = this.toArray(rooms);
887
992
  const toLeave = [];
888
993
  for (const r of list) {
@@ -893,42 +998,49 @@ var SocketClient = class {
893
998
  else this.roomCounts.set(r, next);
894
999
  }
895
1000
  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",
1001
+ const payloadResult = this.roomLeaveSchema.safeParse({
1002
+ rooms: toLeave,
1003
+ meta
1004
+ });
1005
+ if (!payloadResult.success) {
1006
+ this.rollbackLeaveDecrement(toLeave);
1007
+ this.dbg({
1008
+ type: "room",
1009
+ action: "leave",
905
1010
  rooms: toLeave,
906
- socket,
907
- next: defaultHandler,
908
- startHeartbeat: () => this.startHeartbeat(),
909
- stopHeartbeat: () => this.stopHeartbeat()
1011
+ err: "payload validation failed",
1012
+ details: this.getValidationDetails(payloadResult.error)
910
1013
  });
911
- } else {
912
- defaultHandler();
1014
+ return;
913
1015
  }
1016
+ const payload = payloadResult.data;
1017
+ const normalizedRooms = this.toArray(payload.rooms);
1018
+ this.socket.emit("sys:room_leave", payload);
1019
+ this.dbg({ type: "room", action: "leave", rooms: normalizedRooms });
914
1020
  }
915
1021
  }
916
1022
  on(event, handler) {
917
1023
  const schema = this.events[event].message;
918
1024
  this.dbg({ type: "register", action: "register", event });
919
1025
  if (!this.socket) {
920
- if (this.environment === "development") {
921
- console.warn(`[socket] on("${String(event)}") skipped because socket is null`);
922
- }
1026
+ this.dbg({ type: "register", action: "register", event, err: "Socket is null" });
923
1027
  return () => {
924
1028
  };
925
1029
  }
926
1030
  const socket = this.socket;
927
- const wrapped = (envelopeOrRaw, maybeAck) => {
1031
+ const wrapped = (envelopeOrRaw) => {
928
1032
  const maybeEnvelope = envelopeOrRaw;
929
1033
  const rawData = maybeEnvelope?.data ?? maybeEnvelope;
930
1034
  const parsed = schema.safeParse(rawData);
931
- if (!parsed.success) return;
1035
+ if (!parsed.success) {
1036
+ this.dbg({
1037
+ type: "receive",
1038
+ event,
1039
+ err: "payload_validation_failed",
1040
+ details: this.getValidationDetails(parsed.error)
1041
+ });
1042
+ return;
1043
+ }
932
1044
  const receivedAt = /* @__PURE__ */ new Date();
933
1045
  const sentAt = maybeEnvelope?.sentAt ? new Date(maybeEnvelope.sentAt) : void 0;
934
1046
  const meta = {
@@ -942,15 +1054,9 @@ var SocketClient = class {
942
1054
  ctx: {
943
1055
  receivedAt,
944
1056
  latencyMs: sentAt ? Math.max(0, receivedAt.getTime() - sentAt.getTime()) : void 0,
945
- nsp: socket.nsp,
1057
+ nsp: this.getNamespace(socket),
946
1058
  socketId: socket.id,
947
- socket,
948
- reply: typeof maybeAck === "function" ? (d) => {
949
- try {
950
- maybeAck(d);
951
- } catch {
952
- }
953
- } : void 0
1059
+ socket
954
1060
  }
955
1061
  };
956
1062
  this.dbg({
@@ -966,9 +1072,7 @@ var SocketClient = class {
966
1072
  handler(parsed.data, meta);
967
1073
  };
968
1074
  const errorWrapped = (e) => {
969
- if (this.environment === "development") {
970
- console.warn(`[socket] ${String(event)}:error`, e);
971
- }
1075
+ this.dbg({ type: "receive", event, err: String(e) });
972
1076
  };
973
1077
  socket.on(String(event), wrapped);
974
1078
  socket.on(`${String(event)}:error`, errorWrapped);
@@ -994,9 +1098,18 @@ var SocketClient = class {
994
1098
  * Remove all listeners, stop timers, and leave rooms.
995
1099
  * Call when disposing the client instance.
996
1100
  */
997
- destroy() {
1101
+ destroy(leaveMeta) {
998
1102
  this.stopHeartbeat();
999
1103
  const socket = this.socket;
1104
+ this.dbg({
1105
+ type: "lifecycle",
1106
+ phase: "destroy_begin",
1107
+ socketId: socket?.id,
1108
+ details: {
1109
+ roomsTracked: this.roomCounts.size,
1110
+ handlerEvents: this.handlerMap.size
1111
+ }
1112
+ });
1000
1113
  if (socket) {
1001
1114
  socket.off("connect", this.onConnect);
1002
1115
  socket.off("reconnect", this.onReconnect);
@@ -1013,42 +1126,67 @@ var SocketClient = class {
1013
1126
  this.handlerMap.clear();
1014
1127
  const toLeave = Array.from(this.roomCounts.entries()).filter(([, count]) => count > 0).map(([room]) => room);
1015
1128
  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
- }
1129
+ this.leaveRooms(toLeave, leaveMeta);
1033
1130
  }
1034
1131
  this.roomCounts.clear();
1132
+ this.dbg({
1133
+ type: "lifecycle",
1134
+ phase: "destroy_complete",
1135
+ socketId: socket?.id,
1136
+ details: {
1137
+ roomsTracked: this.roomCounts.size,
1138
+ handlerEvents: this.handlerMap.size
1139
+ }
1140
+ });
1035
1141
  }
1036
1142
  /** Pass-throughs. Managing connection is the caller’s responsibility. */
1037
1143
  disconnect() {
1038
- if (!this.socket) return;
1144
+ if (!this.socket) {
1145
+ this.dbg({
1146
+ type: "connection",
1147
+ phase: "disconnect",
1148
+ reason: "client_disconnect",
1149
+ err: "Socket is null"
1150
+ });
1151
+ return;
1152
+ }
1039
1153
  this.stopHeartbeat();
1040
1154
  this.socket.disconnect();
1041
- this.dbg({ type: "connection", phase: "disconnect", reason: "client_disconnect" });
1155
+ this.dbg({
1156
+ type: "connection",
1157
+ phase: "disconnect",
1158
+ reason: "client_disconnect",
1159
+ details: {
1160
+ nsp: this.getNamespace(this.socket)
1161
+ }
1162
+ });
1163
+ this.logSocketConfigSnapshot("update", "disconnect_call");
1042
1164
  }
1043
1165
  connect() {
1044
- if (!this.socket) return;
1166
+ if (!this.socket) {
1167
+ this.dbg({
1168
+ type: "connection",
1169
+ phase: "connect",
1170
+ err: "Socket is null"
1171
+ });
1172
+ return;
1173
+ }
1045
1174
  this.socket.connect();
1046
- this.dbg({ type: "connection", phase: "connect", id: this.socket.id ?? "" });
1175
+ this.dbg({
1176
+ type: "connection",
1177
+ phase: "connect",
1178
+ id: this.socket.id ?? "",
1179
+ details: {
1180
+ nsp: this.getNamespace(this.socket)
1181
+ }
1182
+ });
1183
+ this.logSocketConfigSnapshot("update", "connect_call");
1047
1184
  }
1048
1185
  };
1049
1186
  // Annotate the CommonJS export names for ESM import in node:
1050
1187
  0 && (module.exports = {
1051
1188
  SocketClient,
1189
+ buildRoomPayloadSchema,
1052
1190
  buildSocketProvider,
1053
1191
  createRouteClient,
1054
1192
  defaultFetcher