@rljson/server 0.0.5 → 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/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 { Db } from "@rljson/db";
5
- import { Route } from "@rljson/rljson";
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
- await this._setupIo();
1017
- await this._setupBs();
1018
- await this.ready();
1019
- return this._ioMulti;
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
- if (this._bsMulti) ;
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,81 +1218,210 @@ 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
- const sockets = normalizeSocketBundle(this._socketToServer);
1059
- this._ioMultiIos.push({
1060
- io: this._localIo,
1061
- dump: true,
1062
- read: true,
1063
- write: true,
1064
- priority: 1
1065
- });
1066
- const ioPeerBridge = new IoPeerBridge(this._localIo, sockets.ioUp);
1067
- ioPeerBridge.start();
1068
- const ioPeer = await this._createIoPeer(sockets.ioDown);
1069
- this._ioMultiIos.push({
1070
- io: ioPeer,
1071
- dump: false,
1072
- read: true,
1073
- write: false,
1074
- priority: 2
1075
- });
1076
- this._ioMulti = new IoMulti(this._ioMultiIos);
1077
- await this._ioMulti.init();
1078
- await this._ioMulti.isReady();
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
- const sockets = normalizeSocketBundle(this._socketToServer);
1085
- this._bsMultiBss.push({
1086
- bs: this._localBs,
1087
- read: true,
1088
- write: true,
1089
- priority: 1
1090
- });
1091
- const bsPeerBridge = new BsPeerBridge(this._localBs, sockets.bsUp);
1092
- bsPeerBridge.start();
1093
- const bsPeer = await this._createBsPeer(sockets.bsDown);
1094
- this._bsMultiBss.push({
1095
- bs: bsPeer,
1096
- read: true,
1097
- write: false,
1098
- priority: 2
1099
- });
1100
- this._bsMulti = new BsMulti(this._bsMultiBss);
1101
- await this._bsMulti.init();
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
- const ioPeer = new IoPeer(socket);
1109
- await ioPeer.init();
1110
- await ioPeer.isReady();
1111
- return ioPeer;
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
- const bsPeer = new BsPeer(socket);
1119
- await bsPeer.init();
1120
- return bsPeer;
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;
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
+ }
1129
1425
  const ioMultiIoLocal = {
1130
1426
  io: this._localIo,
1131
1427
  dump: true,
@@ -1158,18 +1454,42 @@ class Server extends BaseNode {
1158
1454
  _bsMulti;
1159
1455
  // Storage => Let Clients read from Servers Bs
1160
1456
  _bsServer;
1161
- // To avoid rebroadcasting the same edit refs multiple times
1162
- _multicastedRefs = /* @__PURE__ */ new Set();
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;
1163
1462
  _refreshPromise;
1164
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;
1165
1478
  /**
1166
1479
  * Initializes Io and Bs multis on the server.
1167
1480
  */
1168
1481
  async init() {
1169
- await this._ioMulti.init();
1170
- await this._ioMulti.isReady();
1171
- await this._bsMulti.init();
1172
- await this.ready();
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
+ }
1173
1493
  }
1174
1494
  /**
1175
1495
  * Resolves once the Io implementation is ready.
@@ -1185,6 +1505,7 @@ class Server extends BaseNode {
1185
1505
  async addSocket(socket) {
1186
1506
  const sockets = normalizeSocketBundle(socket);
1187
1507
  const clientId = `client_${this._clients.size}_${Math.random().toString(36).slice(2)}`;
1508
+ this._logger.info("Server", "Adding client socket", { clientId });
1188
1509
  const ioUp = sockets.ioUp;
1189
1510
  const ioDown = sockets.ioDown;
1190
1511
  const bsUp = sockets.bsUp;
@@ -1193,20 +1514,34 @@ class Server extends BaseNode {
1193
1514
  ioDown.__clientId = clientId;
1194
1515
  bsUp.__clientId = clientId;
1195
1516
  bsDown.__clientId = clientId;
1196
- const ioPeer = await this._createIoPeer(ioUp);
1197
- const bsPeer = await this._createBsPeer(bsUp);
1198
- this._registerClient(
1199
- clientId,
1200
- { ioUp, ioDown, bsUp, bsDown },
1201
- ioPeer,
1202
- bsPeer
1203
- );
1204
- this._pendingSockets.push({ ioDown, bsDown });
1205
- this._queueIoPeer(ioPeer);
1206
- this._queueBsPeer(bsPeer);
1207
- await this._queueRefresh();
1208
- this._removeAllListeners();
1209
- this._multicastRefs();
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
+ }
1210
1545
  return this;
1211
1546
  }
1212
1547
  // ...........................................................................
@@ -1216,25 +1551,50 @@ class Server extends BaseNode {
1216
1551
  _removeAllListeners() {
1217
1552
  for (const { ioUp } of this._clients.values()) {
1218
1553
  ioUp.removeAllListeners(this._route.flat);
1554
+ ioUp.removeAllListeners(this._events.gapFillReq);
1555
+ ioUp.removeAllListeners(this._events.ackClient);
1219
1556
  }
1220
1557
  }
1221
1558
  // ...........................................................................
1222
1559
  /**
1223
1560
  * Broadcasts incoming payloads from any client to all other connected clients.
1224
- * Ensures the sender is filtered out when broadcasting.
1561
+ * Enriched with ref log, ACK aggregation, and gap-fill support when
1562
+ * syncConfig is provided.
1225
1563
  */
1226
1564
  _multicastRefs = () => {
1227
1565
  for (const [clientIdA, { ioUp: socketA }] of this._clients.entries()) {
1228
1566
  socketA.on(this._route.flat, (payload) => {
1229
1567
  const ref = payload.r;
1230
- if (this._multicastedRefs.has(ref)) {
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
+ });
1231
1577
  return;
1232
1578
  }
1233
- this._multicastedRefs.add(ref);
1579
+ this._multicastedRefsCurrent.add(ref);
1580
+ this._latestRef = ref;
1234
1581
  const p = payload;
1235
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
+ );
1236
1588
  return;
1237
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
+ }
1238
1598
  for (const [
1239
1599
  clientIdB,
1240
1600
  { ioDown: socketB }
@@ -1243,15 +1603,33 @@ class Server extends BaseNode {
1243
1603
  const forwarded = Object.assign({}, payload, {
1244
1604
  __origin: clientIdA
1245
1605
  });
1606
+ this._logger.traffic("out", "Server.Multicast", this._route.flat, {
1607
+ ref,
1608
+ from: clientIdA,
1609
+ to: clientIdB
1610
+ });
1246
1611
  socketB.emit(this._route.flat, forwarded);
1612
+ receiverCount++;
1247
1613
  }
1248
1614
  }
1615
+ if (ackCollector && receiverCount === 0) {
1616
+ ackCollector();
1617
+ }
1249
1618
  });
