@rljson/server 0.0.5 → 0.0.7
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 +247 -11
- package/README.md +11 -3
- package/README.public.md +401 -10
- package/dist/README.architecture.md +247 -11
- package/dist/README.md +11 -3
- package/dist/README.public.md +401 -10
- package/dist/client.d.ts +70 -1
- package/dist/index.d.ts +6 -0
- package/dist/logger.d.ts +115 -0
- package/dist/server.d.ts +158 -4
- package/dist/server.js +843 -116
- 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
|
/**
|
|
@@ -977,6 +978,137 @@ class BaseNode {
|
|
|
977
978
|
await this._localDb.core.import(data);
|
|
978
979
|
}
|
|
979
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();
|
|
980
1112
|
function normalizeSocketBundle(socket) {
|
|
981
1113
|
const bundle = socket;
|
|
982
1114
|
if (bundle.ioUp && bundle.ioDown && bundle.bsUp && bundle.bsDown) {
|
|
@@ -997,26 +1129,57 @@ class Client extends BaseNode {
|
|
|
997
1129
|
* @param _socketToServer - Socket or namespace bundle to connect to server
|
|
998
1130
|
* @param _localIo - Local Io for local storage
|
|
999
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
|
|
1000
1134
|
*/
|
|
1001
|
-
constructor(_socketToServer, _localIo, _localBs) {
|
|
1135
|
+
constructor(_socketToServer, _localIo, _localBs, _route, options) {
|
|
1002
1136
|
super(_localIo);
|
|
1003
1137
|
this._socketToServer = _socketToServer;
|
|
1004
1138
|
this._localIo = _localIo;
|
|
1005
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
|
+
});
|
|
1006
1149
|
}
|
|
1007
1150
|
_ioMultiIos = [];
|
|
1008
1151
|
_ioMulti;
|
|
1009
1152
|
_bsMultiBss = [];
|
|
1010
1153
|
_bsMulti;
|
|
1154
|
+
_db;
|
|
1155
|
+
_connector;
|
|
1156
|
+
_logger;
|
|
1157
|
+
_syncConfig;
|
|
1158
|
+
_clientIdentity;
|
|
1159
|
+
_peerInitTimeoutMs;
|
|
1011
1160
|
/**
|
|
1012
1161
|
* Initializes Io and Bs multis and their peer bridges.
|
|
1013
1162
|
* @returns The initialized Io implementation.
|
|
1014
1163
|
*/
|
|
1015
1164
|
async init() {
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
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
|
+
}
|
|
1020
1183
|
}
|
|
1021
1184
|
/**
|
|
1022
1185
|
* Resolves once the Io implementation is ready.
|
|
@@ -1030,14 +1193,18 @@ class Client extends BaseNode {
|
|
|
1030
1193
|
* Closes client resources and clears internal state.
|
|
1031
1194
|
*/
|
|
1032
1195
|
async tearDown() {
|
|
1196
|
+
this._logger.info("Client", "Tearing down client");
|
|
1033
1197
|
if (this._ioMulti && this._ioMulti.isOpen) {
|
|
1034
1198
|
this._ioMulti.close();
|
|
1035
1199
|
}
|
|
1036
|
-
|
|
1037
|
-
this._ioMultiIos = [];
|
|
1200
|
+
this._connector?.tearDown();
|
|
1038
1201
|
this._bsMultiBss = [];
|
|
1202
|
+
this._ioMultiIos = [];
|
|
1039
1203
|
this._ioMulti = void 0;
|
|
1040
1204
|
this._bsMulti = void 0;
|
|
1205
|
+
this._db = void 0;
|
|
1206
|
+
this._connector = void 0;
|
|
1207
|
+
this._logger.info("Client", "Client torn down successfully");
|
|
1041
1208
|
}
|
|
1042
1209
|
/**
|
|
1043
1210
|
* Returns the Io implementation.
|
|
@@ -1051,98 +1218,232 @@ class Client extends BaseNode {
|
|
|
1051
1218
|
get bs() {
|
|
1052
1219
|
return this._bsMulti;
|
|
1053
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
|
+
}
|
|
1054
1264
|
/**
|
|
1055
1265
|
* Builds the Io multi with local and peer layers.
|
|
1056
1266
|
*/
|
|
1057
1267
|
async _setupIo() {
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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
|
+
}
|
|
1079
1297
|
}
|
|
1080
1298
|
/**
|
|
1081
1299
|
* Builds the Bs multi with local and peer layers.
|
|
1082
1300
|
*/
|
|
1083
1301
|
async _setupBs() {
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
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
|
+
}
|
|
1102
1328
|
}
|
|
1103
1329
|
/**
|
|
1104
1330
|
* Creates and initializes a downstream Io peer.
|
|
1105
1331
|
* @param socket - Downstream socket to the server Io namespace.
|
|
1106
1332
|
*/
|
|
1107
1333
|
async _createIoPeer(socket) {
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
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
|
+
}
|
|
1112
1351
|
}
|
|
1113
1352
|
/**
|
|
1114
1353
|
* Creates and initializes a downstream Bs peer.
|
|
1115
1354
|
* @param socket - Downstream socket to the server Bs namespace.
|
|
1116
1355
|
*/
|
|
1117
1356
|
async _createBsPeer(socket) {
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
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
|
+
);
|
|
1121
1400
|
}
|
|
1122
1401
|
}
|
|
1123
1402
|
class Server extends BaseNode {
|
|
1124
|
-
constructor(_route, _localIo, _localBs) {
|
|
1403
|
+
constructor(_route, _localIo, _localBs, options) {
|
|
1125
1404
|
super(_localIo);
|
|
1126
1405
|
this._route = _route;
|
|
1127
1406
|
this._localIo = _localIo;
|
|
1128
1407
|
this._localBs = _localBs;
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
this.
|
|
1408
|
+
this._logger = options?.logger ?? noopLogger;
|
|
1409
|
+
this._peerInitTimeoutMs = options?.peerInitTimeoutMs ?? 3e4;
|
|
1410
|
+
this._disableLocalCache = options?.disableLocalCache ?? false;
|
|
1411
|
+
this._syncConfig = options?.syncConfig;
|
|
1412
|
+
this._refLogSize = options?.refLogSize ?? 1e3;
|
|
1413
|
+
this._ackTimeoutMs = options?.ackTimeoutMs ?? options?.syncConfig?.ackTimeoutMs ?? 1e4;
|
|
1414
|
+
this._events = syncEvents(this._route.flat);
|
|
1415
|
+
this._logger.info("Server", "Constructing server", {
|
|
1416
|
+
route: this._route.flat
|
|
1417
|
+
});
|
|
1418
|
+
const evictionMs = options?.refEvictionIntervalMs ?? 6e4;
|
|
1419
|
+
if (evictionMs > 0) {
|
|
1420
|
+
this._refEvictionTimer = setInterval(() => {
|
|
1421
|
+
this._multicastedRefsPrevious = this._multicastedRefsCurrent;
|
|
1422
|
+
this._multicastedRefsCurrent = /* @__PURE__ */ new Set();
|
|
1423
|
+
}, evictionMs);
|
|
1424
|
+
this._refEvictionTimer.unref();
|
|
1425
|
+
}
|
|
1426
|
+
if (!this._disableLocalCache) {
|
|
1427
|
+
const ioMultiIoLocal = {
|
|
1428
|
+
io: this._localIo,
|
|
1429
|
+
dump: true,
|
|
1430
|
+
read: true,
|
|
1431
|
+
write: true,
|
|
1432
|
+
priority: 1
|
|
1433
|
+
};
|
|
1434
|
+
this._ios.push(ioMultiIoLocal);
|
|
1435
|
+
}
|
|
1137
1436
|
this._ioMulti = new IoMulti(this._ios);
|
|
1138
1437
|
this._ioServer = new IoServer(this._ioMulti);
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1438
|
+
if (!this._disableLocalCache) {
|
|
1439
|
+
const bsMultiBsLocal = {
|
|
1440
|
+
bs: this._localBs,
|
|
1441
|
+
read: true,
|
|
1442
|
+
write: true,
|
|
1443
|
+
priority: 1
|
|
1444
|
+
};
|
|
1445
|
+
this._bss.push(bsMultiBsLocal);
|
|
1446
|
+
}
|
|
1146
1447
|
this._bsMulti = new BsMulti(this._bss);
|
|
1147
1448
|
this._bsServer = new BsServer(this._bsMulti);
|
|
1148
1449
|
}
|
|
@@ -1158,18 +1459,44 @@ class Server extends BaseNode {
|
|
|
1158
1459
|
_bsMulti;
|
|
1159
1460
|
// Storage => Let Clients read from Servers Bs
|
|
1160
1461
|
_bsServer;
|
|
1161
|
-
//
|
|
1162
|
-
|
|
1462
|
+
// Two-generation ref dedup: refs in current or previous are considered seen.
|
|
1463
|
+
// On each eviction tick, previous is discarded and current becomes previous.
|
|
1464
|
+
_multicastedRefsCurrent = /* @__PURE__ */ new Set();
|
|
1465
|
+
_multicastedRefsPrevious = /* @__PURE__ */ new Set();
|
|
1466
|
+
_refEvictionTimer;
|
|
1163
1467
|
_refreshPromise;
|
|
1164
1468
|
_pendingSockets = [];
|
|
1469
|
+
_logger;
|
|
1470
|
+
_peerInitTimeoutMs;
|
|
1471
|
+
// Cleanup callbacks for socket disconnect listeners (clientId → cleanup fn)
|
|
1472
|
+
_disconnectCleanups = /* @__PURE__ */ new Map();
|
|
1473
|
+
// Sync protocol state
|
|
1474
|
+
_syncConfig;
|
|
1475
|
+
_events;
|
|
1476
|
+
_refLog = [];
|
|
1477
|
+
_refLogSize;
|
|
1478
|
+
_ackTimeoutMs;
|
|
1479
|
+
// Local cache toggle
|
|
1480
|
+
_disableLocalCache;
|
|
1481
|
+
// Bootstrap state
|
|
1482
|
+
_latestRef;
|
|
1483
|
+
_bootstrapHeartbeatTimer;
|
|
1484
|
+
_tornDown = false;
|
|
1165
1485
|
/**
|
|
1166
1486
|
* Initializes Io and Bs multis on the server.
|
|
1167
1487
|
*/
|
|
1168
1488
|
async init() {
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1489
|
+
this._logger.info("Server", "Initializing server");
|
|
1490
|
+
try {
|
|
1491
|
+
await this._ioMulti.init();
|
|
1492
|
+
await this._ioMulti.isReady();
|
|
1493
|
+
await this._bsMulti.init();
|
|
1494
|
+
await this.ready();
|
|
1495
|
+
this._logger.info("Server", "Server initialized successfully");
|
|
1496
|
+
} catch (error) {
|
|
1497
|
+
this._logger.error("Server", "Failed to initialize server", error);
|
|
1498
|
+
throw error;
|
|
1499
|
+
}
|
|
1173
1500
|
}
|
|
1174
1501
|
/**
|
|
1175
1502
|
* Resolves once the Io implementation is ready.
|
|
@@ -1185,6 +1512,7 @@ class Server extends BaseNode {
|
|
|
1185
1512
|
async addSocket(socket) {
|
|
1186
1513
|
const sockets = normalizeSocketBundle(socket);
|
|
1187
1514
|
const clientId = `client_${this._clients.size}_${Math.random().toString(36).slice(2)}`;
|
|
1515
|
+
this._logger.info("Server", "Adding client socket", { clientId });
|
|
1188
1516
|
const ioUp = sockets.ioUp;
|
|
1189
1517
|
const ioDown = sockets.ioDown;
|
|
1190
1518
|
const bsUp = sockets.bsUp;
|
|
@@ -1193,20 +1521,34 @@ class Server extends BaseNode {
|
|
|
1193
1521
|
ioDown.__clientId = clientId;
|
|
1194
1522
|
bsUp.__clientId = clientId;
|
|
1195
1523
|
bsDown.__clientId = clientId;
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1524
|
+
try {
|
|
1525
|
+
const ioPeer = await this._createIoPeer(socket, clientId);
|
|
1526
|
+
const bsPeer = await this._createBsPeer(socket, clientId);
|
|
1527
|
+
this._registerClient(
|
|
1528
|
+
clientId,
|
|
1529
|
+
{ ioUp, ioDown, bsUp, bsDown },
|
|
1530
|
+
ioPeer,
|
|
1531
|
+
bsPeer
|
|
1532
|
+
);
|
|
1533
|
+
this._pendingSockets.push({ ioDown, bsDown });
|
|
1534
|
+
this._queueIoPeer(ioPeer);
|
|
1535
|
+
this._queueBsPeer(bsPeer);
|
|
1536
|
+
await this._queueRefresh();
|
|
1537
|
+
this._removeAllListeners();
|
|
1538
|
+
this._multicastRefs();
|
|
1539
|
+
this._registerDisconnectHandler(clientId, ioUp);
|
|
1540
|
+
this._sendBootstrap(ioDown);
|
|
1541
|
+
this._startBootstrapHeartbeat();
|
|
1542
|
+
this._logger.info("Server", "Client socket added successfully", {
|
|
1543
|
+
clientId,
|
|
1544
|
+
totalClients: this._clients.size
|
|
1545
|
+
});
|
|
1546
|
+
} catch (error) {
|
|
1547
|
+
this._logger.error("Server", "Failed to add client socket", error, {
|
|
1548
|
+
clientId
|
|
1549
|
+
});
|
|
1550
|
+
throw error;
|
|
1551
|
+
}
|
|
1210
1552
|
return this;
|
|
1211
1553
|
}
|
|
1212
1554
|
// ...........................................................................
|
|
@@ -1216,25 +1558,50 @@ class Server extends BaseNode {
|
|
|
1216
1558
|
_removeAllListeners() {
|
|
1217
1559
|
for (const { ioUp } of this._clients.values()) {
|
|
1218
1560
|
ioUp.removeAllListeners(this._route.flat);
|
|
1561
|
+
ioUp.removeAllListeners(this._events.gapFillReq);
|
|
1562
|
+
ioUp.removeAllListeners(this._events.ackClient);
|
|
1219
1563
|
}
|
|
1220
1564
|
}
|
|
1221
1565
|
// ...........................................................................
|
|
1222
1566
|
/**
|
|
1223
1567
|
* Broadcasts incoming payloads from any client to all other connected clients.
|
|
1224
|
-
*
|
|
1568
|
+
* Enriched with ref log, ACK aggregation, and gap-fill support when
|
|
1569
|
+
* syncConfig is provided.
|
|
1225
1570
|
*/
|
|
1226
1571
|
_multicastRefs = () => {
|
|
1227
1572
|
for (const [clientIdA, { ioUp: socketA }] of this._clients.entries()) {
|
|
1228
1573
|
socketA.on(this._route.flat, (payload) => {
|
|
1229
1574
|
const ref = payload.r;
|
|
1230
|
-
|
|
1575
|
+
this._logger.traffic("in", "Server.Multicast", this._route.flat, {
|
|
1576
|
+
ref,
|
|
1577
|
+
from: clientIdA
|
|
1578
|
+
});
|
|
1579
|
+
if (this._multicastedRefsCurrent.has(ref) || this._multicastedRefsPrevious.has(ref)) {
|
|
1580
|
+
this._logger.warn("Server.Multicast", "Duplicate ref suppressed", {
|
|
1581
|
+
ref,
|
|
1582
|
+
from: clientIdA
|
|
1583
|
+
});
|
|
1231
1584
|
return;
|
|
1232
1585
|
}
|
|
1233
|
-
this.
|
|
1586
|
+
this._multicastedRefsCurrent.add(ref);
|
|
1587
|
+
this._latestRef = ref;
|
|
1234
1588
|
const p = payload;
|
|
1235
1589
|
if (p && p.__origin) {
|
|
1590
|
+
this._logger.warn(
|
|
1591
|
+
"Server.Multicast",
|
|
1592
|
+
"Loop prevention: payload already has origin",
|
|
1593
|
+
{ ref, origin: p.__origin, from: clientIdA }
|
|
1594
|
+
);
|
|
1236
1595
|
return;
|
|
1237
1596
|
}
|
|
1597
|
+
if (this._syncConfig) {
|
|
1598
|
+
this._appendToRefLog(payload);
|
|
1599
|
+
}
|
|
1600
|
+
let receiverCount = 0;
|
|
1601
|
+
let ackCollector;
|
|
1602
|
+
if (this._syncConfig?.requireAck) {
|
|
1603
|
+
ackCollector = this._setupAckCollection(clientIdA, ref);
|
|
1604
|
+
}
|
|
1238
1605
|
for (const [
|
|
1239
1606
|
clientIdB,
|
|
1240
1607
|
{ ioDown: socketB }
|
|
@@ -1243,15 +1610,33 @@ class Server extends BaseNode {
|
|
|
1243
1610
|
const forwarded = Object.assign({}, payload, {
|
|
1244
1611
|
__origin: clientIdA
|
|
1245
1612
|
});
|
|
1613
|
+
this._logger.traffic("out", "Server.Multicast", this._route.flat, {
|
|
1614
|
+
ref,
|
|
1615
|
+
from: clientIdA,
|
|
1616
|
+
to: clientIdB
|
|
1617
|
+
});
|
|
1246
1618
|
socketB.emit(this._route.flat, forwarded);
|
|
1619
|
+
receiverCount++;
|
|
1247
1620
|
}
|
|
1248
1621
|
}
|
|
1622
|
+
if (ackCollector && receiverCount === 0) {
|
|
1623
|
+
ackCollector();
|
|
1624
|
+
}
|
|
1249
1625
|
});
|
|
1626
|
+
if (this._syncConfig?.causalOrdering) {
|
|
1627
|
+
this._registerGapFillListener(clientIdA, socketA);
|
|
1628
|
+
}
|
|
1250
1629
|
}
|
|
1251
1630
|
};
|
|
1252
1631
|
get route() {
|
|
1253
1632
|
return this._route;
|
|
1254
1633
|
}
|
|
1634
|
+
/**
|
|
1635
|
+
* Returns the logger instance.
|
|
1636
|
+
*/
|
|
1637
|
+
get logger() {
|
|
1638
|
+
return this._logger;
|
|
1639
|
+
}
|
|
1255
1640
|
/**
|
|
1256
1641
|
* Returns the Io implementation.
|
|
1257
1642
|
*/
|
|
@@ -1270,24 +1655,227 @@ class Server extends BaseNode {
|
|
|
1270
1655
|
get clients() {
|
|
1271
1656
|
return this._clients;
|
|
1272
1657
|
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Returns the sync configuration, if any.
|
|
1660
|
+
*/
|
|
1661
|
+
get syncConfig() {
|
|
1662
|
+
return this._syncConfig;
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* Returns the typed sync event names.
|
|
1666
|
+
*/
|
|
1667
|
+
get events() {
|
|
1668
|
+
return this._events;
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Returns the configured maximum ref log size.
|
|
1672
|
+
*/
|
|
1673
|
+
get refLogSize() {
|
|
1674
|
+
return this._refLogSize;
|
|
1675
|
+
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Returns the current ref log contents (for diagnostics / testing).
|
|
1678
|
+
*/
|
|
1679
|
+
get refLog() {
|
|
1680
|
+
return this._refLog;
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Returns whether the local cache is disabled.
|
|
1684
|
+
*/
|
|
1685
|
+
get isLocalCacheDisabled() {
|
|
1686
|
+
return this._disableLocalCache;
|
|
1687
|
+
}
|
|
1688
|
+
/**
|
|
1689
|
+
* Returns the latest ref tracked by the server (for bootstrap / diagnostics).
|
|
1690
|
+
*/
|
|
1691
|
+
get latestRef() {
|
|
1692
|
+
return this._latestRef;
|
|
1693
|
+
}
|
|
1694
|
+
// ...........................................................................
|
|
1695
|
+
// Sync protocol private methods
|
|
1696
|
+
// ...........................................................................
|
|
1697
|
+
/**
|
|
1698
|
+
* Appends a payload to the bounded ref log (ring buffer).
|
|
1699
|
+
* Drops the oldest entry when the log exceeds `_refLogSize`.
|
|
1700
|
+
* @param payload - The ConnectorPayload to append.
|
|
1701
|
+
*/
|
|
1702
|
+
_appendToRefLog(payload) {
|
|
1703
|
+
this._refLog.push(payload);
|
|
1704
|
+
if (this._refLog.length > this._refLogSize) {
|
|
1705
|
+
this._refLog.shift();
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
// ...........................................................................
|
|
1709
|
+
// Bootstrap methods
|
|
1710
|
+
// ...........................................................................
|
|
1711
|
+
/**
|
|
1712
|
+
* Sends the latest ref to a specific client socket as a bootstrap message.
|
|
1713
|
+
* If no ref has been seen yet, this is a no-op.
|
|
1714
|
+
* @param ioDown - The downstream socket to send the bootstrap message on.
|
|
1715
|
+
*/
|
|
1716
|
+
_sendBootstrap(ioDown) {
|
|
1717
|
+
if (!this._latestRef) return;
|
|
1718
|
+
const payload = {
|
|
1719
|
+
o: "__server__",
|
|
1720
|
+
r: this._latestRef
|
|
1721
|
+
};
|
|
1722
|
+
this._logger.info("Server.Bootstrap", "Sending bootstrap ref", {
|
|
1723
|
+
ref: this._latestRef,
|
|
1724
|
+
to: ioDown.__clientId
|
|
1725
|
+
});
|
|
1726
|
+
ioDown.emit(this._events.bootstrap, payload);
|
|
1727
|
+
}
|
|
1728
|
+
/**
|
|
1729
|
+
* Broadcasts the latest ref to all connected clients as a heartbeat.
|
|
1730
|
+
* Each client's dedup pipeline will filter out refs it already has.
|
|
1731
|
+
*/
|
|
1732
|
+
_broadcastBootstrapHeartbeat() {
|
|
1733
|
+
if (!this._latestRef) return;
|
|
1734
|
+
const payload = {
|
|
1735
|
+
o: "__server__",
|
|
1736
|
+
r: this._latestRef
|
|
1737
|
+
};
|
|
1738
|
+
this._logger.info("Server.Bootstrap", "Heartbeat broadcast", {
|
|
1739
|
+
ref: this._latestRef,
|
|
1740
|
+
clientCount: this._clients.size
|
|
1741
|
+
});
|
|
1742
|
+
for (const { ioDown } of this._clients.values()) {
|
|
1743
|
+
ioDown.emit(this._events.bootstrap, payload);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Starts the periodic bootstrap heartbeat timer if configured
|
|
1748
|
+
* and not already running.
|
|
1749
|
+
*/
|
|
1750
|
+
_startBootstrapHeartbeat() {
|
|
1751
|
+
const ms = this._syncConfig?.bootstrapHeartbeatMs;
|
|
1752
|
+
if (!ms || ms <= 0 || this._bootstrapHeartbeatTimer) return;
|
|
1753
|
+
this._bootstrapHeartbeatTimer = setInterval(() => {
|
|
1754
|
+
this._broadcastBootstrapHeartbeat();
|
|
1755
|
+
}, ms);
|
|
1756
|
+
this._bootstrapHeartbeatTimer.unref();
|
|
1757
|
+
this._logger.info("Server.Bootstrap", "Heartbeat timer started", {
|
|
1758
|
+
intervalMs: ms
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
/**
|
|
1762
|
+
* Sets up ACK collection listeners before broadcast.
|
|
1763
|
+
* Returns a cleanup function that emits an immediate ACK
|
|
1764
|
+
* (used when there are no receivers).
|
|
1765
|
+
* @param senderClientId - The internal client ID of the sender.
|
|
1766
|
+
* @param ref - The ref being acknowledged.
|
|
1767
|
+
* @returns A function to call for immediate ACK (zero receivers).
|
|
1768
|
+
*/
|
|
1769
|
+
_setupAckCollection(senderClientId, ref) {
|
|
1770
|
+
const senderEntry = this._clients.get(senderClientId);
|
|
1771
|
+
if (!senderEntry) return () => {
|
|
1772
|
+
};
|
|
1773
|
+
const totalClients = this._clients.size - 1;
|
|
1774
|
+
let acksReceived = 0;
|
|
1775
|
+
let finished = false;
|
|
1776
|
+
const ackHandlers = /* @__PURE__ */ new Map();
|
|
1777
|
+
const finish = (ok) => {
|
|
1778
|
+
if (finished) return;
|
|
1779
|
+
finished = true;
|
|
1780
|
+
clearTimeout(timeout);
|
|
1781
|
+
for (const [cId, handler] of ackHandlers.entries()) {
|
|
1782
|
+
const clientEntry = this._clients.get(cId);
|
|
1783
|
+
if (clientEntry) {
|
|
1784
|
+
clientEntry.ioUp.off(this._events.ackClient, handler);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
ackHandlers.clear();
|
|
1788
|
+
const ack = {
|
|
1789
|
+
r: ref,
|
|
1790
|
+
ok,
|
|
1791
|
+
receivedBy: acksReceived,
|
|
1792
|
+
totalClients
|
|
1793
|
+
};
|
|
1794
|
+
senderEntry.ioDown.emit(this._events.ack, ack);
|
|
1795
|
+
};
|
|
1796
|
+
const timeout = setTimeout(() => {
|
|
1797
|
+
finish(false);
|
|
1798
|
+
}, this._ackTimeoutMs);
|
|
1799
|
+
for (const [clientId, { ioUp }] of this._clients.entries()) {
|
|
1800
|
+
if (clientId === senderClientId) continue;
|
|
1801
|
+
const handler = (clientAck) => {
|
|
1802
|
+
if (clientAck.r !== ref) return;
|
|
1803
|
+
acksReceived++;
|
|
1804
|
+
if (acksReceived >= totalClients) {
|
|
1805
|
+
finish(true);
|
|
1806
|
+
}
|
|
1807
|
+
};
|
|
1808
|
+
ioUp.on(this._events.ackClient, handler);
|
|
1809
|
+
ackHandlers.set(clientId, handler);
|
|
1810
|
+
}
|
|
1811
|
+
return () => {
|
|
1812
|
+
finish(true);
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
/**
|
|
1816
|
+
* Registers a gap-fill request listener for a specific client socket.
|
|
1817
|
+
* @param clientId - The internal client ID.
|
|
1818
|
+
* @param socket - The upstream socket to listen on.
|
|
1819
|
+
*/
|
|
1820
|
+
_registerGapFillListener(clientId, socket) {
|
|
1821
|
+
socket.on(this._events.gapFillReq, (req) => {
|
|
1822
|
+
this._logger.info("Server.GapFill", "Gap-fill request received", {
|
|
1823
|
+
from: clientId,
|
|
1824
|
+
afterSeq: req.afterSeq
|
|
1825
|
+
});
|
|
1826
|
+
const refs = this._refLog.filter(
|
|
1827
|
+
(p) => p.seq != null && p.seq > req.afterSeq
|
|
1828
|
+
);
|
|
1829
|
+
const res = {
|
|
1830
|
+
route: req.route,
|
|
1831
|
+
refs
|
|
1832
|
+
};
|
|
1833
|
+
const clientEntry = this._clients.get(clientId);
|
|
1834
|
+
if (clientEntry) {
|
|
1835
|
+
clientEntry.ioDown.emit(this._events.gapFillRes, res);
|
|
1836
|
+
}
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1273
1839
|
/**
|
|
1274
1840
|
* Creates and initializes a downstream Io peer for a socket.
|
|
1275
1841
|
* @param socket - Client socket to bind the peer to.
|
|
1842
|
+
* @param clientId - Client identifier for logging.
|
|
1276
1843
|
*/
|
|
1277
|
-
async _createIoPeer(socket) {
|
|
1278
|
-
const
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1844
|
+
async _createIoPeer(socket, clientId) {
|
|
1845
|
+
const sockets = normalizeSocketBundle(socket);
|
|
1846
|
+
this._logger.info("Server.Io", "Creating Io peer", { clientId });
|
|
1847
|
+
try {
|
|
1848
|
+
const ioPeer = new IoPeer(sockets.ioUp);
|
|
1849
|
+
await this._withTimeout(ioPeer.init(), "IoPeer.init()");
|
|
1850
|
+
await this._withTimeout(ioPeer.isReady(), "IoPeer.isReady()");
|
|
1851
|
+
this._logger.info("Server.Io", "Io peer created", { clientId });
|
|
1852
|
+
return ioPeer;
|
|
1853
|
+
} catch (error) {
|
|
1854
|
+
this._logger.error("Server.Io", "Failed to create Io peer", error, {
|
|
1855
|
+
clientId
|
|
1856
|
+
});
|
|
1857
|
+
throw error;
|
|
1858
|
+
}
|
|
1282
1859
|
}
|
|
1283
1860
|
/**
|
|
1284
1861
|
* Creates and initializes a downstream Bs peer for a socket.
|
|
1285
1862
|
* @param socket - Client socket to bind the peer to.
|
|
1863
|
+
* @param clientId - Client identifier for logging.
|
|
1286
1864
|
*/
|
|
1287
|
-
async _createBsPeer(socket) {
|
|
1288
|
-
const
|
|
1289
|
-
|
|
1290
|
-
|
|
1865
|
+
async _createBsPeer(socket, clientId) {
|
|
1866
|
+
const sockets = normalizeSocketBundle(socket);
|
|
1867
|
+
this._logger.info("Server.Bs", "Creating Bs peer", { clientId });
|
|
1868
|
+
try {
|
|
1869
|
+
const bsPeer = new BsPeer(sockets.bsUp);
|
|
1870
|
+
await this._withTimeout(bsPeer.init(), "BsPeer.init()");
|
|
1871
|
+
this._logger.info("Server.Bs", "Bs peer created", { clientId });
|
|
1872
|
+
return bsPeer;
|
|
1873
|
+
} catch (error) {
|
|
1874
|
+
this._logger.error("Server.Bs", "Failed to create Bs peer", error, {
|
|
1875
|
+
clientId
|
|
1876
|
+
});
|
|
1877
|
+
throw error;
|
|
1878
|
+
}
|
|
1291
1879
|
}
|
|
1292
1880
|
/**
|
|
1293
1881
|
* Registers the client socket and peers.
|
|
@@ -1335,38 +1923,171 @@ class Server extends BaseNode {
|
|
|
1335
1923
|
* Rebuilds Io and Bs multis from queued peers.
|
|
1336
1924
|
*/
|
|
1337
1925
|
async _rebuildMultis() {
|
|
1338
|
-
this.
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1926
|
+
this._logger.info("Server", "Rebuilding multis", {
|
|
1927
|
+
ioCount: this._ios.length,
|
|
1928
|
+
bsCount: this._bss.length
|
|
1929
|
+
});
|
|
1930
|
+
try {
|
|
1931
|
+
this._ioMulti = new IoMulti(this._ios);
|
|
1932
|
+
await this._ioMulti.init();
|
|
1933
|
+
await this._ioMulti.isReady();
|
|
1934
|
+
this._bsMulti = new BsMulti(this._bss);
|
|
1935
|
+
await this._bsMulti.init();
|
|
1936
|
+
this._logger.info("Server", "Multis rebuilt successfully");
|
|
1937
|
+
} catch (error) {
|
|
1938
|
+
this._logger.error("Server", "Failed to rebuild multis", error);
|
|
1939
|
+
throw error;
|
|
1940
|
+
}
|
|
1343
1941
|
}
|
|
1344
1942
|
/**
|
|
1345
1943
|
* Recreates servers and reattaches sockets.
|
|
1346
1944
|
*/
|
|
1347
1945
|
async _refreshServers() {
|
|
1348
|
-
this.
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1946
|
+
this._logger.info("Server", "Refreshing servers", {
|
|
1947
|
+
pendingSockets: this._pendingSockets.length
|
|
1948
|
+
});
|
|
1949
|
+
try {
|
|
1950
|
+
this._ioServer._io = this._ioMulti;
|
|
1951
|
+
this._bsServer._bs = this._bsMulti;
|
|
1952
|
+
for (const pending of this._pendingSockets) {
|
|
1953
|
+
await this._ioServer.addSocket(pending.ioDown);
|
|
1954
|
+
await this._bsServer.addSocket(pending.bsDown);
|
|
1955
|
+
}
|
|
1956
|
+
this._pendingSockets = [];
|
|
1957
|
+
this._logger.info("Server", "Servers refreshed successfully");
|
|
1958
|
+
} catch (error) {
|
|
1959
|
+
this._logger.error("Server", "Failed to refresh servers", error);
|
|
1960
|
+
throw error;
|
|
1353
1961
|
}
|
|
1354
|
-
this._pendingSockets = [];
|
|
1355
1962
|
}
|
|
1356
1963
|
/**
|
|
1357
1964
|
* Batches multi/server refreshes into a single queued task.
|
|
1358
1965
|
*/
|
|
1359
1966
|
_queueRefresh() {
|
|
1360
1967
|
if (!this._refreshPromise) {
|
|
1968
|
+
this._logger.info("Server", "Queuing refresh");
|
|
1361
1969
|
this._refreshPromise = Promise.resolve().then(async () => {
|
|
1362
1970
|
await this._rebuildMultis();
|
|
1363
1971
|
await this._refreshServers();
|
|
1364
|
-
}).
|
|
1972
|
+
}).catch(
|
|
1973
|
+
/* v8 ignore next -- @preserve */
|
|
1974
|
+
(error) => {
|
|
1975
|
+
this._logger.error("Server", "Queued refresh failed", error);
|
|
1976
|
+
throw error;
|
|
1977
|
+
}
|
|
1978
|
+
).finally(() => {
|
|
1365
1979
|
this._refreshPromise = void 0;
|
|
1366
1980
|
});
|
|
1367
1981
|
}
|
|
1368
1982
|
return this._refreshPromise;
|
|
1369
1983
|
}
|
|
1984
|
+
// ...........................................................................
|
|
1985
|
+
/**
|
|
1986
|
+
* Removes a connected client by its internal client ID.
|
|
1987
|
+
* Cleans up listeners, peers, and rebuilds multis.
|
|
1988
|
+
* @param clientId - The client identifier (from server.clients keys).
|
|
1989
|
+
*/
|
|
1990
|
+
async removeSocket(clientId) {
|
|
1991
|
+
const client = this._clients.get(clientId);
|
|
1992
|
+
if (!client) return;
|
|
1993
|
+
this._logger.info("Server", "Removing client socket", { clientId });
|
|
1994
|
+
client.ioUp.removeAllListeners(this._route.flat);
|
|
1995
|
+
const cleanup = this._disconnectCleanups.get(clientId);
|
|
1996
|
+
if (cleanup) {
|
|
1997
|
+
cleanup();
|
|
1998
|
+
this._disconnectCleanups.delete(clientId);
|
|
1999
|
+
}
|
|
2000
|
+
this._ios = this._ios.filter((entry) => entry.io !== client.io);
|
|
2001
|
+
this._bss = this._bss.filter((entry) => entry.bs !== client.bs);
|
|
2002
|
+
this._clients.delete(clientId);
|
|
2003
|
+
await this._rebuildMultis();
|
|
2004
|
+
await this._refreshServers();
|
|
2005
|
+
this._removeAllListeners();
|
|
2006
|
+
this._multicastRefs();
|
|
2007
|
+
this._logger.info("Server", "Client socket removed", {
|
|
2008
|
+
clientId,
|
|
2009
|
+
remainingClients: this._clients.size
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
// ...........................................................................
|
|
2013
|
+
/**
|
|
2014
|
+
* Gracefully shuts down the server: stops timers, removes listeners,
|
|
2015
|
+
* clears all client state, and closes storage layers.
|
|
2016
|
+
*/
|
|
2017
|
+
async tearDown() {
|
|
2018
|
+
this._logger.info("Server", "Tearing down server");
|
|
2019
|
+
if (this._refEvictionTimer) {
|
|
2020
|
+
clearInterval(this._refEvictionTimer);
|
|
2021
|
+
this._refEvictionTimer = void 0;
|
|
2022
|
+
}
|
|
2023
|
+
if (this._bootstrapHeartbeatTimer) {
|
|
2024
|
+
clearInterval(this._bootstrapHeartbeatTimer);
|
|
2025
|
+
this._bootstrapHeartbeatTimer = void 0;
|
|
2026
|
+
}
|
|
2027
|
+
this._removeAllListeners();
|
|
2028
|
+
for (const cleanup of this._disconnectCleanups.values()) {
|
|
2029
|
+
cleanup();
|
|
2030
|
+
}
|
|
2031
|
+
this._disconnectCleanups.clear();
|
|
2032
|
+
this._clients.clear();
|
|
2033
|
+
this._pendingSockets = [];
|
|
2034
|
+
if (this._ioMulti && this._ioMulti.isOpen) {
|
|
2035
|
+
this._ioMulti.close();
|
|
2036
|
+
}
|
|
2037
|
+
this._ios = [];
|
|
2038
|
+
this._bss = [];
|
|
2039
|
+
this._multicastedRefsCurrent.clear();
|
|
2040
|
+
this._multicastedRefsPrevious.clear();
|
|
2041
|
+
this._refLog = [];
|
|
2042
|
+
this._latestRef = void 0;
|
|
2043
|
+
this._tornDown = true;
|
|
2044
|
+
this._logger.info("Server", "Server torn down successfully");
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* Whether the server has been torn down.
|
|
2048
|
+
*/
|
|
2049
|
+
get isTornDown() {
|
|
2050
|
+
return this._tornDown;
|
|
2051
|
+
}
|
|
2052
|
+
// ...........................................................................
|
|
2053
|
+
/**
|
|
2054
|
+
* Registers a listener that auto-removes the client on socket disconnect.
|
|
2055
|
+
* @param clientId - Client identifier.
|
|
2056
|
+
* @param socket - The upstream socket to listen on.
|
|
2057
|
+
*/
|
|
2058
|
+
_registerDisconnectHandler(clientId, socket) {
|
|
2059
|
+
const handler = () => {
|
|
2060
|
+
this._logger.info("Server", "Client disconnected", { clientId });
|
|
2061
|
+
this.removeSocket(clientId);
|
|
2062
|
+
};
|
|
2063
|
+
socket.on("disconnect", handler);
|
|
2064
|
+
this._disconnectCleanups.set(clientId, () => {
|
|
2065
|
+
socket.off("disconnect", handler);
|
|
2066
|
+
});
|
|
2067
|
+
}
|
|
2068
|
+
// ...........................................................................
|
|
2069
|
+
/**
|
|
2070
|
+
* Races a promise against a timeout. Resolves/rejects with the original
|
|
2071
|
+
* promise outcome if it settles first, or rejects with a timeout error.
|
|
2072
|
+
* @param promise - The promise to race.
|
|
2073
|
+
* @param label - Human-readable label for timeout error messages.
|
|
2074
|
+
*/
|
|
2075
|
+
_withTimeout(promise, label) {
|
|
2076
|
+
const ms = this._peerInitTimeoutMs;
|
|
2077
|
+
if (ms <= 0) return promise;
|
|
2078
|
+
let timer;
|
|
2079
|
+
const timeout = new Promise((_, reject) => {
|
|
2080
|
+
timer = setTimeout(
|
|
2081
|
+
/* v8 ignore next -- @preserve */
|
|
2082
|
+
() => reject(new Error(`Timeout after ${ms}ms: ${label}`)),
|
|
2083
|
+
ms
|
|
2084
|
+
);
|
|
2085
|
+
});
|
|
2086
|
+
return Promise.race([promise, timeout]).finally(
|
|
2087
|
+
/* v8 ignore next -- @preserve */
|
|
2088
|
+
() => clearTimeout(timer)
|
|
2089
|
+
);
|
|
2090
|
+
}
|
|
1370
2091
|
/** Example instance for test purposes */
|
|
1371
2092
|
static async example() {
|
|
1372
2093
|
const route = Route.fromFlat("example.route");
|
|
@@ -1433,7 +2154,13 @@ class SocketIoBridge {
|
|
|
1433
2154
|
}
|
|
1434
2155
|
}
|
|
1435
2156
|
export {
|
|
2157
|
+
BufferedLogger,
|
|
1436
2158
|
Client,
|
|
2159
|
+
ConsoleLogger,
|
|
2160
|
+
FilteredLogger,
|
|
2161
|
+
NoopLogger,
|
|
1437
2162
|
Server,
|
|
1438
|
-
SocketIoBridge
|
|
2163
|
+
SocketIoBridge,
|
|
2164
|
+
noopLogger,
|
|
2165
|
+
syncEvents2 as syncEvents
|
|
1439
2166
|
};
|