@muhkoo/connect 0.2.0-alpha.2 → 0.4.0-alpha.1
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/README.md +73 -6
- package/dist/browser/index.js +844 -282
- package/dist/browser/index.js.map +1 -1
- package/dist/connect.d.ts +661 -121
- package/dist/server/index.js +1456 -894
- package/dist/server/index.js.map +1 -1
- package/dist/workers/index.js +138 -4
- package/dist/workers/index.js.map +1 -1
- package/package.json +1 -1
package/dist/workers/index.js
CHANGED
|
@@ -1889,11 +1889,31 @@ 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;
|
|
1916
|
+
urlProvider;
|
|
1897
1917
|
socket = null;
|
|
1898
1918
|
_isConnected = false;
|
|
1899
1919
|
_isConnecting = false;
|
|
@@ -1906,16 +1926,29 @@ class WSTransport extends EventCore {
|
|
|
1906
1926
|
// Outbound queue (raw frame strings — already serialized by caller)
|
|
1907
1927
|
outboundQueue = [];
|
|
1908
1928
|
maxQueueSize;
|
|
1929
|
+
// Heartbeat / liveness state
|
|
1930
|
+
heartbeatInterval;
|
|
1931
|
+
heartbeatTimeout;
|
|
1932
|
+
pingFrame;
|
|
1933
|
+
pongFrame;
|
|
1934
|
+
heartbeatTimer = null;
|
|
1935
|
+
pongTimer = null;
|
|
1909
1936
|
constructor(opts) {
|
|
1910
1937
|
super();
|
|
1911
1938
|
if (!opts || !opts.url) {
|
|
1912
1939
|
throw new Error("WSTransport: `url` is required");
|
|
1913
1940
|
}
|
|
1914
1941
|
this.url = opts.url;
|
|
1942
|
+
this.urlProvider = opts.urlProvider;
|
|
1943
|
+
this.id = opts.id ?? `ws${++__wsInstanceSeq}`;
|
|
1915
1944
|
this.autoReconnect = opts.autoReconnect ?? true;
|
|
1916
1945
|
this.reconnectDelay = opts.reconnectDelay ?? 3000;
|
|
1917
1946
|
this.maxReconnectAttempts = opts.maxReconnectAttempts ?? 5;
|
|
1918
1947
|
this.maxQueueSize = opts.maxQueueSize ?? 100;
|
|
1948
|
+
this.heartbeatInterval = opts.heartbeatInterval ?? 30000;
|
|
1949
|
+
this.heartbeatTimeout = opts.heartbeatTimeout ?? 10000;
|
|
1950
|
+
this.pingFrame = opts.pingFrame ?? "ping";
|
|
1951
|
+
this.pongFrame = opts.pongFrame ?? "pong";
|
|
1919
1952
|
}
|
|
1920
1953
|
/** Open the socket. Resolves when CONNECTED fires. Rejects on first error. */
|
|
1921
1954
|
async connect() {
|
|
@@ -1923,6 +1956,18 @@ class WSTransport extends EventCore {
|
|
|
1923
1956
|
throw new Error("WSTransport: already connected or connecting");
|
|
1924
1957
|
}
|
|
1925
1958
|
this._isConnecting = true;
|
|
1959
|
+
// On a RECONNECT, refresh the URL (the baked-in single-use ticket is spent).
|
|
1960
|
+
// The first connect uses `url` as-is. If the provider throws (e.g. can't
|
|
1961
|
+
// mint a ticket), surface it and let scheduleReconnect retry.
|
|
1962
|
+
if (this.urlProvider && this.reconnectAttempts > 0) {
|
|
1963
|
+
try {
|
|
1964
|
+
this.url = await this.urlProvider();
|
|
1965
|
+
}
|
|
1966
|
+
catch (err) {
|
|
1967
|
+
this._isConnecting = false;
|
|
1968
|
+
throw err;
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1926
1971
|
return new Promise((resolve, reject) => {
|
|
1927
1972
|
try {
|
|
1928
1973
|
const socket = new WebSocket(this.url);
|
|
@@ -1933,9 +1978,19 @@ class WSTransport extends EventCore {
|
|
|
1933
1978
|
this.reconnectAttempts = 0;
|
|
1934
1979
|
this.emit(EventCoreEvents.CONNECTED, undefined);
|
|
1935
1980
|
this.flushQueue();
|
|
1981
|
+
this.startHeartbeat();
|
|
1936
1982
|
resolve();
|
|
1937
1983
|
};
|
|
1938
1984
|
socket.onmessage = (event) => {
|
|
1985
|
+
// Heartbeat ack — swallow it (don't surface as a MESSAGE) and clear
|
|
1986
|
+
// the liveness deadline. The layer above never sees keep-alive frames.
|
|
1987
|
+
if (event.data === this.pongFrame) {
|
|
1988
|
+
if (this.pongTimer) {
|
|
1989
|
+
clearTimeout(this.pongTimer);
|
|
1990
|
+
this.pongTimer = null;
|
|
1991
|
+
}
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1939
1994
|
// Raw inbound frame — layer above is responsible for parsing.
|
|
1940
1995
|
this.emit(EventCoreEvents.MESSAGE, event.data);
|
|
1941
1996
|
};
|
|
@@ -1950,6 +2005,7 @@ class WSTransport extends EventCore {
|
|
|
1950
2005
|
const wasConnected = this._isConnected;
|
|
1951
2006
|
this._isConnected = false;
|
|
1952
2007
|
this._isConnecting = false;
|
|
2008
|
+
this.stopHeartbeat();
|
|
1953
2009
|
this.emit(EventCoreEvents.DISCONNECTED, undefined);
|
|
1954
2010
|
if (this.autoReconnect && wasConnected) {
|
|
1955
2011
|
this.scheduleReconnect();
|
|
@@ -1968,6 +2024,7 @@ class WSTransport extends EventCore {
|
|
|
1968
2024
|
clearTimeout(this.reconnectTimeout);
|
|
1969
2025
|
this.reconnectTimeout = null;
|
|
1970
2026
|
}
|
|
2027
|
+
this.stopHeartbeat();
|
|
1971
2028
|
this.autoReconnect = false;
|
|
1972
2029
|
if (this.socket) {
|
|
1973
2030
|
try {
|
|
@@ -1998,6 +2055,78 @@ class WSTransport extends EventCore {
|
|
|
1998
2055
|
queuedFrames() { return this.outboundQueue.length; }
|
|
1999
2056
|
reconnectAttemptCount() { return this.reconnectAttempts; }
|
|
2000
2057
|
// -- internals -------------------------------------------------------------
|
|
2058
|
+
/** Begin the keep-alive loop (no-op when disabled). */
|
|
2059
|
+
startHeartbeat() {
|
|
2060
|
+
if (this.heartbeatInterval <= 0)
|
|
2061
|
+
return;
|
|
2062
|
+
this.stopHeartbeat();
|
|
2063
|
+
this.heartbeatTimer = setInterval(() => this.sendHeartbeat(), this.heartbeatInterval);
|
|
2064
|
+
}
|
|
2065
|
+
/** Stop the keep-alive loop and clear any pending liveness deadline. */
|
|
2066
|
+
stopHeartbeat() {
|
|
2067
|
+
if (this.heartbeatTimer) {
|
|
2068
|
+
clearInterval(this.heartbeatTimer);
|
|
2069
|
+
this.heartbeatTimer = null;
|
|
2070
|
+
}
|
|
2071
|
+
if (this.pongTimer) {
|
|
2072
|
+
clearTimeout(this.pongTimer);
|
|
2073
|
+
this.pongTimer = null;
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* Send one ping and arm a liveness deadline. If no `pongFrame` arrives before
|
|
2078
|
+
* `heartbeatTimeout`, the connection is considered dead and we force a
|
|
2079
|
+
* reconnect — this is what catches a silently-dropped TCP connection that
|
|
2080
|
+
* never fired `onclose` (the failure mode that "loses connection with the
|
|
2081
|
+
* Accelerator" on long sessions).
|
|
2082
|
+
*/
|
|
2083
|
+
sendHeartbeat() {
|
|
2084
|
+
if (!this._isConnected || !this.socket)
|
|
2085
|
+
return;
|
|
2086
|
+
try {
|
|
2087
|
+
this.socket.send(this.pingFrame);
|
|
2088
|
+
}
|
|
2089
|
+
catch {
|
|
2090
|
+
this.forceReconnect();
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
// A still-pending deadline means the previous pong never came back.
|
|
2094
|
+
if (this.pongTimer)
|
|
2095
|
+
return;
|
|
2096
|
+
this.pongTimer = setTimeout(() => {
|
|
2097
|
+
this.pongTimer = null;
|
|
2098
|
+
this.forceReconnect();
|
|
2099
|
+
}, this.heartbeatTimeout);
|
|
2100
|
+
}
|
|
2101
|
+
/**
|
|
2102
|
+
* Tear down a connection we believe is dead and kick off reconnect, without
|
|
2103
|
+
* waiting on the OS/browser to eventually fire `onclose` (which a half-open
|
|
2104
|
+
* socket may never do). We detach the handlers first so the normal close path
|
|
2105
|
+
* can't double-fire the reconnect.
|
|
2106
|
+
*/
|
|
2107
|
+
forceReconnect() {
|
|
2108
|
+
this.stopHeartbeat();
|
|
2109
|
+
const sock = this.socket;
|
|
2110
|
+
const wasConnected = this._isConnected;
|
|
2111
|
+
this.socket = null;
|
|
2112
|
+
this._isConnected = false;
|
|
2113
|
+
this._isConnecting = false;
|
|
2114
|
+
if (sock) {
|
|
2115
|
+
sock.onopen = null;
|
|
2116
|
+
sock.onmessage = null;
|
|
2117
|
+
sock.onerror = null;
|
|
2118
|
+
sock.onclose = null;
|
|
2119
|
+
try {
|
|
2120
|
+
sock.close();
|
|
2121
|
+
}
|
|
2122
|
+
catch { /* already gone */ }
|
|
2123
|
+
}
|
|
2124
|
+
this.emit(EventCoreEvents.ERROR, new Error("WSTransport: heartbeat timeout"));
|
|
2125
|
+
this.emit(EventCoreEvents.DISCONNECTED, undefined);
|
|
2126
|
+
if (this.autoReconnect && wasConnected) {
|
|
2127
|
+
this.scheduleReconnect();
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2001
2130
|
scheduleReconnect() {
|
|
2002
2131
|
if (this.maxReconnectAttempts > 0 &&
|
|
2003
2132
|
this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
@@ -2011,7 +2140,12 @@ class WSTransport extends EventCore {
|
|
|
2011
2140
|
await this.connect();
|
|
2012
2141
|
}
|
|
2013
2142
|
catch {
|
|
2014
|
-
//
|
|
2143
|
+
// A failed reconnect never opened the socket, so its `onclose` won't
|
|
2144
|
+
// re-schedule (that path is guarded by `wasConnected`). Re-schedule here
|
|
2145
|
+
// so we keep retrying (with a fresh ticket each time) until we succeed
|
|
2146
|
+
// or hit the attempt cap.
|
|
2147
|
+
if (this.autoReconnect)
|
|
2148
|
+
this.scheduleReconnect();
|
|
2015
2149
|
}
|
|
2016
2150
|
}, this.reconnectDelay);
|
|
2017
2151
|
}
|