1619
+ if (this._syncConfig?.causalOrdering) {
1620
+ this._registerGapFillListener(clientIdA, socketA);
1621
+ }
1250
1622
  }
1251
1623
  };
1252
1624
  get route() {
1253
1625
  return this._route;
1254
1626
  }
1627
+ /**
1628
+ * Returns the logger instance.
1629
+ */
1630
+ get logger() {
1631
+ return this._logger;
1632
+ }
1255
1633
  /**
1256
1634
  * Returns the Io implementation.
1257
1635
  */
@@ -1270,24 +1648,215 @@ class Server extends BaseNode {
1270
1648
  get clients() {
1271
1649
  return this._clients;
1272
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
+ }
1273
1820
  /**
1274
1821
  * Creates and initializes a downstream Io peer for a socket.
1275
1822
  * @param socket - Client socket to bind the peer to.
1823
+ * @param clientId - Client identifier for logging.
1276
1824
  */
1277
- async _createIoPeer(socket) {
1278
- const ioPeer = new IoPeer(socket);
1279
- await ioPeer.init();
1280
- await ioPeer.isReady();
1281
- return ioPeer;
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
+ }
1282
1840
  }
1283
1841
  /**
1284
1842
  * Creates and initializes a downstream Bs peer for a socket.
1285
1843
  * @param socket - Client socket to bind the peer to.
1844
+ * @param clientId - Client identifier for logging.
1286
1845
  */
1287
- async _createBsPeer(socket) {
1288
- const bsPeer = new BsPeer(socket);
1289
- await bsPeer.init();
1290
- return bsPeer;
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
+ }
1291
1860
  }
1292
1861
  /**
1293
1862
  * Registers the client socket and peers.
@@ -1335,38 +1904,171 @@ class Server extends BaseNode {
1335
1904
  * Rebuilds Io and Bs multis from queued peers.
1336
1905
  */
1337
1906
  async _rebuildMultis() {
1338
- this._ioMulti = new IoMulti(this._ios);
1339
- await this._ioMulti.init();
1340
- await this._ioMulti.isReady();
1341
- this._bsMulti = new BsMulti(this._bss);
1342
- await this._bsMulti.init();
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
+ }
1343
1922
  }
1344
1923
  /**
1345
1924
  * Recreates servers and reattaches sockets.
1346
1925
  */
1347
1926
  async _refreshServers() {
1348
- this._ioServer._io = this._ioMulti;
1349
- this._bsServer._bs = this._bsMulti;
1350
- for (const pending of this._pendingSockets) {
1351
- await this._ioServer.addSocket(pending.ioDown);
1352
- await this._bsServer.addSocket(pending.bsDown);
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;
1353
1942
  }
1354
- this._pendingSockets = [];
1355
1943
  }
1356
1944
  /**
1357
1945
  * Batches multi/server refreshes into a single queued task.
1358
1946
  */
1359
1947
  _queueRefresh() {
1360
1948
  if (!this._refreshPromise) {
1949
+ this._logger.info("Server", "Queuing refresh");
1361
1950
  this._refreshPromise = Promise.resolve().then(async () => {
1362
1951
  await this._rebuildMultis();
1363
1952
  await this._refreshServers();
1364
- }).finally(() => {
1953
+ }).catch(
1954
+ /* v8 ignore next -- @preserve */
1955
+ (error) => {
1956
+ this._logger.error("Server", "Queued refresh failed", error);
1957
+ throw error;
1958
+ }
1959
+ ).finally(() => {
1365
1960
  this._refreshPromise = void 0;
1366
1961
  });
1367
1962
  }
1368
1963
  return this._refreshPromise;
1369
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
+ }
1370
2072
  /** Example instance for test purposes */
1371
2073
  static async example() {
1372
2074
  const route = Route.fromFlat("example.route");
@@ -1433,7 +2135,13 @@ class SocketIoBridge {
1433
2135
  }
1434
2136
  }
1435
2137
  export {
2138
+ BufferedLogger,
1436
2139
  Client,
2140
+ ConsoleLogger,
2141
+ FilteredLogger,
2142
+ NoopLogger,
1437
2143
  Server,
1438
- SocketIoBridge
2144
+ SocketIoBridge,
2145
+ noopLogger,
2146
+ syncEvents2 as syncEvents
1439
2147
  };