hypha-rpc 0.21.44 → 0.21.47
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/coverage/html/index.html +1 -1
- package/dist/hypha-rpc-websocket.js +206 -2
- package/dist/hypha-rpc-websocket.js.map +1 -1
- package/dist/hypha-rpc-websocket.min.js +1 -1
- package/dist/hypha-rpc-websocket.min.js.map +1 -1
- package/dist/hypha-rpc-websocket.min.mjs +1 -1
- package/dist/hypha-rpc-websocket.min.mjs.map +1 -1
- package/dist/hypha-rpc-websocket.mjs +208 -3
- package/dist/hypha-rpc-websocket.mjs.map +1 -1
- package/index.mjs +1 -0
- package/package.json +1 -1
- package/src/websocket-client.js +205 -2
package/coverage/html/index.html
CHANGED
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
87
87
|
Code coverage generated by
|
|
88
88
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
89
|
-
at 2026-
|
|
89
|
+
at 2026-08-31T13:24:20.113Z
|
|
90
90
|
</div>
|
|
91
91
|
<script src="prettify.js"></script>
|
|
92
92
|
<script>
|
|
@@ -10389,6 +10389,7 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
10389
10389
|
/* harmony export */ getRemoteService: () => (/* binding */ getRemoteService),
|
|
10390
10390
|
/* harmony export */ getRemoteServiceHTTP: () => (/* reexport safe */ _http_client_js__WEBPACK_IMPORTED_MODULE_5__.getRemoteServiceHTTP),
|
|
10391
10391
|
/* harmony export */ hyphaWebsocketClient: () => (/* binding */ hyphaWebsocketClient),
|
|
10392
|
+
/* harmony export */ isLoginCompleteMessage: () => (/* binding */ isLoginCompleteMessage),
|
|
10392
10393
|
/* harmony export */ loadRequirements: () => (/* reexport safe */ _utils_index_js__WEBPACK_IMPORTED_MODULE_2__.loadRequirements),
|
|
10393
10394
|
/* harmony export */ login: () => (/* binding */ login),
|
|
10394
10395
|
/* harmony export */ logout: () => (/* binding */ logout),
|
|
@@ -10424,6 +10425,16 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
10424
10425
|
|
|
10425
10426
|
const MAX_RETRY = 1000000;
|
|
10426
10427
|
|
|
10428
|
+
// Reconnect flap control: a connection that stays up for less than
|
|
10429
|
+
// RECONNECT_FLAP_WINDOW_MS after connecting is treated as a "flap" (e.g. the
|
|
10430
|
+
// server accepts the reconnect then immediately supersedes/closes it because a
|
|
10431
|
+
// prior session with the same client_id is still active). Flaps escalate the
|
|
10432
|
+
// backoff across reconnect cycles — instead of resetting to an immediate retry
|
|
10433
|
+
// each time — and never reconnect faster than RECONNECT_MIN_DELAY_MS while
|
|
10434
|
+
// flapping. This prevents a tight succeed→superseded→succeed reconnect storm.
|
|
10435
|
+
const RECONNECT_FLAP_WINDOW_MS = 5000;
|
|
10436
|
+
const RECONNECT_MIN_DELAY_MS = 1000;
|
|
10437
|
+
|
|
10427
10438
|
// When the socket errors/closes during the handshake, wait briefly before
|
|
10428
10439
|
// rejecting with the generic reason: the server usually sends a descriptive
|
|
10429
10440
|
// {type:"error"} message (e.g. token/workspace mismatch) just before closing,
|
|
@@ -10481,6 +10492,8 @@ class WebsocketRPCConnection {
|
|
|
10481
10492
|
this._reconnecting = false; // Mutex to prevent overlapping reconnection attempts
|
|
10482
10493
|
this._closedDuringReconnect = false; // Flag for close events during reconnection
|
|
10483
10494
|
this._disconnectedNotified = false;
|
|
10495
|
+
this._lastConnectedTime = 0; // ms timestamp of last successful connection_info
|
|
10496
|
+
this._reconnectFlapCount = 0; // consecutive short-lived (flapping) connections
|
|
10484
10497
|
}
|
|
10485
10498
|
|
|
10486
10499
|
/**
|
|
@@ -10633,6 +10646,9 @@ class WebsocketRPCConnection {
|
|
|
10633
10646
|
const first_message = JSON.parse(data);
|
|
10634
10647
|
if (first_message.type == "connection_info") {
|
|
10635
10648
|
settled = true;
|
|
10649
|
+
// Mark when a connection is fully established; used for flap
|
|
10650
|
+
// detection when the connection later closes.
|
|
10651
|
+
this._lastConnectedTime = Date.now();
|
|
10636
10652
|
this.connection_info = first_message;
|
|
10637
10653
|
if (this._workspace) {
|
|
10638
10654
|
(0,_utils_index_js__WEBPACK_IMPORTED_MODULE_2__.assert)(
|
|
@@ -10840,7 +10856,25 @@ class WebsocketRPCConnection {
|
|
|
10840
10856
|
}
|
|
10841
10857
|
this._reconnecting = true;
|
|
10842
10858
|
|
|
10843
|
-
|
|
10859
|
+
// Flap detection: if the connection that just closed was short-lived,
|
|
10860
|
+
// escalate the backoff across reconnect cycles instead of resetting to
|
|
10861
|
+
// an immediate retry. A healthy long-lived connection resets the count
|
|
10862
|
+
// so genuine disconnects still recover fast.
|
|
10863
|
+
const aliveMs = this._lastConnectedTime
|
|
10864
|
+
? Date.now() - this._lastConnectedTime
|
|
10865
|
+
: Infinity;
|
|
10866
|
+
if (aliveMs < RECONNECT_FLAP_WINDOW_MS) {
|
|
10867
|
+
this._reconnectFlapCount += 1;
|
|
10868
|
+
} else {
|
|
10869
|
+
this._reconnectFlapCount = 0;
|
|
10870
|
+
}
|
|
10871
|
+
|
|
10872
|
+
// Seed the retry counter from the flap history so a REPEATED flap
|
|
10873
|
+
// escalates the backoff. The first flap is free (retry 0 → immediate)
|
|
10874
|
+
// so a genuine single quick disconnect still recovers fast; only the
|
|
10875
|
+
// 2nd+ consecutive short-lived connection is delayed, which is the
|
|
10876
|
+
// succeed→superseded storm signature.
|
|
10877
|
+
let retry = Math.max(0, this._reconnectFlapCount - 1);
|
|
10844
10878
|
const baseDelay = 1000; // Start with 1 second
|
|
10845
10879
|
const maxDelay = 60000; // Maximum delay of 60 seconds
|
|
10846
10880
|
const maxJitter = 0.1; // Maximum jitter factor
|
|
@@ -10980,7 +11014,35 @@ class WebsocketRPCConnection {
|
|
|
10980
11014
|
this._reconnect_timeouts.add(timeoutId);
|
|
10981
11015
|
}
|
|
10982
11016
|
};
|
|
10983
|
-
|
|
11017
|
+
|
|
11018
|
+
// Kick off the reconnect loop. When flapping (retry seeded > 0), delay
|
|
11019
|
+
// the first attempt with the same exponential backoff + jitter and a
|
|
11020
|
+
// minimum floor, so a succeed→superseded loop cannot hammer the server.
|
|
11021
|
+
// A genuine disconnect after a healthy connection (retry === 0) still
|
|
11022
|
+
// reconnects immediately.
|
|
11023
|
+
if (retry > 0) {
|
|
11024
|
+
const delay = Math.min(baseDelay * Math.pow(2, retry - 1), maxDelay);
|
|
11025
|
+
const jitter = (Math.random() * 2 - 1) * maxJitter * delay;
|
|
11026
|
+
const finalDelay = Math.max(RECONNECT_MIN_DELAY_MS, delay + jitter);
|
|
11027
|
+
const timeoutId = setTimeout(() => {
|
|
11028
|
+
this._reconnect_timeouts.delete(timeoutId);
|
|
11029
|
+
if (this._closed) {
|
|
11030
|
+
this._reconnecting = false;
|
|
11031
|
+
return;
|
|
11032
|
+
}
|
|
11033
|
+
if (
|
|
11034
|
+
this._websocket &&
|
|
11035
|
+
this._websocket.readyState === WebSocket.OPEN
|
|
11036
|
+
) {
|
|
11037
|
+
this._reconnecting = false;
|
|
11038
|
+
return;
|
|
11039
|
+
}
|
|
11040
|
+
reconnect();
|
|
11041
|
+
}, finalDelay);
|
|
11042
|
+
this._reconnect_timeouts.add(timeoutId);
|
|
11043
|
+
} else {
|
|
11044
|
+
reconnect();
|
|
11045
|
+
}
|
|
10984
11046
|
}
|
|
10985
11047
|
} else {
|
|
10986
11048
|
// Clean up timers in all cases
|
|
@@ -11034,6 +11096,89 @@ function normalizeServerUrl(server_url) {
|
|
|
11034
11096
|
return server_url;
|
|
11035
11097
|
}
|
|
11036
11098
|
|
|
11099
|
+
/**
|
|
11100
|
+
* Validate a window "message" event as a hypha login-complete signal.
|
|
11101
|
+
*
|
|
11102
|
+
* Exported so it can be unit-tested without a browser. The completion message
|
|
11103
|
+
* carries ONLY the public session key (never a token); a valid message merely
|
|
11104
|
+
* decides when to tear down the inline login modal — the token itself is always
|
|
11105
|
+
* fetched over the trusted check() RPC, so a forged message cannot inject one.
|
|
11106
|
+
*/
|
|
11107
|
+
function isLoginCompleteMessage(event, expectedOrigin, expectedKey) {
|
|
11108
|
+
if (!event || event.origin !== expectedOrigin) return false;
|
|
11109
|
+
const data = event.data;
|
|
11110
|
+
if (!data || typeof data !== "object") return false;
|
|
11111
|
+
return data.type === "hypha-login-complete" && data.key === expectedKey;
|
|
11112
|
+
}
|
|
11113
|
+
|
|
11114
|
+
/**
|
|
11115
|
+
* Render the login page in an in-page modal iframe (browser only). This is the
|
|
11116
|
+
* no-popup alternative to opening login_url in a popup window. Returns
|
|
11117
|
+
* { teardown } to remove the modal; the close button invokes onCancel.
|
|
11118
|
+
*/
|
|
11119
|
+
function openLoginModal(loginUrl, { container, onCancel } = {}) {
|
|
11120
|
+
const doc = window.document;
|
|
11121
|
+
const overlay = doc.createElement("div");
|
|
11122
|
+
overlay.setAttribute("data-hypha-login-modal", "");
|
|
11123
|
+
if (!container) {
|
|
11124
|
+
Object.assign(overlay.style, {
|
|
11125
|
+
position: "fixed",
|
|
11126
|
+
inset: "0",
|
|
11127
|
+
zIndex: "2147483647",
|
|
11128
|
+
background: "rgba(0,0,0,0.5)",
|
|
11129
|
+
display: "flex",
|
|
11130
|
+
alignItems: "center",
|
|
11131
|
+
justifyContent: "center",
|
|
11132
|
+
});
|
|
11133
|
+
}
|
|
11134
|
+
const frame = doc.createElement("div");
|
|
11135
|
+
Object.assign(frame.style, {
|
|
11136
|
+
position: "relative",
|
|
11137
|
+
width: "min(480px, 92vw)",
|
|
11138
|
+
height: "min(640px, 90vh)",
|
|
11139
|
+
background: "#fff",
|
|
11140
|
+
borderRadius: "10px",
|
|
11141
|
+
overflow: "hidden",
|
|
11142
|
+
boxShadow: "0 10px 40px rgba(0,0,0,0.3)",
|
|
11143
|
+
});
|
|
11144
|
+
const closeBtn = doc.createElement("button");
|
|
11145
|
+
closeBtn.setAttribute("aria-label", "Close login");
|
|
11146
|
+
closeBtn.textContent = "×";
|
|
11147
|
+
Object.assign(closeBtn.style, {
|
|
11148
|
+
position: "absolute",
|
|
11149
|
+
top: "6px",
|
|
11150
|
+
right: "10px",
|
|
11151
|
+
zIndex: "1",
|
|
11152
|
+
border: "none",
|
|
11153
|
+
background: "transparent",
|
|
11154
|
+
fontSize: "24px",
|
|
11155
|
+
lineHeight: "1",
|
|
11156
|
+
cursor: "pointer",
|
|
11157
|
+
color: "#555",
|
|
11158
|
+
});
|
|
11159
|
+
const iframe = doc.createElement("iframe");
|
|
11160
|
+
iframe.src = loginUrl;
|
|
11161
|
+
iframe.setAttribute("title", "Hypha login");
|
|
11162
|
+
Object.assign(iframe.style, { width: "100%", height: "100%", border: "none" });
|
|
11163
|
+
|
|
11164
|
+
frame.appendChild(closeBtn);
|
|
11165
|
+
frame.appendChild(iframe);
|
|
11166
|
+
overlay.appendChild(frame);
|
|
11167
|
+
(container || doc.body).appendChild(overlay);
|
|
11168
|
+
|
|
11169
|
+
let torn = false;
|
|
11170
|
+
const teardown = () => {
|
|
11171
|
+
if (torn) return;
|
|
11172
|
+
torn = true;
|
|
11173
|
+
if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
|
|
11174
|
+
};
|
|
11175
|
+
closeBtn.addEventListener("click", () => {
|
|
11176
|
+
teardown();
|
|
11177
|
+
if (onCancel) onCancel();
|
|
11178
|
+
});
|
|
11179
|
+
return { teardown };
|
|
11180
|
+
}
|
|
11181
|
+
|
|
11037
11182
|
/**
|
|
11038
11183
|
* Login to the hypha server.
|
|
11039
11184
|
*
|
|
@@ -11044,6 +11189,13 @@ function normalizeServerUrl(server_url) {
|
|
|
11044
11189
|
* expires_in: Token expiration time (optional)
|
|
11045
11190
|
* login_timeout: Timeout for login process (default: 60)
|
|
11046
11191
|
* login_callback: Callback function for login URL (optional)
|
|
11192
|
+
* mode: "popup" (default) or "inline". In "inline" mode (browser only, and only
|
|
11193
|
+
* when no login_callback is given) the login page is rendered in an in-page
|
|
11194
|
+
* modal iframe instead of a popup window — for host apps that block popups.
|
|
11195
|
+
* Works with local-auth and custom-auth providers whose login page is
|
|
11196
|
+
* iframe-embeddable; does NOT apply to Auth0 (keep the popup for it).
|
|
11197
|
+
* container: optional DOM element to mount the inline login modal into
|
|
11198
|
+
* (default: a full-screen overlay on document.body)
|
|
11047
11199
|
* profile: Whether to return user profile (optional)
|
|
11048
11200
|
* additional_headers: Additional HTTP headers (optional)
|
|
11049
11201
|
* transport: Transport type - "websocket" (default) or "http"
|
|
@@ -11058,6 +11210,7 @@ async function login(config) {
|
|
|
11058
11210
|
const profile = config.profile;
|
|
11059
11211
|
const additional_headers = config.additional_headers;
|
|
11060
11212
|
const transport = config.transport || "websocket";
|
|
11213
|
+
const mode = config.mode || "popup";
|
|
11061
11214
|
|
|
11062
11215
|
const server = await connectToServer({
|
|
11063
11216
|
name: "initial login client",
|
|
@@ -11074,8 +11227,58 @@ async function login(config) {
|
|
|
11074
11227
|
} else {
|
|
11075
11228
|
context = await svc.start();
|
|
11076
11229
|
}
|
|
11230
|
+
const canInline =
|
|
11231
|
+
mode === "inline" &&
|
|
11232
|
+
!callback &&
|
|
11233
|
+
typeof window !== "undefined" &&
|
|
11234
|
+
window.document;
|
|
11235
|
+
|
|
11077
11236
|
if (callback) {
|
|
11237
|
+
if (mode === "inline") {
|
|
11238
|
+
_logger.warn(
|
|
11239
|
+
"login: both login_callback and mode:'inline' were given; using login_callback.",
|
|
11240
|
+
);
|
|
11241
|
+
}
|
|
11078
11242
|
await callback(context);
|
|
11243
|
+
} else if (canInline) {
|
|
11244
|
+
// No-popup mode: render the login page in an in-page modal iframe and wait
|
|
11245
|
+
// for its 'hypha-login-complete' postMessage. The token is NOT taken from
|
|
11246
|
+
// the message — it is always fetched over the trusted check() RPC (which is
|
|
11247
|
+
// also the real timeout authority and covers a dropped/early message).
|
|
11248
|
+
const origin = new URL(config.server_url).origin;
|
|
11249
|
+
const loginUrl = new URL(context.login_url, config.server_url).href;
|
|
11250
|
+
return await new Promise((resolve, reject) => {
|
|
11251
|
+
let modal = null;
|
|
11252
|
+
let msgListener = null;
|
|
11253
|
+
let done = false;
|
|
11254
|
+
const cleanup = () => {
|
|
11255
|
+
if (msgListener) window.removeEventListener("message", msgListener);
|
|
11256
|
+
if (modal) modal.teardown();
|
|
11257
|
+
};
|
|
11258
|
+
const finish = (fn, arg) => {
|
|
11259
|
+
if (done) return;
|
|
11260
|
+
done = true;
|
|
11261
|
+
cleanup();
|
|
11262
|
+
fn(arg);
|
|
11263
|
+
};
|
|
11264
|
+
const fetchToken = () =>
|
|
11265
|
+
svc
|
|
11266
|
+
.check(context.key, { timeout, profile, _rkwargs: true })
|
|
11267
|
+
.then((r) => finish(resolve, r))
|
|
11268
|
+
.catch((e) => finish(reject, e));
|
|
11269
|
+
msgListener = (event) => {
|
|
11270
|
+
if (!isLoginCompleteMessage(event, origin, context.key)) return;
|
|
11271
|
+
fetchToken();
|
|
11272
|
+
};
|
|
11273
|
+
window.addEventListener("message", msgListener);
|
|
11274
|
+
modal = openLoginModal(loginUrl, {
|
|
11275
|
+
container: config.container,
|
|
11276
|
+
onCancel: () => finish(reject, new Error("Login cancelled by user")),
|
|
11277
|
+
});
|
|
11278
|
+
// Safety net: resolves when the user completes login even if the
|
|
11279
|
+
// postMessage is missed, and enforces the login timeout server-side.
|
|
11280
|
+
fetchToken();
|
|
11281
|
+
});
|
|
11079
11282
|
} else {
|
|
11080
11283
|
_logger.log(`Please open your browser and login at ${context.login_url}`);
|
|
11081
11284
|
}
|
|
@@ -11744,6 +11947,7 @@ const hyphaWebsocketClient = {
|
|
|
11744
11947
|
schemaFunction: _utils_schema_js__WEBPACK_IMPORTED_MODULE_3__.schemaFunction,
|
|
11745
11948
|
loadRequirements: _utils_index_js__WEBPACK_IMPORTED_MODULE_2__.loadRequirements,
|
|
11746
11949
|
login,
|
|
11950
|
+
isLoginCompleteMessage,
|
|
11747
11951
|
logout,
|
|
11748
11952
|
connectToServer,
|
|
11749
11953
|
connectToServerHTTP: _http_client_js__WEBPACK_IMPORTED_MODULE_5__.connectToServerHTTP,
|