@muhkoo/connect 0.2.0-alpha.2 → 0.3.0-alpha.0
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/browser/index.js +580 -275
- package/dist/browser/index.js.map +1 -1
- package/dist/connect.d.ts +335 -120
- package/dist/server/index.js +1204 -899
- package/dist/server/index.js.map +1 -1
- package/dist/workers/index.js +118 -3
- package/dist/workers/index.js.map +1 -1
- package/package.json +1 -1
package/dist/workers/index.js
CHANGED
|
@@ -1889,10 +1889,29 @@ function isCipherFrame(frame) {
|
|
|
1889
1889
|
* - Emit lifecycle events (CONNECTED / DISCONNECTED / RECONNECTING / ERROR)
|
|
1890
1890
|
* plus raw inbound MESSAGE events
|
|
1891
1891
|
*/
|
|
1892
|
+
/** Monotonic source of unique transport ids when a caller doesn't supply one. */
|
|
1893
|
+
let __wsInstanceSeq = 0;
|
|
1892
1894
|
class WSTransport extends EventCore {
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1895
|
+
/**
|
|
1896
|
+
* Per-instance event scope. `EventCore` is a process-global static emitter,
|
|
1897
|
+
* so without namespacing every WSTransport would hear every other one's
|
|
1898
|
+
* frames. We prefix each event with this transport's `id` and expose
|
|
1899
|
+
* `on`/`off`/`emit` that scope transparently — callers still use the plain
|
|
1900
|
+
* `EventCoreEvents` enum and only ever see their own connection's events.
|
|
1901
|
+
*/
|
|
1902
|
+
id;
|
|
1903
|
+
emit = (event, data) => {
|
|
1904
|
+
EventCore.emit(this.scopedEvent(event), data);
|
|
1905
|
+
};
|
|
1906
|
+
on = (event, handler) => {
|
|
1907
|
+
EventCore.on(this.scopedEvent(event), handler);
|
|
1908
|
+
};
|
|
1909
|
+
off = (event, handler) => {
|
|
1910
|
+
EventCore.off(this.scopedEvent(event), handler);
|
|
1911
|
+
};
|
|
1912
|
+
scopedEvent(event) {
|
|
1913
|
+
return `${this.id}:${event}`;
|
|
1914
|
+
}
|
|
1896
1915
|
url;
|
|
1897
1916
|
socket = null;
|
|
1898
1917
|
_isConnected = false;
|
|
@@ -1906,16 +1925,28 @@ class WSTransport extends EventCore {
|
|
|
1906
1925
|
// Outbound queue (raw frame strings — already serialized by caller)
|
|
1907
1926
|
outboundQueue = [];
|
|
1908
1927
|
maxQueueSize;
|
|
1928
|
+
// Heartbeat / liveness state
|
|
1929
|
+
heartbeatInterval;
|
|
1930
|
+
heartbeatTimeout;
|
|
1931
|
+
pingFrame;
|
|
1932
|
+
pongFrame;
|
|
1933
|
+
heartbeatTimer = null;
|
|
1934
|
+
pongTimer = null;
|
|
1909
1935
|
constructor(opts) {
|
|
1910
1936
|
super();
|
|
1911
1937
|
if (!opts || !opts.url) {
|
|
1912
1938
|
throw new Error("WSTransport: `url` is required");
|
|
1913
1939
|
}
|
|
1914
1940
|
this.url = opts.url;
|
|
1941
|
+
this.id = opts.id ?? `ws${++__wsInstanceSeq}`;
|
|
1915
1942
|
this.autoReconnect = opts.autoReconnect ?? true;
|
|
1916
1943
|
this.reconnectDelay = opts.reconnectDelay ?? 3000;
|
|
1917
1944
|
this.maxReconnectAttempts = opts.maxReconnectAttempts ?? 5;
|
|
1918
1945
|
this.maxQueueSize = opts.maxQueueSize ?? 100;
|
|
1946
|
+
this.heartbeatInterval = opts.heartbeatInterval ?? 30000;
|
|
1947
|
+
this.heartbeatTimeout = opts.heartbeatTimeout ?? 10000;
|
|
1948
|
+
this.pingFrame = opts.pingFrame ?? "ping";
|
|
1949
|
+
this.pongFrame = opts.pongFrame ?? "pong";
|
|
1919
1950
|
}
|
|
1920
1951
|
/** Open the socket. Resolves when CONNECTED fires. Rejects on first error. */
|
|
1921
1952
|
async connect() {
|
|
@@ -1933,9 +1964,19 @@ class WSTransport extends EventCore {
|
|
|
1933
1964
|
this.reconnectAttempts = 0;
|
|
1934
1965
|
this.emit(EventCoreEvents.CONNECTED, undefined);
|
|
1935
1966
|
this.flushQueue();
|
|
1967
|
+
this.startHeartbeat();
|
|
1936
1968
|
resolve();
|
|
1937
1969
|
};
|
|
1938
1970
|
socket.onmessage = (event) => {
|
|
1971
|
+
// Heartbeat ack — swallow it (don't surface as a MESSAGE) and clear
|
|
1972
|
+
// the liveness deadline. The layer above never sees keep-alive frames.
|
|
1973
|
+
if (event.data === this.pongFrame) {
|
|
1974
|
+
if (this.pongTimer) {
|
|
1975
|
+
clearTimeout(this.pongTimer);
|
|
1976
|
+
this.pongTimer = null;
|
|
1977
|
+
}
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1939
1980
|
// Raw inbound frame — layer above is responsible for parsing.
|
|
1940
1981
|
this.emit(EventCoreEvents.MESSAGE, event.data);
|
|
1941
1982
|
};
|
|
@@ -1950,6 +1991,7 @@ class WSTransport extends EventCore {
|
|
|
1950
1991
|
const wasConnected = this._isConnected;
|
|
1951
1992
|
this._isConnected = false;
|
|
1952
1993
|
this._isConnecting = false;
|
|
1994
|
+
this.stopHeartbeat();
|
|
1953
1995
|
this.emit(EventCoreEvents.DISCONNECTED, undefined);
|
|
1954
1996
|
if (this.autoReconnect && wasConnected) {
|
|
1955
1997
|
this.scheduleReconnect();
|
|
@@ -1968,6 +2010,7 @@ class WSTransport extends EventCore {
|
|
|
1968
2010
|
clearTimeout(this.reconnectTimeout);
|
|
1969
2011
|
this.reconnectTimeout = null;
|
|
1970
2012
|
}
|
|
2013
|
+
this.stopHeartbeat();
|
|
1971
2014
|
this.autoReconnect = false;
|
|
1972
2015
|
if (this.socket) {
|
|
1973
2016
|
try {
|
|
@@ -1998,6 +2041,78 @@ class WSTransport extends EventCore {
|
|
|
1998
2041
|
queuedFrames() { return this.outboundQueue.length; }
|
|
1999
2042
|
reconnectAttemptCount() { return this.reconnectAttempts; }
|
|
2000
2043
|
// -- internals -------------------------------------------------------------
|
|
2044
|
+
/** Begin the keep-alive loop (no-op when disabled). */
|
|
2045
|
+
startHeartbeat() {
|
|
2046
|
+
if (this.heartbeatInterval <= 0)
|
|
2047
|
+
return;
|
|
2048
|
+
this.stopHeartbeat();
|
|
2049
|
+
this.heartbeatTimer = setInterval(() => this.sendHeartbeat(), this.heartbeatInterval);
|
|
2050
|
+
}
|
|
2051
|
+
/** Stop the keep-alive loop and clear any pending liveness deadline. */
|
|
2052
|
+
stopHeartbeat() {
|
|
2053
|
+
if (this.heartbeatTimer) {
|
|
2054
|
+
clearInterval(this.heartbeatTimer);
|
|
2055
|
+
this.heartbeatTimer = null;
|
|
2056
|
+
}
|
|
2057
|
+
if (this.pongTimer) {
|
|
2058
|
+
clearTimeout(this.pongTimer);
|
|
2059
|
+
this.pongTimer = null;
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
/**
|
|
2063
|
+
* Send one ping and arm a liveness deadline. If no `pongFrame` arrives before
|
|
2064
|
+
* `heartbeatTimeout`, the connection is considered dead and we force a
|
|
2065
|
+
* reconnect — this is what catches a silently-dropped TCP connection that
|
|
2066
|
+
* never fired `onclose` (the failure mode that "loses connection with the
|
|
2067
|
+
* Accelerator" on long sessions).
|
|
2068
|
+
*/
|
|
2069
|
+
sendHeartbeat() {
|
|
2070
|
+
if (!this._isConnected || !this.socket)
|
|
2071
|
+
return;
|
|
2072
|
+
try {
|
|
2073
|
+
this.socket.send(this.pingFrame);
|
|
2074
|
+
}
|
|
2075
|
+
catch {
|
|
2076
|
+
this.forceReconnect();
|
|
2077
|
+
return;
|
|
2078
|
+
}
|
|
2079
|
+
// A still-pending deadline means the previous pong never came back.
|
|
2080
|
+
if (this.pongTimer)
|
|
2081
|
+
return;
|
|
2082
|
+
this.pongTimer = setTimeout(() => {
|
|
2083
|
+
this.pongTimer = null;
|
|
2084
|
+
this.forceReconnect();
|
|
2085
|
+
}, this.heartbeatTimeout);
|
|
2086
|
+
}
|
|
2087
|
+
/**
|
|
2088
|
+
* Tear down a connection we believe is dead and kick off reconnect, without
|
|
2089
|
+
* waiting on the OS/browser to eventually fire `onclose` (which a half-open
|
|
2090
|
+
* socket may never do). We detach the handlers first so the normal close path
|
|
2091
|
+
* can't double-fire the reconnect.
|
|
2092
|
+
*/
|
|
2093
|
+
forceReconnect() {
|
|
2094
|
+
this.stopHeartbeat();
|
|
2095
|
+
const sock = this.socket;
|
|
2096
|
+
const wasConnected = this._isConnected;
|
|
2097
|
+
this.socket = null;
|
|
2098
|
+
this._isConnected = false;
|
|
2099
|
+
this._isConnecting = false;
|
|
2100
|
+
if (sock) {
|
|
2101
|
+
sock.onopen = null;
|
|
2102
|
+
sock.onmessage = null;
|
|
2103
|
+
sock.onerror = null;
|
|
2104
|
+
sock.onclose = null;
|
|
2105
|
+
try {
|
|
2106
|
+
sock.close();
|
|
2107
|
+
}
|
|
2108
|
+
catch { /* already gone */ }
|
|
2109
|
+
}
|
|
2110
|
+
this.emit(EventCoreEvents.ERROR, new Error("WSTransport: heartbeat timeout"));
|
|
2111
|
+
this.emit(EventCoreEvents.DISCONNECTED, undefined);
|
|
2112
|
+
if (this.autoReconnect && wasConnected) {
|
|
2113
|
+
this.scheduleReconnect();
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2001
2116
|
scheduleReconnect() {
|
|
2002
2117
|
if (this.maxReconnectAttempts > 0 &&
|
|
2003
2118
|
this.reconnectAttempts >= this.maxReconnectAttempts) {
|