@rljson/server 0.0.15 → 0.0.17
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/node.d.ts +7 -0
- package/dist/server.d.ts +28 -0
- package/dist/server.js +79 -0
- package/package.json +1 -1
package/dist/node.d.ts
CHANGED
|
@@ -121,6 +121,7 @@ export declare class Node {
|
|
|
121
121
|
private _bsMem?;
|
|
122
122
|
private _role;
|
|
123
123
|
private _running;
|
|
124
|
+
private _transportReady;
|
|
124
125
|
private _transitioning?;
|
|
125
126
|
private _listeners;
|
|
126
127
|
private readonly _logger;
|
|
@@ -149,6 +150,12 @@ export declare class Node {
|
|
|
149
150
|
get socket(): SocketLike | undefined;
|
|
150
151
|
/** Whether the node is currently running. */
|
|
151
152
|
get isRunning(): boolean;
|
|
153
|
+
/**
|
|
154
|
+
* Whether the node's transport is fully ready.
|
|
155
|
+
* `false` during role transitions and before the first transition completes.
|
|
156
|
+
* `true` only after `_becomeHub()` or `_becomeClient()` has finished.
|
|
157
|
+
*/
|
|
158
|
+
get isTransportReady(): boolean;
|
|
152
159
|
/** The underlying NetworkManager. */
|
|
153
160
|
get networkManager(): NetworkManager;
|
|
154
161
|
/**
|
package/dist/server.d.ts
CHANGED
|
@@ -49,6 +49,19 @@ export interface ServerOptions {
|
|
|
49
49
|
* Defaults to false (local cache enabled).
|
|
50
50
|
*/
|
|
51
51
|
disableLocalCache?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Interval in milliseconds for application-level health checks.
|
|
54
|
+
* The server pings each connected client and prunes those that
|
|
55
|
+
* do not respond within {@link healthCheckTimeoutMs}.
|
|
56
|
+
* Defaults to 30 000 (30 s). Set to 0 to disable health checks.
|
|
57
|
+
*/
|
|
58
|
+
healthCheckIntervalMs?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Timeout in milliseconds to wait for a health check pong.
|
|
61
|
+
* Clients that do not respond within this window are pruned.
|
|
62
|
+
* Defaults to 10 000 (10 s).
|
|
63
|
+
*/
|
|
64
|
+
healthCheckTimeoutMs?: number;
|
|
52
65
|
}
|
|
53
66
|
export declare class Server extends BaseNode {
|
|
54
67
|
private _route;
|
|
@@ -77,6 +90,9 @@ export declare class Server extends BaseNode {
|
|
|
77
90
|
private _disableLocalCache;
|
|
78
91
|
private _latestRef;
|
|
79
92
|
private _bootstrapHeartbeatTimer?;
|
|
93
|
+
private _healthCheckIntervalMs;
|
|
94
|
+
private _healthCheckTimeoutMs;
|
|
95
|
+
private _healthCheckTimer?;
|
|
80
96
|
private _tornDown;
|
|
81
97
|
constructor(_route: Route, _localIo: Io, _localBs: Bs, options?: ServerOptions);
|
|
82
98
|
/**
|
|
@@ -181,6 +197,18 @@ export declare class Server extends BaseNode {
|
|
|
181
197
|
* Each client's dedup pipeline will filter out refs it already has.
|
|
182
198
|
*/
|
|
183
199
|
private _broadcastBootstrapHeartbeat;
|
|
200
|
+
/**
|
|
201
|
+
* Starts the periodic health check timer if not already running.
|
|
202
|
+
* Each cycle sends a ping to every non-broadcast client and waits
|
|
203
|
+
* for a pong. Clients that do not respond are pruned.
|
|
204
|
+
*/
|
|
205
|
+
private _startHealthChecks;
|
|
206
|
+
/**
|
|
207
|
+
* Sends a health ping to each connected (non-broadcast) client.
|
|
208
|
+
* If a client does not respond within `_healthCheckTimeoutMs`,
|
|
209
|
+
* the server force-disconnects and removes it.
|
|
210
|
+
*/
|
|
211
|
+
private _runHealthCheck;
|
|
184
212
|
/**
|
|
185
213
|
* Starts the periodic bootstrap heartbeat timer if configured
|
|
186
214
|
* and not already running.
|
package/dist/server.js
CHANGED
|
@@ -393,9 +393,14 @@ class Client extends BaseNode {
|
|
|
393
393
|
};
|
|
394
394
|
socket.on("disconnect", disconnectHandler);
|
|
395
395
|
socket.on("connect", reconnectHandler);
|
|
396
|
+
const healthHandler = (payload) => {
|
|
397
|
+
sockets.ioUp.emit("__health:pong", { nonce: payload.nonce });
|
|
398
|
+
};
|
|
399
|
+
sockets.ioDown.on("__health:ping", healthHandler);
|
|
396
400
|
this._connectionCleanup = () => {
|
|
397
401
|
socket.off("disconnect", disconnectHandler);
|
|
398
402
|
socket.off("connect", reconnectHandler);
|
|
403
|
+
sockets.ioDown.off("__health:ping", healthHandler);
|
|
399
404
|
};
|
|
400
405
|
}
|
|
401
406
|
/**
|
|
@@ -655,6 +660,8 @@ class Server extends BaseNode {
|
|
|
655
660
|
this._logger = options?.logger ?? noopLogger;
|
|
656
661
|
this._peerInitTimeoutMs = options?.peerInitTimeoutMs ?? 3e4;
|
|
657
662
|
this._disableLocalCache = options?.disableLocalCache ?? false;
|
|
663
|
+
this._healthCheckIntervalMs = options?.healthCheckIntervalMs ?? 3e4;
|
|
664
|
+
this._healthCheckTimeoutMs = options?.healthCheckTimeoutMs ?? 1e4;
|
|
658
665
|
this._syncConfig = options?.syncConfig;
|
|
659
666
|
this._refLogSize = options?.refLogSize ?? 1e3;
|
|
660
667
|
this._ackTimeoutMs = options?.ackTimeoutMs ?? options?.syncConfig?.ackTimeoutMs ?? 1e4;
|
|
@@ -728,6 +735,10 @@ class Server extends BaseNode {
|
|
|
728
735
|
// Bootstrap state
|
|
729
736
|
_latestRef;
|
|
730
737
|
_bootstrapHeartbeatTimer;
|
|
738
|
+
// Health check state
|
|
739
|
+
_healthCheckIntervalMs;
|
|
740
|
+
_healthCheckTimeoutMs;
|
|
741
|
+
_healthCheckTimer;
|
|
731
742
|
_tornDown = false;
|
|
732
743
|
/**
|
|
733
744
|
* Initializes Io and Bs multis on the server.
|
|
@@ -786,6 +797,7 @@ class Server extends BaseNode {
|
|
|
786
797
|
this._registerDisconnectHandler(clientId, ioUp);
|
|
787
798
|
this._sendBootstrap(ioDown);
|
|
788
799
|
this._startBootstrapHeartbeat();
|
|
800
|
+
this._startHealthChecks();
|
|
789
801
|
this._logger.info("Server", "Client socket added successfully", {
|
|
790
802
|
clientId,
|
|
791
803
|
totalClients: this._clients.size
|
|
@@ -836,6 +848,7 @@ class Server extends BaseNode {
|
|
|
836
848
|
this._registerDisconnectHandler(clientId, ioUp);
|
|
837
849
|
this._sendBootstrap(ioDown);
|
|
838
850
|
this._startBootstrapHeartbeat();
|
|
851
|
+
this._startHealthChecks();
|
|
839
852
|
this._logger.info("Server", "Broadcast-only socket added", {
|
|
840
853
|
clientId,
|
|
841
854
|
totalClients: this._clients.size
|
|
@@ -1034,6 +1047,54 @@ class Server extends BaseNode {
|
|
|
1034
1047
|
ioDown.emit(this._events.bootstrap, payload);
|
|
1035
1048
|
}
|
|
1036
1049
|
}
|
|
1050
|
+
// ...........................................................................
|
|
1051
|
+
// Health checks
|
|
1052
|
+
// ...........................................................................
|
|
1053
|
+
/**
|
|
1054
|
+
* Starts the periodic health check timer if not already running.
|
|
1055
|
+
* Each cycle sends a ping to every non-broadcast client and waits
|
|
1056
|
+
* for a pong. Clients that do not respond are pruned.
|
|
1057
|
+
*/
|
|
1058
|
+
_startHealthChecks() {
|
|
1059
|
+
if (this._healthCheckTimer || this._healthCheckIntervalMs <= 0) return;
|
|
1060
|
+
this._healthCheckTimer = setInterval(() => {
|
|
1061
|
+
this._runHealthCheck();
|
|
1062
|
+
}, this._healthCheckIntervalMs);
|
|
1063
|
+
this._healthCheckTimer.unref();
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Sends a health ping to each connected (non-broadcast) client.
|
|
1067
|
+
* If a client does not respond within `_healthCheckTimeoutMs`,
|
|
1068
|
+
* the server force-disconnects and removes it.
|
|
1069
|
+
*/
|
|
1070
|
+
_runHealthCheck() {
|
|
1071
|
+
for (const [clientId, { ioUp, ioDown }] of this._clients.entries()) {
|
|
1072
|
+
if (clientId.startsWith("broadcast_")) continue;
|
|
1073
|
+
const nonce = Math.random().toString(36).slice(2);
|
|
1074
|
+
let resolved = false;
|
|
1075
|
+
const handler = (payload) => {
|
|
1076
|
+
if (payload?.nonce !== nonce) return;
|
|
1077
|
+
resolved = true;
|
|
1078
|
+
ioUp.off("__health:pong", handler);
|
|
1079
|
+
clearTimeout(timer);
|
|
1080
|
+
};
|
|
1081
|
+
ioUp.on("__health:pong", handler);
|
|
1082
|
+
const timer = setTimeout(() => {
|
|
1083
|
+
if (resolved) return;
|
|
1084
|
+
ioUp.off("__health:pong", handler);
|
|
1085
|
+
this._logger.warn(
|
|
1086
|
+
"Server.Health",
|
|
1087
|
+
"Client failed health check — pruning",
|
|
1088
|
+
{ clientId }
|
|
1089
|
+
);
|
|
1090
|
+
if ("disconnect" in ioUp) {
|
|
1091
|
+
ioUp.disconnect(true);
|
|
1092
|
+
}
|
|
1093
|
+
this.removeSocket(clientId);
|
|
1094
|
+
}, this._healthCheckTimeoutMs);
|
|
1095
|
+
ioDown.emit("__health:ping", { nonce });
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1037
1098
|
/**
|
|
1038
1099
|
* Starts the periodic bootstrap heartbeat timer if configured
|
|
1039
1100
|
* and not already running.
|
|
@@ -1315,6 +1376,10 @@ class Server extends BaseNode {
|
|
|
1315
1376
|
clearInterval(this._bootstrapHeartbeatTimer);
|
|
1316
1377
|
this._bootstrapHeartbeatTimer = void 0;
|
|
1317
1378
|
}
|
|
1379
|
+
if (this._healthCheckTimer) {
|
|
1380
|
+
clearInterval(this._healthCheckTimer);
|
|
1381
|
+
this._healthCheckTimer = void 0;
|
|
1382
|
+
}
|
|
1318
1383
|
this._removeAllListeners();
|
|
1319
1384
|
for (const cleanup of this._disconnectCleanups.values()) {
|
|
1320
1385
|
cleanup();
|
|
@@ -1423,6 +1488,7 @@ class Node {
|
|
|
1423
1488
|
_bsMem;
|
|
1424
1489
|
_role = "unassigned";
|
|
1425
1490
|
_running = false;
|
|
1491
|
+
_transportReady = false;
|
|
1426
1492
|
_transitioning;
|
|
1427
1493
|
_listeners = /* @__PURE__ */ new Map();
|
|
1428
1494
|
_logger;
|
|
@@ -1449,6 +1515,7 @@ class Node {
|
|
|
1449
1515
|
async stop() {
|
|
1450
1516
|
if (!this._running) return;
|
|
1451
1517
|
this._running = false;
|
|
1518
|
+
this._transportReady = false;
|
|
1452
1519
|
this._networkManager.off("role-changed", this._onRoleChanged);
|
|
1453
1520
|
this._networkManager.off("hub-changed", this._onHubChanged);
|
|
1454
1521
|
if (this._transitioning) {
|
|
@@ -1495,6 +1562,14 @@ class Node {
|
|
|
1495
1562
|
get isRunning() {
|
|
1496
1563
|
return this._running;
|
|
1497
1564
|
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Whether the node's transport is fully ready.
|
|
1567
|
+
* `false` during role transitions and before the first transition completes.
|
|
1568
|
+
* `true` only after `_becomeHub()` or `_becomeClient()` has finished.
|
|
1569
|
+
*/
|
|
1570
|
+
get isTransportReady() {
|
|
1571
|
+
return this._transportReady;
|
|
1572
|
+
}
|
|
1498
1573
|
/** The underlying NetworkManager. */
|
|
1499
1574
|
get networkManager() {
|
|
1500
1575
|
return this._networkManager;
|
|
@@ -1542,6 +1617,7 @@ class Node {
|
|
|
1542
1617
|
const prev = this._transitioning ?? Promise.resolve();
|
|
1543
1618
|
this._transitioning = prev.then(async () => {
|
|
1544
1619
|
if (!this._running || this._role !== "client") return;
|
|
1620
|
+
this._transportReady = false;
|
|
1545
1621
|
await this._tearDownCurrentRole();
|
|
1546
1622
|
await this._becomeClient();
|
|
1547
1623
|
});
|
|
@@ -1557,6 +1633,7 @@ class Node {
|
|
|
1557
1633
|
if (!this._running) return;
|
|
1558
1634
|
const { current } = event;
|
|
1559
1635
|
if (current === this._role) return;
|
|
1636
|
+
this._transportReady = false;
|
|
1560
1637
|
this._logger.info("Node", `Role changing: ${this._role} → ${current}`);
|
|
1561
1638
|
await this._tearDownCurrentRole();
|
|
1562
1639
|
this._role = current;
|
|
@@ -1605,6 +1682,7 @@ class Node {
|
|
|
1605
1682
|
`Hub transport failed (no incoming connections): ${err}`
|
|
1606
1683
|
);
|
|
1607
1684
|
}
|
|
1685
|
+
this._transportReady = true;
|
|
1608
1686
|
const ctx = { role: "hub", server: this._server };
|
|
1609
1687
|
this._emit("ready", ctx);
|
|
1610
1688
|
await this._startAgent(ctx);
|
|
@@ -1642,6 +1720,7 @@ class Node {
|
|
|
1642
1720
|
);
|
|
1643
1721
|
await this._client.init();
|
|
1644
1722
|
this._logger.info("Node", `Now client — connected to hub ${hubAddress}`);
|
|
1723
|
+
this._transportReady = true;
|
|
1645
1724
|
const ctx = {
|
|
1646
1725
|
role: "client",
|
|
1647
1726
|
client: this._client,
|