@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 +393 -255
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +392 -255
- package/dist/index.mjs.map +1 -1
- package/dist/sockets/socket.client.context.d.ts +12 -8
- package/dist/sockets/socket.client.debug.d.ts +71 -0
- package/dist/sockets/socket.client.index.d.ts +23 -168
- package/dist/sockets/socket.client.sys.d.ts +113 -0
- package/package.json +2 -2
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.
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
|
686
|
-
|
|
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: "
|
|
697
|
-
|
|
698
|
-
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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: "
|
|
794
|
-
|
|
858
|
+
action: "tick_skip",
|
|
859
|
+
err: "Socket missing during heartbeat tick"
|
|
795
860
|
});
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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
|
-
|
|
812
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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
|
-
|
|
876
|
-
|
|
877
|
-
startHeartbeat: () => this.startHeartbeat(),
|
|
878
|
-
stopHeartbeat: () => this.stopHeartbeat()
|
|
966
|
+
err: "payload validation failed",
|
|
967
|
+
details: this.getValidationDetails(payloadResult.error)
|
|
879
968
|
});
|
|
880
|
-
|
|
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
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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
|
-
|
|
907
|
-
|
|
908
|
-
startHeartbeat: () => this.startHeartbeat(),
|
|
909
|
-
stopHeartbeat: () => this.stopHeartbeat()
|
|
1011
|
+
err: "payload validation failed",
|
|
1012
|
+
details: this.getValidationDetails(payloadResult.error)
|
|
910
1013
|
});
|
|
911
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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({
|
|
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)
|
|
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({
|
|
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
|