@rljson/server 0.0.4 → 0.0.6
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.architecture.md +1352 -0
- package/README.blog.md +9 -1
- package/README.contributors.md +47 -13
- package/README.md +78 -11
- package/README.public.md +600 -7
- package/README.trouble.md +50 -0
- package/dist/README.architecture.md +1352 -0
- package/dist/README.blog.md +9 -1
- package/dist/README.contributors.md +47 -13
- package/dist/README.md +78 -11
- package/dist/README.public.md +600 -7
- package/dist/README.trouble.md +50 -0
- package/dist/client.d.ts +75 -3
- package/dist/index.d.ts +6 -0
- package/dist/logger.d.ts +115 -0
- package/dist/server.d.ts +148 -7
- package/dist/server.js +858 -118
- package/dist/socket-bundle.d.ts +16 -0
- package/package.json +17 -17
package/dist/server.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { hshBuffer } from "@rljson/hash";
|
|
2
2
|
import { Readable } from "node:stream";
|
|
3
|
+
import { Db, Connector } from "@rljson/db";
|
|
3
4
|
import { IoPeerBridge, IoMulti, IoPeer, IoServer, IoMem, SocketMock } from "@rljson/io";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { syncEvents, Route } from "@rljson/rljson";
|
|
6
|
+
import { syncEvents as syncEvents2 } from "@rljson/rljson";
|
|
6
7
|
class BsMem {
|
|
7
8
|
blobs = /* @__PURE__ */ new Map();
|
|
8
9
|
/**
|
|
@@ -743,21 +744,23 @@ class BsPeerBridge {
|
|
|
743
744
|
this._socket = _socket;
|
|
744
745
|
}
|
|
745
746
|
_eventHandlers = /* @__PURE__ */ new Map();
|
|
747
|
+
_handleConnectBound = this._handleConnect.bind(this);
|
|
748
|
+
_handleDisconnectBound = this._handleDisconnect.bind(this);
|
|
746
749
|
/**
|
|
747
750
|
* Starts the bridge by setting up connection event handlers and
|
|
748
751
|
* automatically registering all Bs methods.
|
|
749
752
|
*/
|
|
750
753
|
start() {
|
|
751
|
-
this._socket.on("connect",
|
|
752
|
-
this._socket.on("disconnect",
|
|
754
|
+
this._socket.on("connect", this._handleConnectBound);
|
|
755
|
+
this._socket.on("disconnect", this._handleDisconnectBound);
|
|
753
756
|
this._registerBsMethods();
|
|
754
757
|
}
|
|
755
758
|
/**
|
|
756
759
|
* Stops the bridge by removing all event handlers.
|
|
757
760
|
*/
|
|
758
761
|
stop() {
|
|
759
|
-
this._socket.off("connect",
|
|
760
|
-
this._socket.off("disconnect",
|
|
762
|
+
this._socket.off("connect", this._handleConnectBound);
|
|
763
|
+
this._socket.off("disconnect", this._handleDisconnectBound);
|
|
761
764
|
for (const [eventName, handler] of this._eventHandlers) {
|
|
762
765
|
this._socket.off(eventName, handler);
|
|
763
766
|
}
|
|
@@ -768,14 +771,11 @@ class BsPeerBridge {
|
|
|
768
771
|
*/
|
|
769
772
|
_registerBsMethods() {
|
|
770
773
|
const bsMethods = [
|
|
771
|
-
"setBlob",
|
|
772
774
|
"getBlob",
|
|
773
775
|
"getBlobStream",
|
|
774
|
-
"deleteBlob",
|
|
775
776
|
"blobExists",
|
|
776
777
|
"getBlobProperties",
|
|
777
|
-
"listBlobs"
|
|
778
|
-
"generateSignedUrl"
|
|
778
|
+
"listBlobs"
|
|
779
779
|
];
|
|
780
780
|
for (const methodName of bsMethods) {
|
|
781
781
|
this.registerEvent(methodName);
|
|
@@ -797,17 +797,17 @@ class BsPeerBridge {
|
|
|
797
797
|
`Method "${methodName}" not found on Bs instance`
|
|
798
798
|
);
|
|
799
799
|
if (typeof callback === "function") {
|
|
800
|
-
callback(
|
|
800
|
+
callback(error, null);
|
|
801
801
|
}
|
|
802
802
|
return;
|
|
803
803
|
}
|
|
804
804
|
bsMethod.apply(this._bs, methodArgs).then((result) => {
|
|
805
805
|
if (typeof callback === "function") {
|
|
806
|
-
callback(
|
|
806
|
+
callback(null, result);
|
|
807
807
|
}
|
|
808
808
|
}).catch((error) => {
|
|
809
809
|
if (typeof callback === "function") {
|
|
810
|
-
callback(
|
|
810
|
+
callback(error, null);
|
|
811
811
|
}
|
|
812
812
|
});
|
|
813
813
|
};
|
|
@@ -855,9 +855,9 @@ class BsPeerBridge {
|
|
|
855
855
|
throw new Error(`Method "${bsMethodName}" not found on Bs instance`);
|
|
856
856
|
}
|
|
857
857
|
const result = await bsMethod.apply(this._bs, args);
|
|
858
|
-
this._socket.emit(socketEventName,
|
|
858
|
+
this._socket.emit(socketEventName, null, result);
|
|
859
859
|
} catch (error) {
|
|
860
|
-
this._socket.emit(socketEventName,
|
|
860
|
+
this._socket.emit(socketEventName, error, null);
|
|
861
861
|
}
|
|
862
862
|
}
|
|
863
863
|
/* v8 ignore next -- @preserve */
|
|
@@ -978,33 +978,208 @@ class BaseNode {
|
|
|
978
978
|
await this._localDb.core.import(data);
|
|
979
979
|
}
|
|
980
980
|
}
|
|
981
|
+
class NoopLogger {
|
|
982
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
983
|
+
info(..._args) {
|
|
984
|
+
}
|
|
985
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
986
|
+
warn(..._args) {
|
|
987
|
+
}
|
|
988
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
989
|
+
error(..._args) {
|
|
990
|
+
}
|
|
991
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
992
|
+
traffic(..._args) {
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
class ConsoleLogger {
|
|
996
|
+
_fmt(data) {
|
|
997
|
+
return data ? " " + JSON.stringify(data) : "";
|
|
998
|
+
}
|
|
999
|
+
info(source, message, data) {
|
|
1000
|
+
console.log(`[INFO] [${source}] ${message}${this._fmt(data)}`);
|
|
1001
|
+
}
|
|
1002
|
+
warn(source, message, data) {
|
|
1003
|
+
console.warn(`[WARN] [${source}] ${message}${this._fmt(data)}`);
|
|
1004
|
+
}
|
|
1005
|
+
error(source, message, error, data) {
|
|
1006
|
+
const errStr = error ? ` ${error}` : "";
|
|
1007
|
+
console.error(`[ERROR] [${source}] ${message}${errStr}${this._fmt(data)}`);
|
|
1008
|
+
}
|
|
1009
|
+
traffic(direction, source, event, data) {
|
|
1010
|
+
const arrow = direction === "in" ? "⬅" : "➡";
|
|
1011
|
+
console.log(`[TRAFFIC] ${arrow} [${source}] ${event}${this._fmt(data)}`);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
class BufferedLogger {
|
|
1015
|
+
entries = [];
|
|
1016
|
+
info(source, message, data) {
|
|
1017
|
+
this.entries.push({
|
|
1018
|
+
level: "info",
|
|
1019
|
+
source,
|
|
1020
|
+
message,
|
|
1021
|
+
data,
|
|
1022
|
+
timestamp: Date.now()
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
warn(source, message, data) {
|
|
1026
|
+
this.entries.push({
|
|
1027
|
+
level: "warn",
|
|
1028
|
+
source,
|
|
1029
|
+
message,
|
|
1030
|
+
data,
|
|
1031
|
+
timestamp: Date.now()
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
error(source, message, error, data) {
|
|
1035
|
+
this.entries.push({
|
|
1036
|
+
level: "error",
|
|
1037
|
+
source,
|
|
1038
|
+
message,
|
|
1039
|
+
error,
|
|
1040
|
+
data,
|
|
1041
|
+
timestamp: Date.now()
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
traffic(direction, source, event, data) {
|
|
1045
|
+
this.entries.push({
|
|
1046
|
+
level: "traffic",
|
|
1047
|
+
source,
|
|
1048
|
+
message: event,
|
|
1049
|
+
direction,
|
|
1050
|
+
event,
|
|
1051
|
+
data,
|
|
1052
|
+
timestamp: Date.now()
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Returns entries filtered by level.
|
|
1057
|
+
* @param level - The log level to filter by
|
|
1058
|
+
*/
|
|
1059
|
+
byLevel(level) {
|
|
1060
|
+
return this.entries.filter((e) => e.level === level);
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Returns entries filtered by source (substring match).
|
|
1064
|
+
* @param source - The source substring to match
|
|
1065
|
+
*/
|
|
1066
|
+
bySource(source) {
|
|
1067
|
+
return this.entries.filter((e) => e.source.includes(source));
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Clears all stored entries.
|
|
1071
|
+
*/
|
|
1072
|
+
clear() {
|
|
1073
|
+
this.entries.length = 0;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
class FilteredLogger {
|
|
1077
|
+
constructor(_inner, _filter = {}) {
|
|
1078
|
+
this._inner = _inner;
|
|
1079
|
+
this._filter = _filter;
|
|
1080
|
+
}
|
|
1081
|
+
_shouldLog(level, source) {
|
|
1082
|
+
if (this._filter.levels && !this._filter.levels.includes(level)) {
|
|
1083
|
+
return false;
|
|
1084
|
+
}
|
|
1085
|
+
if (this._filter.sources && !this._filter.sources.some((s) => source.includes(s))) {
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
1088
|
+
return true;
|
|
1089
|
+
}
|
|
1090
|
+
info(source, message, data) {
|
|
1091
|
+
if (this._shouldLog("info", source)) {
|
|
1092
|
+
this._inner.info(source, message, data);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
warn(source, message, data) {
|
|
1096
|
+
if (this._shouldLog("warn", source)) {
|
|
1097
|
+
this._inner.warn(source, message, data);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
error(source, message, error, data) {
|
|
1101
|
+
if (this._shouldLog("error", source)) {
|
|
1102
|
+
this._inner.error(source, message, error, data);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
traffic(direction, source, event, data) {
|
|
1106
|
+
if (this._shouldLog("traffic", source)) {
|
|
1107
|
+
this._inner.traffic(direction, source, event, data);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
const noopLogger = new NoopLogger();
|
|
1112
|
+
function normalizeSocketBundle(socket) {
|
|
1113
|
+
const bundle = socket;
|
|
1114
|
+
if (bundle.ioUp && bundle.ioDown && bundle.bsUp && bundle.bsDown) {
|
|
1115
|
+
return bundle;
|
|
1116
|
+
}
|
|
1117
|
+
const single = socket;
|
|
1118
|
+
return {
|
|
1119
|
+
ioUp: single,
|
|
1120
|
+
ioDown: single,
|
|
1121
|
+
bsUp: single,
|
|
1122
|
+
bsDown: single
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
981
1125
|
class Client extends BaseNode {
|
|
982
1126
|
// ...........................................................................
|
|
983
1127
|
/**
|
|
984
1128
|
* Creates a Client instance
|
|
985
|
-
* @param _socketToServer - Socket to connect to server
|
|
1129
|
+
* @param _socketToServer - Socket or namespace bundle to connect to server
|
|
986
1130
|
* @param _localIo - Local Io for local storage
|
|
987
1131
|
* @param _localBs - Local Bs for local blob storage
|
|
1132
|
+
* @param _route - Optional route for automatic Db and Connector creation
|
|
1133
|
+
* @param options - Optional configuration including logger for monitoring
|
|
988
1134
|
*/
|
|
989
|
-
constructor(_socketToServer, _localIo, _localBs) {
|
|
1135
|
+
constructor(_socketToServer, _localIo, _localBs, _route, options) {
|
|
990
1136
|
super(_localIo);
|
|
991
1137
|
this._socketToServer = _socketToServer;
|
|
992
1138
|
this._localIo = _localIo;
|
|
993
1139
|
this._localBs = _localBs;
|
|
1140
|
+
this._route = _route;
|
|
1141
|
+
this._logger = options?.logger ?? noopLogger;
|
|
1142
|
+
this._syncConfig = options?.syncConfig;
|
|
1143
|
+
this._clientIdentity = options?.clientIdentity;
|
|
1144
|
+
this._peerInitTimeoutMs = options?.peerInitTimeoutMs ?? 3e4;
|
|
1145
|
+
this._logger.info("Client", "Constructing client", {
|
|
1146
|
+
hasRoute: !!this._route,
|
|
1147
|
+
route: this._route?.flat
|
|
1148
|
+
});
|
|
994
1149
|
}
|
|
995
1150
|
_ioMultiIos = [];
|
|
996
1151
|
_ioMulti;
|
|
997
1152
|
_bsMultiBss = [];
|
|
998
1153
|
_bsMulti;
|
|
1154
|
+
_db;
|
|
1155
|
+
_connector;
|
|
1156
|
+
_logger;
|
|
1157
|
+
_syncConfig;
|
|
1158
|
+
_clientIdentity;
|
|
1159
|
+
_peerInitTimeoutMs;
|
|
999
1160
|
/**
|
|
1000
1161
|
* Initializes Io and Bs multis and their peer bridges.
|
|
1001
1162
|
* @returns The initialized Io implementation.
|
|
1002
1163
|
*/
|
|
1003
1164
|
async init() {
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1165
|
+
this._logger.info("Client", "Initializing client");
|
|
1166
|
+
try {
|
|
1167
|
+
await this._setupIo();
|
|
1168
|
+
await this._setupBs();
|
|
1169
|
+
if (this._route) {
|
|
1170
|
+
this._setupDbAndConnector();
|
|
1171
|
+
}
|
|
1172
|
+
await this.ready();
|
|
1173
|
+
this._logger.info("Client", "Client initialized successfully", {
|
|
1174
|
+
hasRoute: !!this._route,
|
|
1175
|
+
hasDb: !!this._db,
|
|
1176
|
+
hasConnector: !!this._connector
|
|
1177
|
+
});
|
|
1178
|
+
return this._ioMulti;
|
|
1179
|
+
} catch (error) {
|
|
1180
|
+
this._logger.error("Client", "Failed to initialize client", error);
|
|
1181
|
+
throw error;
|
|
1182
|
+
}
|
|
1008
1183
|
}
|
|
1009
1184
|
/**
|
|
1010
1185
|
* Resolves once the Io implementation is ready.
|
|
@@ -1018,14 +1193,18 @@ class Client extends BaseNode {
|
|
|
1018
1193
|
* Closes client resources and clears internal state.
|
|
1019
1194
|
*/
|
|
1020
1195
|
async tearDown() {
|
|
1196
|
+
this._logger.info("Client", "Tearing down client");
|
|
1021
1197
|
if (this._ioMulti && this._ioMulti.isOpen) {
|
|
1022
1198
|
this._ioMulti.close();
|
|
1023
1199
|
}
|
|
1024
|
-
|
|
1025
|
-
this._ioMultiIos = [];
|
|
1200
|
+
this._connector?.tearDown();
|
|
1026
1201
|
this._bsMultiBss = [];
|
|
1202
|
+
this._ioMultiIos = [];
|
|
1027
1203
|
this._ioMulti = void 0;
|
|
1028
1204
|
this._bsMulti = void 0;
|
|
1205
|
+
this._db = void 0;
|
|
1206
|
+
this._connector = void 0;
|
|
1207
|
+
this._logger.info("Client", "Client torn down successfully");
|
|
1029
1208
|
}
|
|
1030
1209
|
/**
|
|
1031
1210
|
* Returns the Io implementation.
|
|
@@ -1039,77 +1218,210 @@ class Client extends BaseNode {
|
|
|
1039
1218
|
get bs() {
|
|
1040
1219
|
return this._bsMulti;
|
|
1041
1220
|
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Returns the Db instance (available when route was provided).
|
|
1223
|
+
*/
|
|
1224
|
+
get db() {
|
|
1225
|
+
return this._db;
|
|
1226
|
+
}
|
|
1227
|
+
/**
|
|
1228
|
+
* Returns the Connector instance (available when route was provided).
|
|
1229
|
+
*/
|
|
1230
|
+
get connector() {
|
|
1231
|
+
return this._connector;
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Returns the route (if provided).
|
|
1235
|
+
*/
|
|
1236
|
+
get route() {
|
|
1237
|
+
return this._route;
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Returns the logger instance.
|
|
1241
|
+
*/
|
|
1242
|
+
get logger() {
|
|
1243
|
+
return this._logger;
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Creates Db and Connector from the route and IoMulti.
|
|
1247
|
+
* Called during init() when a route was provided.
|
|
1248
|
+
*/
|
|
1249
|
+
_setupDbAndConnector() {
|
|
1250
|
+
this._logger.info("Client", "Setting up Db and Connector", {
|
|
1251
|
+
route: this._route.flat
|
|
1252
|
+
});
|
|
1253
|
+
this._db = new Db(this._ioMulti);
|
|
1254
|
+
const socket = normalizeSocketBundle(this._socketToServer);
|
|
1255
|
+
this._connector = new Connector(
|
|
1256
|
+
this._db,
|
|
1257
|
+
this._route,
|
|
1258
|
+
socket.ioUp,
|
|
1259
|
+
this._syncConfig,
|
|
1260
|
+
this._clientIdentity
|
|
1261
|
+
);
|
|
1262
|
+
this._logger.info("Client", "Db and Connector created");
|
|
1263
|
+
}
|
|
1042
1264
|
/**
|
|
1043
1265
|
* Builds the Io multi with local and peer layers.
|
|
1044
1266
|
*/
|
|
1045
1267
|
async _setupIo() {
|
|
1046
|
-
this.
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1268
|
+
this._logger.info("Client.Io", "Setting up Io multi");
|
|
1269
|
+
try {
|
|
1270
|
+
const sockets = normalizeSocketBundle(this._socketToServer);
|
|
1271
|
+
this._ioMultiIos.push({
|
|
1272
|
+
io: this._localIo,
|
|
1273
|
+
dump: true,
|
|
1274
|
+
read: true,
|
|
1275
|
+
write: true,
|
|
1276
|
+
priority: 1
|
|
1277
|
+
});
|
|
1278
|
+
const ioPeerBridge = new IoPeerBridge(this._localIo, sockets.ioUp);
|
|
1279
|
+
ioPeerBridge.start();
|
|
1280
|
+
this._logger.info("Client.Io", "Io peer bridge started (upstream)");
|
|
1281
|
+
const ioPeer = await this._createIoPeer(sockets.ioDown);
|
|
1282
|
+
this._ioMultiIos.push({
|
|
1283
|
+
io: ioPeer,
|
|
1284
|
+
dump: false,
|
|
1285
|
+
read: true,
|
|
1286
|
+
write: false,
|
|
1287
|
+
priority: 2
|
|
1288
|
+
});
|
|
1289
|
+
this._ioMulti = new IoMulti(this._ioMultiIos);
|
|
1290
|
+
await this._ioMulti.init();
|
|
1291
|
+
await this._ioMulti.isReady();
|
|
1292
|
+
this._logger.info("Client.Io", "Io multi ready");
|
|
1293
|
+
} catch (error) {
|
|
1294
|
+
this._logger.error("Client.Io", "Failed to set up Io", error);
|
|
1295
|
+
throw error;
|
|
1296
|
+
}
|
|
1066
1297
|
}
|
|
1067
1298
|
/**
|
|
1068
1299
|
* Builds the Bs multi with local and peer layers.
|
|
1069
1300
|
*/
|
|
1070
1301
|
async _setupBs() {
|
|
1071
|
-
this.
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1302
|
+
this._logger.info("Client.Bs", "Setting up Bs multi");
|
|
1303
|
+
try {
|
|
1304
|
+
const sockets = normalizeSocketBundle(this._socketToServer);
|
|
1305
|
+
this._bsMultiBss.push({
|
|
1306
|
+
bs: this._localBs,
|
|
1307
|
+
read: true,
|
|
1308
|
+
write: true,
|
|
1309
|
+
priority: 1
|
|
1310
|
+
});
|
|
1311
|
+
const bsPeerBridge = new BsPeerBridge(this._localBs, sockets.bsUp);
|
|
1312
|
+
bsPeerBridge.start();
|
|
1313
|
+
this._logger.info("Client.Bs", "Bs peer bridge started (upstream)");
|
|
1314
|
+
const bsPeer = await this._createBsPeer(sockets.bsDown);
|
|
1315
|
+
this._bsMultiBss.push({
|
|
1316
|
+
bs: bsPeer,
|
|
1317
|
+
read: true,
|
|
1318
|
+
write: false,
|
|
1319
|
+
priority: 2
|
|
1320
|
+
});
|
|
1321
|
+
this._bsMulti = new BsMulti(this._bsMultiBss);
|
|
1322
|
+
await this._bsMulti.init();
|
|
1323
|
+
this._logger.info("Client.Bs", "Bs multi ready");
|
|
1324
|
+
} catch (error) {
|
|
1325
|
+
this._logger.error("Client.Bs", "Failed to set up Bs", error);
|
|
1326
|
+
throw error;
|
|
1327
|
+
}
|
|
1088
1328
|
}
|
|
1089
1329
|
/**
|
|
1090
1330
|
* Creates and initializes a downstream Io peer.
|
|
1331
|
+
* @param socket - Downstream socket to the server Io namespace.
|
|
1091
1332
|
*/
|
|
1092
|
-
async _createIoPeer() {
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1333
|
+
async _createIoPeer(socket) {
|
|
1334
|
+
this._logger.info("Client.Io", "Creating Io peer (downstream)");
|
|
1335
|
+
try {
|
|
1336
|
+
const ioPeer = new IoPeer(socket);
|
|
1337
|
+
await this._withTimeout(
|
|
1338
|
+
ioPeer.init().then(() => ioPeer.isReady()),
|
|
1339
|
+
"IoPeer init"
|
|
1340
|
+
);
|
|
1341
|
+
this._logger.info("Client.Io", "Io peer created (downstream)");
|
|
1342
|
+
return ioPeer;
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
this._logger.error(
|
|
1345
|
+
"Client.Io",
|
|
1346
|
+
"Failed to create Io peer (downstream)",
|
|
1347
|
+
error
|
|
1348
|
+
);
|
|
1349
|
+
throw error;
|
|
1350
|
+
}
|
|
1097
1351
|
}
|
|
1098
1352
|
/**
|
|
1099
1353
|
* Creates and initializes a downstream Bs peer.
|
|
1354
|
+
* @param socket - Downstream socket to the server Bs namespace.
|
|
1100
1355
|
*/
|
|
1101
|
-
async _createBsPeer() {
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1356
|
+
async _createBsPeer(socket) {
|
|
1357
|
+
this._logger.info("Client.Bs", "Creating Bs peer (downstream)");
|
|
1358
|
+
try {
|
|
1359
|
+
const bsPeer = new BsPeer(socket);
|
|
1360
|
+
await this._withTimeout(bsPeer.init(), "BsPeer init");
|
|
1361
|
+
this._logger.info("Client.Bs", "Bs peer created (downstream)");
|
|
1362
|
+
return bsPeer;
|
|
1363
|
+
} catch (error) {
|
|
1364
|
+
this._logger.error(
|
|
1365
|
+
"Client.Bs",
|
|
1366
|
+
"Failed to create Bs peer (downstream)",
|
|
1367
|
+
error
|
|
1368
|
+
);
|
|
1369
|
+
throw error;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* Returns the configured peer init timeout in milliseconds.
|
|
1374
|
+
*/
|
|
1375
|
+
get peerInitTimeoutMs() {
|
|
1376
|
+
return this._peerInitTimeoutMs;
|
|
1377
|
+
}
|
|
1378
|
+
// ...........................................................................
|
|
1379
|
+
/**
|
|
1380
|
+
* Races a promise against a timeout. Resolves/rejects with the original
|
|
1381
|
+
* promise outcome if it settles first, or rejects with a timeout error.
|
|
1382
|
+
* @param promise - The promise to race.
|
|
1383
|
+
* @param label - Human-readable label for timeout error messages.
|
|
1384
|
+
*/
|
|
1385
|
+
_withTimeout(promise, label) {
|
|
1386
|
+
const ms = this._peerInitTimeoutMs;
|
|
1387
|
+
if (ms <= 0) return promise;
|
|
1388
|
+
let timer;
|
|
1389
|
+
const timeout = new Promise((_, reject) => {
|
|
1390
|
+
timer = setTimeout(
|
|
1391
|
+
/* v8 ignore next -- @preserve */
|
|
1392
|
+
() => reject(new Error(`Timeout after ${ms}ms: ${label}`)),
|
|
1393
|
+
ms
|
|
1394
|
+
);
|
|
1395
|
+
});
|
|
1396
|
+
return Promise.race([promise, timeout]).finally(
|
|
1397
|
+
/* v8 ignore next -- @preserve */
|
|
1398
|
+
() => clearTimeout(timer)
|
|
1399
|
+
);
|
|
1105
1400
|
}
|
|
1106
1401
|
}
|
|
1107
1402
|
class Server extends BaseNode {
|
|
1108
|
-
constructor(_route, _localIo, _localBs) {
|
|
1403
|
+
constructor(_route, _localIo, _localBs, options) {
|
|
1109
1404
|
super(_localIo);
|
|
1110
1405
|
this._route = _route;
|
|
1111
1406
|
this._localIo = _localIo;
|
|
1112
1407
|
this._localBs = _localBs;
|
|
1408
|
+
this._logger = options?.logger ?? noopLogger;
|
|
1409
|
+
this._peerInitTimeoutMs = options?.peerInitTimeoutMs ?? 3e4;
|
|
1410
|
+
this._syncConfig = options?.syncConfig;
|
|
1411
|
+
this._refLogSize = options?.refLogSize ?? 1e3;
|
|
1412
|
+
this._ackTimeoutMs = options?.ackTimeoutMs ?? options?.syncConfig?.ackTimeoutMs ?? 1e4;
|
|
1413
|
+
this._events = syncEvents(this._route.flat);
|
|
1414
|
+
this._logger.info("Server", "Constructing server", {
|
|
1415
|
+
route: this._route.flat
|
|
1416
|
+
});
|
|
1417
|
+
const evictionMs = options?.refEvictionIntervalMs ?? 6e4;
|
|
1418
|
+
if (evictionMs > 0) {
|
|
1419
|
+
this._refEvictionTimer = setInterval(() => {
|
|
1420
|
+
this._multicastedRefsPrevious = this._multicastedRefsCurrent;
|
|
1421
|
+
this._multicastedRefsCurrent = /* @__PURE__ */ new Set();
|
|
1422
|
+
}, evictionMs);
|
|
1423
|
+
this._refEvictionTimer.unref();
|
|
1424
|
+
}
|
|
1113
1425
|
const ioMultiIoLocal = {
|
|
1114
1426
|
io: this._localIo,
|
|
1115
1427
|
dump: true,
|
|
@@ -1142,18 +1454,42 @@ class Server extends BaseNode {
|
|
|
1142
1454
|
_bsMulti;
|
|
1143
1455
|
// Storage => Let Clients read from Servers Bs
|
|
1144
1456
|
_bsServer;
|
|
1145
|
-
//
|
|
1146
|
-
|
|
1457
|
+
// Two-generation ref dedup: refs in current or previous are considered seen.
|
|
1458
|
+
// On each eviction tick, previous is discarded and current becomes previous.
|
|
1459
|
+
_multicastedRefsCurrent = /* @__PURE__ */ new Set();
|
|
1460
|
+
_multicastedRefsPrevious = /* @__PURE__ */ new Set();
|
|
1461
|
+
_refEvictionTimer;
|
|
1147
1462
|
_refreshPromise;
|
|
1148
1463
|
_pendingSockets = [];
|
|
1464
|
+
_logger;
|
|
1465
|
+
_peerInitTimeoutMs;
|
|
1466
|
+
// Cleanup callbacks for socket disconnect listeners (clientId → cleanup fn)
|
|
1467
|
+
_disconnectCleanups = /* @__PURE__ */ new Map();
|
|
1468
|
+
// Sync protocol state
|
|
1469
|
+
_syncConfig;
|
|
1470
|
+
_events;
|
|
1471
|
+
_refLog = [];
|
|
1472
|
+
_refLogSize;
|
|
1473
|
+
_ackTimeoutMs;
|
|
1474
|
+
// Bootstrap state
|
|
1475
|
+
_latestRef;
|
|
1476
|
+
_bootstrapHeartbeatTimer;
|
|
1477
|
+
_tornDown = false;
|
|
1149
1478
|
/**
|
|
1150
1479
|
* Initializes Io and Bs multis on the server.
|
|
1151
1480
|
*/
|
|
1152
1481
|
async init() {
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1482
|
+
this._logger.info("Server", "Initializing server");
|
|
1483
|
+
try {
|
|
1484
|
+
await this._ioMulti.init();
|
|
1485
|
+
await this._ioMulti.isReady();
|
|
1486
|
+
await this._bsMulti.init();
|
|
1487
|
+
await this.ready();
|
|
1488
|
+
this._logger.info("Server", "Server initialized successfully");
|
|
1489
|
+
} catch (error) {
|
|
1490
|
+
this._logger.error("Server", "Failed to initialize server", error);
|
|
1491
|
+
throw error;
|
|
1492
|
+
}
|
|
1157
1493
|
}
|
|
1158
1494
|
/**
|
|
1159
1495
|
* Resolves once the Io implementation is ready.
|
|
@@ -1167,17 +1503,45 @@ class Server extends BaseNode {
|
|
|
1167
1503
|
* @returns The server instance.
|
|
1168
1504
|
*/
|
|
1169
1505
|
async addSocket(socket) {
|
|
1506
|
+
const sockets = normalizeSocketBundle(socket);
|
|
1170
1507
|
const clientId = `client_${this._clients.size}_${Math.random().toString(36).slice(2)}`;
|
|
1171
|
-
|
|
1172
|
-
const
|
|
1173
|
-
const
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1508
|
+
this._logger.info("Server", "Adding client socket", { clientId });
|
|
1509
|
+
const ioUp = sockets.ioUp;
|
|
1510
|
+
const ioDown = sockets.ioDown;
|
|
1511
|
+
const bsUp = sockets.bsUp;
|
|
1512
|
+
const bsDown = sockets.bsDown;
|
|
1513
|
+
ioUp.__clientId = clientId;
|
|
1514
|
+
ioDown.__clientId = clientId;
|
|
1515
|
+
bsUp.__clientId = clientId;
|
|
1516
|
+
bsDown.__clientId = clientId;
|
|
1517
|
+
try {
|
|
1518
|
+
const ioPeer = await this._createIoPeer(socket, clientId);
|
|
1519
|
+
const bsPeer = await this._createBsPeer(socket, clientId);
|
|
1520
|
+
this._registerClient(
|
|
1521
|
+
clientId,
|
|
1522
|
+
{ ioUp, ioDown, bsUp, bsDown },
|
|
1523
|
+
ioPeer,
|
|
1524
|
+
bsPeer
|
|
1525
|
+
);
|
|
1526
|
+
this._pendingSockets.push({ ioDown, bsDown });
|
|
1527
|
+
this._queueIoPeer(ioPeer);
|
|
1528
|
+
this._queueBsPeer(bsPeer);
|
|
1529
|
+
await this._queueRefresh();
|
|
1530
|
+
this._removeAllListeners();
|
|
1531
|
+
this._multicastRefs();
|
|
1532
|
+
this._registerDisconnectHandler(clientId, ioUp);
|
|
1533
|
+
this._sendBootstrap(ioDown);
|
|
1534
|
+
this._startBootstrapHeartbeat();
|
|
1535
|
+
this._logger.info("Server", "Client socket added successfully", {
|
|
1536
|
+
clientId,
|
|
1537
|
+
totalClients: this._clients.size
|
|
1538
|
+
});
|
|
1539
|
+
} catch (error) {
|
|
1540
|
+
this._logger.error("Server", "Failed to add client socket", error, {
|
|
1541
|
+
clientId
|
|
1542
|
+
});
|
|
1543
|
+
throw error;
|
|
1544
|
+
}
|
|
1181
1545
|
return this;
|
|
1182
1546
|
}
|
|
1183
1547
|
// ...........................................................................
|
|
@@ -1185,44 +1549,87 @@ class Server extends BaseNode {
|
|
|
1185
1549
|
* Removes all listeners from all connected clients.
|
|
1186
1550
|
*/
|
|
1187
1551
|
_removeAllListeners() {
|
|
1188
|
-
for (const {
|
|
1189
|
-
|
|
1552
|
+
for (const { ioUp } of this._clients.values()) {
|
|
1553
|
+
ioUp.removeAllListeners(this._route.flat);
|
|
1554
|
+
ioUp.removeAllListeners(this._events.gapFillReq);
|
|
1555
|
+
ioUp.removeAllListeners(this._events.ackClient);
|
|
1190
1556
|
}
|
|
1191
1557
|
}
|
|
1192
1558
|
// ...........................................................................
|
|
1193
1559
|
/**
|
|
1194
1560
|
* Broadcasts incoming payloads from any client to all other connected clients.
|
|
1195
|
-
*
|
|
1561
|
+
* Enriched with ref log, ACK aggregation, and gap-fill support when
|
|
1562
|
+
* syncConfig is provided.
|
|
1196
1563
|
*/
|
|
1197
1564
|
_multicastRefs = () => {
|
|
1198
|
-
for (const [clientIdA, {
|
|
1565
|
+
for (const [clientIdA, { ioUp: socketA }] of this._clients.entries()) {
|
|
1199
1566
|
socketA.on(this._route.flat, (payload) => {
|
|
1200
1567
|
const ref = payload.r;
|
|
1201
|
-
|
|
1568
|
+
this._logger.traffic("in", "Server.Multicast", this._route.flat, {
|
|
1569
|
+
ref,
|
|
1570
|
+
from: clientIdA
|
|
1571
|
+
});
|
|
1572
|
+
if (this._multicastedRefsCurrent.has(ref) || this._multicastedRefsPrevious.has(ref)) {
|
|
1573
|
+
this._logger.warn("Server.Multicast", "Duplicate ref suppressed", {
|
|
1574
|
+
ref,
|
|
1575
|
+
from: clientIdA
|
|
1576
|
+
});
|
|
1202
1577
|
return;
|
|
1203
1578
|
}
|
|
1204
|
-
this.
|
|
1579
|
+
this._multicastedRefsCurrent.add(ref);
|
|
1580
|
+
this._latestRef = ref;
|
|
1205
1581
|
const p = payload;
|
|
1206
1582
|
if (p && p.__origin) {
|
|
1583
|
+
this._logger.warn(
|
|
1584
|
+
"Server.Multicast",
|
|
1585
|
+
"Loop prevention: payload already has origin",
|
|
1586
|
+
{ ref, origin: p.__origin, from: clientIdA }
|
|
1587
|
+
);
|
|
1207
1588
|
return;
|
|
1208
1589
|
}
|
|
1590
|
+
if (this._syncConfig) {
|
|
1591
|
+
this._appendToRefLog(payload);
|
|
1592
|
+
}
|
|
1593
|
+
let receiverCount = 0;
|
|
1594
|
+
let ackCollector;
|
|
1595
|
+
if (this._syncConfig?.requireAck) {
|
|
1596
|
+
ackCollector = this._setupAckCollection(clientIdA, ref);
|
|
1597
|
+
}
|
|
1209
1598
|
for (const [
|
|
1210
1599
|
clientIdB,
|
|
1211
|
-
{
|
|
1600
|
+
{ ioDown: socketB }
|
|
1212
1601
|
] of this._clients.entries()) {
|
|
1213
1602
|
if (clientIdA !== clientIdB) {
|
|
1214
1603
|
const forwarded = Object.assign({}, payload, {
|
|
1215
1604
|
__origin: clientIdA
|
|
1216
1605
|
});
|
|
1606
|
+
this._logger.traffic("out", "Server.Multicast", this._route.flat, {
|
|
1607
|
+
ref,
|
|
1608
|
+
from: clientIdA,
|
|
1609
|
+
to: clientIdB
|
|
1610
|
+
});
|
|
1217
1611
|
socketB.emit(this._route.flat, forwarded);
|
|
1612
|
+
receiverCount++;
|
|
1218
1613
|
}
|
|
1219
1614
|
}
|
|
1615
|
+
if (ackCollector && receiverCount === 0) {
|
|
1616
|
+
ackCollector();
|
|
1617
|
+
}
|
|
1220
1618
|
});
|
|
1619
|
+
if (this._syncConfig?.causalOrdering) {
|
|
1620
|
+
this._registerGapFillListener(clientIdA, socketA);
|
|
1621
|
+
}
|
|
1221
1622
|
}
|
|
1222
1623
|
};
|
|
1223
1624
|
get route() {
|
|
1224
1625
|
return this._route;
|
|
1225
1626
|
}
|
|
1627
|
+
/**
|
|
1628
|
+
* Returns the logger instance.
|
|
1629
|
+
*/
|
|
1630
|
+
get logger() {
|
|
1631
|
+
return this._logger;
|
|
1632
|
+
}
|
|
1226
1633
|
/**
|
|
1227
1634
|
* Returns the Io implementation.
|
|
1228
1635
|
*/
|
|
@@ -1241,35 +1648,229 @@ class Server extends BaseNode {
|
|
|
1241
1648
|
get clients() {
|
|
1242
1649
|
return this._clients;
|
|
1243
1650
|
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Returns the sync configuration, if any.
|
|
1653
|
+
*/
|
|
1654
|
+
get syncConfig() {
|
|
1655
|
+
return this._syncConfig;
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1658
|
+
* Returns the typed sync event names.
|
|
1659
|
+
*/
|
|
1660
|
+
get events() {
|
|
1661
|
+
return this._events;
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Returns the current ref log contents (for diagnostics / testing).
|
|
1665
|
+
*/
|
|
1666
|
+
get refLog() {
|
|
1667
|
+
return this._refLog;
|
|
1668
|
+
}
|
|
1669
|
+
/**
|
|
1670
|
+
* Returns the latest ref tracked by the server (for bootstrap / diagnostics).
|
|
1671
|
+
*/
|
|
1672
|
+
get latestRef() {
|
|
1673
|
+
return this._latestRef;
|
|
1674
|
+
}
|
|
1675
|
+
// ...........................................................................
|
|
1676
|
+
// Sync protocol private methods
|
|
1677
|
+
// ...........................................................................
|
|
1678
|
+
/**
|
|
1679
|
+
* Appends a payload to the bounded ref log (ring buffer).
|
|
1680
|
+
* Drops the oldest entry when the log exceeds `_refLogSize`.
|
|
1681
|
+
* @param payload - The ConnectorPayload to append.
|
|
1682
|
+
*/
|
|
1683
|
+
_appendToRefLog(payload) {
|
|
1684
|
+
this._refLog.push(payload);
|
|
1685
|
+
if (this._refLog.length > this._refLogSize) {
|
|
1686
|
+
this._refLog.shift();
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
// ...........................................................................
|
|
1690
|
+
// Bootstrap methods
|
|
1691
|
+
// ...........................................................................
|
|
1692
|
+
/**
|
|
1693
|
+
* Sends the latest ref to a specific client socket as a bootstrap message.
|
|
1694
|
+
* If no ref has been seen yet, this is a no-op.
|
|
1695
|
+
* @param ioDown - The downstream socket to send the bootstrap message on.
|
|
1696
|
+
*/
|
|
1697
|
+
_sendBootstrap(ioDown) {
|
|
1698
|
+
if (!this._latestRef) return;
|
|
1699
|
+
const payload = {
|
|
1700
|
+
o: "__server__",
|
|
1701
|
+
r: this._latestRef
|
|
1702
|
+
};
|
|
1703
|
+
this._logger.info("Server.Bootstrap", "Sending bootstrap ref", {
|
|
1704
|
+
ref: this._latestRef,
|
|
1705
|
+
to: ioDown.__clientId
|
|
1706
|
+
});
|
|
1707
|
+
ioDown.emit(this._events.bootstrap, payload);
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* Broadcasts the latest ref to all connected clients as a heartbeat.
|
|
1711
|
+
* Each client's dedup pipeline will filter out refs it already has.
|
|
1712
|
+
*/
|
|
1713
|
+
_broadcastBootstrapHeartbeat() {
|
|
1714
|
+
if (!this._latestRef) return;
|
|
1715
|
+
const payload = {
|
|
1716
|
+
o: "__server__",
|
|
1717
|
+
r: this._latestRef
|
|
1718
|
+
};
|
|
1719
|
+
this._logger.info("Server.Bootstrap", "Heartbeat broadcast", {
|
|
1720
|
+
ref: this._latestRef,
|
|
1721
|
+
clientCount: this._clients.size
|
|
1722
|
+
});
|
|
1723
|
+
for (const { ioDown } of this._clients.values()) {
|
|
1724
|
+
ioDown.emit(this._events.bootstrap, payload);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Starts the periodic bootstrap heartbeat timer if configured
|
|
1729
|
+
* and not already running.
|
|
1730
|
+
*/
|
|
1731
|
+
_startBootstrapHeartbeat() {
|
|
1732
|
+
const ms = this._syncConfig?.bootstrapHeartbeatMs;
|
|
1733
|
+
if (!ms || ms <= 0 || this._bootstrapHeartbeatTimer) return;
|
|
1734
|
+
this._bootstrapHeartbeatTimer = setInterval(() => {
|
|
1735
|
+
this._broadcastBootstrapHeartbeat();
|
|
1736
|
+
}, ms);
|
|
1737
|
+
this._bootstrapHeartbeatTimer.unref();
|
|
1738
|
+
this._logger.info("Server.Bootstrap", "Heartbeat timer started", {
|
|
1739
|
+
intervalMs: ms
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* Sets up ACK collection listeners before broadcast.
|
|
1744
|
+
* Returns a cleanup function that emits an immediate ACK
|
|
1745
|
+
* (used when there are no receivers).
|
|
1746
|
+
* @param senderClientId - The internal client ID of the sender.
|
|
1747
|
+
* @param ref - The ref being acknowledged.
|
|
1748
|
+
* @returns A function to call for immediate ACK (zero receivers).
|
|
1749
|
+
*/
|
|
1750
|
+
_setupAckCollection(senderClientId, ref) {
|
|
1751
|
+
const senderEntry = this._clients.get(senderClientId);
|
|
1752
|
+
if (!senderEntry) return () => {
|
|
1753
|
+
};
|
|
1754
|
+
const totalClients = this._clients.size - 1;
|
|
1755
|
+
let acksReceived = 0;
|
|
1756
|
+
let finished = false;
|
|
1757
|
+
const ackHandlers = /* @__PURE__ */ new Map();
|
|
1758
|
+
const finish = (ok) => {
|
|
1759
|
+
if (finished) return;
|
|
1760
|
+
finished = true;
|
|
1761
|
+
clearTimeout(timeout);
|
|
1762
|
+
for (const [cId, handler] of ackHandlers.entries()) {
|
|
1763
|
+
const clientEntry = this._clients.get(cId);
|
|
1764
|
+
if (clientEntry) {
|
|
1765
|
+
clientEntry.ioUp.off(this._events.ackClient, handler);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
ackHandlers.clear();
|
|
1769
|
+
const ack = {
|
|
1770
|
+
r: ref,
|
|
1771
|
+
ok,
|
|
1772
|
+
receivedBy: acksReceived,
|
|
1773
|
+
totalClients
|
|
1774
|
+
};
|
|
1775
|
+
senderEntry.ioDown.emit(this._events.ack, ack);
|
|
1776
|
+
};
|
|
1777
|
+
const timeout = setTimeout(() => {
|
|
1778
|
+
finish(false);
|
|
1779
|
+
}, this._ackTimeoutMs);
|
|
1780
|
+
for (const [clientId, { ioUp }] of this._clients.entries()) {
|
|
1781
|
+
if (clientId === senderClientId) continue;
|
|
1782
|
+
const handler = (clientAck) => {
|
|
1783
|
+
if (clientAck.r !== ref) return;
|
|
1784
|
+
acksReceived++;
|
|
1785
|
+
if (acksReceived >= totalClients) {
|
|
1786
|
+
finish(true);
|
|
1787
|
+
}
|
|
1788
|
+
};
|
|
1789
|
+
ioUp.on(this._events.ackClient, handler);
|
|
1790
|
+
ackHandlers.set(clientId, handler);
|
|
1791
|
+
}
|
|
1792
|
+
return () => {
|
|
1793
|
+
finish(true);
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
/**
|
|
1797
|
+
* Registers a gap-fill request listener for a specific client socket.
|
|
1798
|
+
* @param clientId - The internal client ID.
|
|
1799
|
+
* @param socket - The upstream socket to listen on.
|
|
1800
|
+
*/
|
|
1801
|
+
_registerGapFillListener(clientId, socket) {
|
|
1802
|
+
socket.on(this._events.gapFillReq, (req) => {
|
|
1803
|
+
this._logger.info("Server.GapFill", "Gap-fill request received", {
|
|
1804
|
+
from: clientId,
|
|
1805
|
+
afterSeq: req.afterSeq
|
|
1806
|
+
});
|
|
1807
|
+
const refs = this._refLog.filter(
|
|
1808
|
+
(p) => p.seq != null && p.seq > req.afterSeq
|
|
1809
|
+
);
|
|
1810
|
+
const res = {
|
|
1811
|
+
route: req.route,
|
|
1812
|
+
refs
|
|
1813
|
+
};
|
|
1814
|
+
const clientEntry = this._clients.get(clientId);
|
|
1815
|
+
if (clientEntry) {
|
|
1816
|
+
clientEntry.ioDown.emit(this._events.gapFillRes, res);
|
|
1817
|
+
}
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1244
1820
|
/**
|
|
1245
1821
|
* Creates and initializes a downstream Io peer for a socket.
|
|
1246
1822
|
* @param socket - Client socket to bind the peer to.
|
|
1823
|
+
* @param clientId - Client identifier for logging.
|
|
1247
1824
|
*/
|
|
1248
|
-
async _createIoPeer(socket) {
|
|
1249
|
-
const
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1825
|
+
async _createIoPeer(socket, clientId) {
|
|
1826
|
+
const sockets = normalizeSocketBundle(socket);
|
|
1827
|
+
this._logger.info("Server.Io", "Creating Io peer", { clientId });
|
|
1828
|
+
try {
|
|
1829
|
+
const ioPeer = new IoPeer(sockets.ioUp);
|
|
1830
|
+
await this._withTimeout(ioPeer.init(), "IoPeer.init()");
|
|
1831
|
+
await this._withTimeout(ioPeer.isReady(), "IoPeer.isReady()");
|
|
1832
|
+
this._logger.info("Server.Io", "Io peer created", { clientId });
|
|
1833
|
+
return ioPeer;
|
|
1834
|
+
} catch (error) {
|
|
1835
|
+
this._logger.error("Server.Io", "Failed to create Io peer", error, {
|
|
1836
|
+
clientId
|
|
1837
|
+
});
|
|
1838
|
+
throw error;
|
|
1839
|
+
}
|
|
1253
1840
|
}
|
|
1254
1841
|
/**
|
|
1255
1842
|
* Creates and initializes a downstream Bs peer for a socket.
|
|
1256
1843
|
* @param socket - Client socket to bind the peer to.
|
|
1844
|
+
* @param clientId - Client identifier for logging.
|
|
1257
1845
|
*/
|
|
1258
|
-
async _createBsPeer(socket) {
|
|
1259
|
-
const
|
|
1260
|
-
|
|
1261
|
-
|
|
1846
|
+
async _createBsPeer(socket, clientId) {
|
|
1847
|
+
const sockets = normalizeSocketBundle(socket);
|
|
1848
|
+
this._logger.info("Server.Bs", "Creating Bs peer", { clientId });
|
|
1849
|
+
try {
|
|
1850
|
+
const bsPeer = new BsPeer(sockets.bsUp);
|
|
1851
|
+
await this._withTimeout(bsPeer.init(), "BsPeer.init()");
|
|
1852
|
+
this._logger.info("Server.Bs", "Bs peer created", { clientId });
|
|
1853
|
+
return bsPeer;
|
|
1854
|
+
} catch (error) {
|
|
1855
|
+
this._logger.error("Server.Bs", "Failed to create Bs peer", error, {
|
|
1856
|
+
clientId
|
|
1857
|
+
});
|
|
1858
|
+
throw error;
|
|
1859
|
+
}
|
|
1262
1860
|
}
|
|
1263
1861
|
/**
|
|
1264
1862
|
* Registers the client socket and peers.
|
|
1265
1863
|
* @param clientId - Stable client identifier.
|
|
1266
|
-
* @param
|
|
1864
|
+
* @param sockets - Directional sockets to register.
|
|
1267
1865
|
* @param io - Io peer associated with the client.
|
|
1268
1866
|
* @param bs - Bs peer associated with the client.
|
|
1269
1867
|
*/
|
|
1270
|
-
_registerClient(clientId,
|
|
1868
|
+
_registerClient(clientId, sockets, io, bs) {
|
|
1271
1869
|
this._clients.set(clientId, {
|
|
1272
|
-
|
|
1870
|
+
ioUp: sockets.ioUp,
|
|
1871
|
+
ioDown: sockets.ioDown,
|
|
1872
|
+
bsUp: sockets.bsUp,
|
|
1873
|
+
bsDown: sockets.bsDown,
|
|
1273
1874
|
io,
|
|
1274
1875
|
bs
|
|
1275
1876
|
});
|
|
@@ -1303,38 +1904,171 @@ class Server extends BaseNode {
|
|
|
1303
1904
|
* Rebuilds Io and Bs multis from queued peers.
|
|
1304
1905
|
*/
|
|
1305
1906
|
async _rebuildMultis() {
|
|
1306
|
-
this.
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1907
|
+
this._logger.info("Server", "Rebuilding multis", {
|
|
1908
|
+
ioCount: this._ios.length,
|
|
1909
|
+
bsCount: this._bss.length
|
|
1910
|
+
});
|
|
1911
|
+
try {
|
|
1912
|
+
this._ioMulti = new IoMulti(this._ios);
|
|
1913
|
+
await this._ioMulti.init();
|
|
1914
|
+
await this._ioMulti.isReady();
|
|
1915
|
+
this._bsMulti = new BsMulti(this._bss);
|
|
1916
|
+
await this._bsMulti.init();
|
|
1917
|
+
this._logger.info("Server", "Multis rebuilt successfully");
|
|
1918
|
+
} catch (error) {
|
|
1919
|
+
this._logger.error("Server", "Failed to rebuild multis", error);
|
|
1920
|
+
throw error;
|
|
1921
|
+
}
|
|
1311
1922
|
}
|
|
1312
1923
|
/**
|
|
1313
1924
|
* Recreates servers and reattaches sockets.
|
|
1314
1925
|
*/
|
|
1315
1926
|
async _refreshServers() {
|
|
1316
|
-
this.
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1927
|
+
this._logger.info("Server", "Refreshing servers", {
|
|
1928
|
+
pendingSockets: this._pendingSockets.length
|
|
1929
|
+
});
|
|
1930
|
+
try {
|
|
1931
|
+
this._ioServer._io = this._ioMulti;
|
|
1932
|
+
this._bsServer._bs = this._bsMulti;
|
|
1933
|
+
for (const pending of this._pendingSockets) {
|
|
1934
|
+
await this._ioServer.addSocket(pending.ioDown);
|
|
1935
|
+
await this._bsServer.addSocket(pending.bsDown);
|
|
1936
|
+
}
|
|
1937
|
+
this._pendingSockets = [];
|
|
1938
|
+
this._logger.info("Server", "Servers refreshed successfully");
|
|
1939
|
+
} catch (error) {
|
|
1940
|
+
this._logger.error("Server", "Failed to refresh servers", error);
|
|
1941
|
+
throw error;
|
|
1321
1942
|
}
|
|
1322
|
-
this._pendingSockets = [];
|
|
1323
1943
|
}
|
|
1324
1944
|
/**
|
|
1325
1945
|
* Batches multi/server refreshes into a single queued task.
|
|
1326
1946
|
*/
|
|
1327
1947
|
_queueRefresh() {
|
|
1328
1948
|
if (!this._refreshPromise) {
|
|
1949
|
+
this._logger.info("Server", "Queuing refresh");
|
|
1329
1950
|
this._refreshPromise = Promise.resolve().then(async () => {
|
|
1330
1951
|
await this._rebuildMultis();
|
|
1331
1952
|
await this._refreshServers();
|
|
1332
|
-
}).
|
|
1953
|
+
}).catch(
|
|
1954
|
+
/* v8 ignore next -- @preserve */
|
|
1955
|
+
(error) => {
|
|
1956
|
+
this._logger.error("Server", "Queued refresh failed", error);
|
|
1957
|
+
throw error;
|
|
1958
|
+
}
|
|
1959
|
+
).finally(() => {
|
|
1333
1960
|
this._refreshPromise = void 0;
|
|
1334
1961
|
});
|
|
1335
1962
|
}
|
|
1336
1963
|
return this._refreshPromise;
|
|
1337
1964
|
}
|
|
1965
|
+
// ...........................................................................
|
|
1966
|
+
/**
|
|
1967
|
+
* Removes a connected client by its internal client ID.
|
|
1968
|
+
* Cleans up listeners, peers, and rebuilds multis.
|
|
1969
|
+
* @param clientId - The client identifier (from server.clients keys).
|
|
1970
|
+
*/
|
|
1971
|
+
async removeSocket(clientId) {
|
|
1972
|
+
const client = this._clients.get(clientId);
|
|
1973
|
+
if (!client) return;
|
|
1974
|
+
this._logger.info("Server", "Removing client socket", { clientId });
|
|
1975
|
+
client.ioUp.removeAllListeners(this._route.flat);
|
|
1976
|
+
const cleanup = this._disconnectCleanups.get(clientId);
|
|
1977
|
+
if (cleanup) {
|
|
1978
|
+
cleanup();
|
|
1979
|
+
this._disconnectCleanups.delete(clientId);
|
|
1980
|
+
}
|
|
1981
|
+
this._ios = this._ios.filter((entry) => entry.io !== client.io);
|
|
1982
|
+
this._bss = this._bss.filter((entry) => entry.bs !== client.bs);
|
|
1983
|
+
this._clients.delete(clientId);
|
|
1984
|
+
await this._rebuildMultis();
|
|
1985
|
+
await this._refreshServers();
|
|
1986
|
+
this._removeAllListeners();
|
|
1987
|
+
this._multicastRefs();
|
|
1988
|
+
this._logger.info("Server", "Client socket removed", {
|
|
1989
|
+
clientId,
|
|
1990
|
+
remainingClients: this._clients.size
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
// ...........................................................................
|
|
1994
|
+
/**
|
|
1995
|
+
* Gracefully shuts down the server: stops timers, removes listeners,
|
|
1996
|
+
* clears all client state, and closes storage layers.
|
|
1997
|
+
*/
|
|
1998
|
+
async tearDown() {
|
|
1999
|
+
this._logger.info("Server", "Tearing down server");
|
|
2000
|
+
if (this._refEvictionTimer) {
|
|
2001
|
+
clearInterval(this._refEvictionTimer);
|
|
2002
|
+
this._refEvictionTimer = void 0;
|
|
2003
|
+
}
|
|
2004
|
+
if (this._bootstrapHeartbeatTimer) {
|
|
2005
|
+
clearInterval(this._bootstrapHeartbeatTimer);
|
|
2006
|
+
this._bootstrapHeartbeatTimer = void 0;
|
|
2007
|
+
}
|
|
2008
|
+
this._removeAllListeners();
|
|
2009
|
+
for (const cleanup of this._disconnectCleanups.values()) {
|
|
2010
|
+
cleanup();
|
|
2011
|
+
}
|
|
2012
|
+
this._disconnectCleanups.clear();
|
|
2013
|
+
this._clients.clear();
|
|
2014
|
+
this._pendingSockets = [];
|
|
2015
|
+
if (this._ioMulti && this._ioMulti.isOpen) {
|
|
2016
|
+
this._ioMulti.close();
|
|
2017
|
+
}
|
|
2018
|
+
this._ios = [];
|
|
2019
|
+
this._bss = [];
|
|
2020
|
+
this._multicastedRefsCurrent.clear();
|
|
2021
|
+
this._multicastedRefsPrevious.clear();
|
|
2022
|
+
this._refLog = [];
|
|
2023
|
+
this._latestRef = void 0;
|
|
2024
|
+
this._tornDown = true;
|
|
2025
|
+
this._logger.info("Server", "Server torn down successfully");
|
|
2026
|
+
}
|
|
2027
|
+
/**
|
|
2028
|
+
* Whether the server has been torn down.
|
|
2029
|
+
*/
|
|
2030
|
+
get isTornDown() {
|
|
2031
|
+
return this._tornDown;
|
|
2032
|
+
}
|
|
2033
|
+
// ...........................................................................
|
|
2034
|
+
/**
|
|
2035
|
+
* Registers a listener that auto-removes the client on socket disconnect.
|
|
2036
|
+
* @param clientId - Client identifier.
|
|
2037
|
+
* @param socket - The upstream socket to listen on.
|
|
2038
|
+
*/
|
|
2039
|
+
_registerDisconnectHandler(clientId, socket) {
|
|
2040
|
+
const handler = () => {
|
|
2041
|
+
this._logger.info("Server", "Client disconnected", { clientId });
|
|
2042
|
+
this.removeSocket(clientId);
|
|
2043
|
+
};
|
|
2044
|
+
socket.on("disconnect", handler);
|
|
2045
|
+
this._disconnectCleanups.set(clientId, () => {
|
|
2046
|
+
socket.off("disconnect", handler);
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
// ...........................................................................
|
|
2050
|
+
/**
|
|
2051
|
+
* Races a promise against a timeout. Resolves/rejects with the original
|
|
2052
|
+
* promise outcome if it settles first, or rejects with a timeout error.
|
|
2053
|
+
* @param promise - The promise to race.
|
|
2054
|
+
* @param label - Human-readable label for timeout error messages.
|
|
2055
|
+
*/
|
|
2056
|
+
_withTimeout(promise, label) {
|
|
2057
|
+
const ms = this._peerInitTimeoutMs;
|
|
2058
|
+
if (ms <= 0) return promise;
|
|
2059
|
+
let timer;
|
|
2060
|
+
const timeout = new Promise((_, reject) => {
|
|
2061
|
+
timer = setTimeout(
|
|
2062
|
+
/* v8 ignore next -- @preserve */
|
|
2063
|
+
() => reject(new Error(`Timeout after ${ms}ms: ${label}`)),
|
|
2064
|
+
ms
|
|
2065
|
+
);
|
|
2066
|
+
});
|
|
2067
|
+
return Promise.race([promise, timeout]).finally(
|
|
2068
|
+
/* v8 ignore next -- @preserve */
|
|
2069
|
+
() => clearTimeout(timer)
|
|
2070
|
+
);
|
|
2071
|
+
}
|
|
1338
2072
|
/** Example instance for test purposes */
|
|
1339
2073
|
static async example() {
|
|
1340
2074
|
const route = Route.fromFlat("example.route");
|
|
@@ -1401,7 +2135,13 @@ class SocketIoBridge {
|
|
|
1401
2135
|
}
|
|
1402
2136
|
}
|
|
1403
2137
|
export {
|
|
2138
|
+
BufferedLogger,
|
|
1404
2139
|
Client,
|
|
2140
|
+
ConsoleLogger,
|
|
2141
|
+
FilteredLogger,
|
|
2142
|
+
NoopLogger,
|
|
1405
2143
|
Server,
|
|
1406
|
-
SocketIoBridge
|
|
2144
|
+
SocketIoBridge,
|
|
2145
|
+
noopLogger,
|
|
2146
|
+
syncEvents2 as syncEvents
|
|
1407
2147
|
};
|