@topgunbuild/server 0.1.0 → 0.2.0-alpha

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/index.d.mts CHANGED
@@ -199,6 +199,7 @@ declare class ServerCoordinator {
199
199
  private systemManager;
200
200
  private pendingClusterQueries;
201
201
  private gcInterval?;
202
+ private heartbeatCheckInterval?;
202
203
  private gcReports;
203
204
  private mapLoadingPromises;
204
205
  private _actualPort;
@@ -231,6 +232,27 @@ declare class ServerCoordinator {
231
232
  getMapAsync(name: string, typeHint?: 'LWW' | 'OR'): Promise<LWWMap<string, any> | ORMap<string, any>>;
232
233
  private loadMapFromStorage;
233
234
  private startGarbageCollection;
235
+ /**
236
+ * Starts the periodic check for dead clients (those that haven't sent PING).
237
+ */
238
+ private startHeartbeatCheck;
239
+ /**
240
+ * Handles incoming PING message from client.
241
+ * Responds with PONG immediately.
242
+ */
243
+ private handlePing;
244
+ /**
245
+ * Checks if a client is still alive based on heartbeat.
246
+ */
247
+ isClientAlive(clientId: string): boolean;
248
+ /**
249
+ * Returns how long the client has been idle (no PING received).
250
+ */
251
+ getClientIdleTime(clientId: string): number;
252
+ /**
253
+ * Evicts clients that haven't sent a PING within the timeout period.
254
+ */
255
+ private evictDeadClients;
234
256
  private reportLocalHlc;
235
257
  private handleGcReport;
236
258
  private performGarbageCollection;
package/dist/index.d.ts CHANGED
@@ -199,6 +199,7 @@ declare class ServerCoordinator {
199
199
  private systemManager;
200
200
  private pendingClusterQueries;
201
201
  private gcInterval?;
202
+ private heartbeatCheckInterval?;
202
203
  private gcReports;
203
204
  private mapLoadingPromises;
204
205
  private _actualPort;
@@ -231,6 +232,27 @@ declare class ServerCoordinator {
231
232
  getMapAsync(name: string, typeHint?: 'LWW' | 'OR'): Promise<LWWMap<string, any> | ORMap<string, any>>;
232
233
  private loadMapFromStorage;
233
234
  private startGarbageCollection;
235
+ /**
236
+ * Starts the periodic check for dead clients (those that haven't sent PING).
237
+ */
238
+ private startHeartbeatCheck;
239
+ /**
240
+ * Handles incoming PING message from client.
241
+ * Responds with PONG immediately.
242
+ */
243
+ private handlePing;
244
+ /**
245
+ * Checks if a client is still alive based on heartbeat.
246
+ */
247
+ isClientAlive(clientId: string): boolean;
248
+ /**
249
+ * Returns how long the client has been idle (no PING received).
250
+ */
251
+ getClientIdleTime(clientId: string): number;
252
+ /**
253
+ * Evicts clients that haven't sent a PING within the timeout period.
254
+ */
255
+ private evictDeadClients;
234
256
  private reportLocalHlc;
235
257
  private handleGcReport;
236
258
  private performGarbageCollection;
package/dist/index.js CHANGED
@@ -1251,6 +1251,8 @@ var SystemManager = class {
1251
1251
  // src/ServerCoordinator.ts
1252
1252
  var GC_INTERVAL_MS = 60 * 60 * 1e3;
1253
1253
  var GC_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
1254
+ var CLIENT_HEARTBEAT_TIMEOUT_MS = 2e4;
1255
+ var CLIENT_HEARTBEAT_CHECK_INTERVAL_MS = 5e3;
1254
1256
  var ServerCoordinator = class {
1255
1257
  constructor(config) {
1256
1258
  this.clients = /* @__PURE__ */ new Map();
@@ -1371,6 +1373,7 @@ var ServerCoordinator = class {
1371
1373
  });
1372
1374
  }
1373
1375
  this.startGarbageCollection();
1376
+ this.startHeartbeatCheck();
1374
1377
  }
1375
1378
  /** Wait for server to be fully ready (ports assigned) */
1376
1379
  ready() {
@@ -1421,6 +1424,10 @@ var ServerCoordinator = class {
1421
1424
  clearInterval(this.gcInterval);
1422
1425
  this.gcInterval = void 0;
1423
1426
  }
1427
+ if (this.heartbeatCheckInterval) {
1428
+ clearInterval(this.heartbeatCheckInterval);
1429
+ this.heartbeatCheckInterval = void 0;
1430
+ }
1424
1431
  if (this.lockManager) {
1425
1432
  this.lockManager.stop();
1426
1433
  }
@@ -1437,8 +1444,10 @@ var ServerCoordinator = class {
1437
1444
  socket: ws,
1438
1445
  isAuthenticated: false,
1439
1446
  subscriptions: /* @__PURE__ */ new Set(),
1440
- lastActiveHlc: this.hlc.now()
1447
+ lastActiveHlc: this.hlc.now(),
1441
1448
  // Initialize with current time
1449
+ lastPingReceived: Date.now()
1450
+ // Initialize heartbeat tracking
1442
1451
  };
1443
1452
  this.clients.set(clientId, connection);
1444
1453
  this.metricsService.setConnectedClients(this.clients.size);
@@ -1534,6 +1543,10 @@ var ServerCoordinator = class {
1534
1543
  return;
1535
1544
  }
1536
1545
  const message = parseResult.data;
1546
+ if (message.type === "PING") {
1547
+ this.handlePing(client, message.timestamp);
1548
+ return;
1549
+ }
1537
1550
  this.updateClientHlc(client, message);
1538
1551
  if (!client.isAuthenticated) {
1539
1552
  if (message.type === "AUTH") {
@@ -1900,6 +1913,184 @@ var ServerCoordinator = class {
1900
1913
  }
1901
1914
  break;
1902
1915
  }
1916
+ // ============ ORMap Sync Message Handlers ============
1917
+ case "ORMAP_SYNC_INIT": {
1918
+ if (!this.securityManager.checkPermission(client.principal, message.mapName, "READ")) {
1919
+ client.socket.send((0, import_core4.serialize)({
1920
+ type: "ERROR",
1921
+ payload: { code: 403, message: `Access Denied for map ${message.mapName}` }
1922
+ }));
1923
+ return;
1924
+ }
1925
+ const lastSync = message.lastSyncTimestamp || 0;
1926
+ const now = Date.now();
1927
+ if (lastSync > 0 && now - lastSync > GC_AGE_MS) {
1928
+ logger.warn({ clientId: client.id, lastSync, age: now - lastSync }, "ORMap client too old, sending SYNC_RESET_REQUIRED");
1929
+ client.socket.send((0, import_core4.serialize)({
1930
+ type: "SYNC_RESET_REQUIRED",
1931
+ payload: { mapName: message.mapName }
1932
+ }));
1933
+ return;
1934
+ }
1935
+ logger.info({ clientId: client.id, mapName: message.mapName }, "Client requested ORMap sync");
1936
+ this.metricsService.incOp("GET", message.mapName);
1937
+ try {
1938
+ const mapForSync = await this.getMapAsync(message.mapName, "OR");
1939
+ if (mapForSync instanceof import_core4.ORMap) {
1940
+ const tree = mapForSync.getMerkleTree();
1941
+ const rootHash = tree.getRootHash();
1942
+ client.socket.send((0, import_core4.serialize)({
1943
+ type: "ORMAP_SYNC_RESP_ROOT",
1944
+ payload: {
1945
+ mapName: message.mapName,
1946
+ rootHash,
1947
+ timestamp: this.hlc.now()
1948
+ }
1949
+ }));
1950
+ } else {
1951
+ client.socket.send((0, import_core4.serialize)({
1952
+ type: "ERROR",
1953
+ payload: { code: 400, message: `Map ${message.mapName} is not an ORMap` }
1954
+ }));
1955
+ }
1956
+ } catch (err) {
1957
+ logger.error({ err, mapName: message.mapName }, "Failed to load map for ORMAP_SYNC_INIT");
1958
+ client.socket.send((0, import_core4.serialize)({
1959
+ type: "ERROR",
1960
+ payload: { code: 500, message: `Failed to load map ${message.mapName}` }
1961
+ }));
1962
+ }
1963
+ break;
1964
+ }
1965
+ case "ORMAP_MERKLE_REQ_BUCKET": {
1966
+ if (!this.securityManager.checkPermission(client.principal, message.payload.mapName, "READ")) {
1967
+ client.socket.send((0, import_core4.serialize)({
1968
+ type: "ERROR",
1969
+ payload: { code: 403, message: `Access Denied for map ${message.payload.mapName}` }
1970
+ }));
1971
+ return;
1972
+ }
1973
+ const { mapName, path } = message.payload;
1974
+ try {
1975
+ const mapForBucket = await this.getMapAsync(mapName, "OR");
1976
+ if (mapForBucket instanceof import_core4.ORMap) {
1977
+ const tree = mapForBucket.getMerkleTree();
1978
+ const buckets = tree.getBuckets(path);
1979
+ const isLeaf = tree.isLeaf(path);
1980
+ if (isLeaf) {
1981
+ const keys = tree.getKeysInBucket(path);
1982
+ const entries = [];
1983
+ for (const key of keys) {
1984
+ const recordsMap = mapForBucket.getRecordsMap(key);
1985
+ if (recordsMap && recordsMap.size > 0) {
1986
+ entries.push({
1987
+ key,
1988
+ records: Array.from(recordsMap.values()),
1989
+ tombstones: mapForBucket.getTombstones()
1990
+ });
1991
+ }
1992
+ }
1993
+ client.socket.send((0, import_core4.serialize)({
1994
+ type: "ORMAP_SYNC_RESP_LEAF",
1995
+ payload: { mapName, path, entries }
1996
+ }));
1997
+ } else {
1998
+ client.socket.send((0, import_core4.serialize)({
1999
+ type: "ORMAP_SYNC_RESP_BUCKETS",
2000
+ payload: { mapName, path, buckets }
2001
+ }));
2002
+ }
2003
+ }
2004
+ } catch (err) {
2005
+ logger.error({ err, mapName }, "Failed to load map for ORMAP_MERKLE_REQ_BUCKET");
2006
+ }
2007
+ break;
2008
+ }
2009
+ case "ORMAP_DIFF_REQUEST": {
2010
+ if (!this.securityManager.checkPermission(client.principal, message.payload.mapName, "READ")) {
2011
+ client.socket.send((0, import_core4.serialize)({
2012
+ type: "ERROR",
2013
+ payload: { code: 403, message: `Access Denied for map ${message.payload.mapName}` }
2014
+ }));
2015
+ return;
2016
+ }
2017
+ const { mapName: diffMapName, keys } = message.payload;
2018
+ try {
2019
+ const mapForDiff = await this.getMapAsync(diffMapName, "OR");
2020
+ if (mapForDiff instanceof import_core4.ORMap) {
2021
+ const entries = [];
2022
+ const allTombstones = mapForDiff.getTombstones();
2023
+ for (const key of keys) {
2024
+ const recordsMap = mapForDiff.getRecordsMap(key);
2025
+ entries.push({
2026
+ key,
2027
+ records: recordsMap ? Array.from(recordsMap.values()) : [],
2028
+ tombstones: allTombstones
2029
+ });
2030
+ }
2031
+ client.socket.send((0, import_core4.serialize)({
2032
+ type: "ORMAP_DIFF_RESPONSE",
2033
+ payload: { mapName: diffMapName, entries }
2034
+ }));
2035
+ }
2036
+ } catch (err) {
2037
+ logger.error({ err, mapName: diffMapName }, "Failed to load map for ORMAP_DIFF_REQUEST");
2038
+ }
2039
+ break;
2040
+ }
2041
+ case "ORMAP_PUSH_DIFF": {
2042
+ if (!this.securityManager.checkPermission(client.principal, message.payload.mapName, "PUT")) {
2043
+ client.socket.send((0, import_core4.serialize)({
2044
+ type: "ERROR",
2045
+ payload: { code: 403, message: `Access Denied for map ${message.payload.mapName}` }
2046
+ }));
2047
+ return;
2048
+ }
2049
+ const { mapName: pushMapName, entries: pushEntries } = message.payload;
2050
+ try {
2051
+ const mapForPush = await this.getMapAsync(pushMapName, "OR");
2052
+ if (mapForPush instanceof import_core4.ORMap) {
2053
+ let totalAdded = 0;
2054
+ let totalUpdated = 0;
2055
+ for (const entry of pushEntries) {
2056
+ const { key, records, tombstones } = entry;
2057
+ const result = mapForPush.mergeKey(key, records, tombstones);
2058
+ totalAdded += result.added;
2059
+ totalUpdated += result.updated;
2060
+ }
2061
+ if (totalAdded > 0 || totalUpdated > 0) {
2062
+ logger.info({ mapName: pushMapName, added: totalAdded, updated: totalUpdated, clientId: client.id }, "Merged ORMap diff from client");
2063
+ for (const entry of pushEntries) {
2064
+ for (const record of entry.records) {
2065
+ this.broadcast({
2066
+ type: "SERVER_EVENT",
2067
+ payload: {
2068
+ mapName: pushMapName,
2069
+ eventType: "OR_ADD",
2070
+ key: entry.key,
2071
+ orRecord: record
2072
+ }
2073
+ }, client.id);
2074
+ }
2075
+ }
2076
+ if (this.storage) {
2077
+ for (const entry of pushEntries) {
2078
+ const recordsMap = mapForPush.getRecordsMap(entry.key);
2079
+ if (recordsMap && recordsMap.size > 0) {
2080
+ await this.storage.store(pushMapName, entry.key, {
2081
+ type: "OR",
2082
+ records: Array.from(recordsMap.values())
2083
+ });
2084
+ }
2085
+ }
2086
+ }
2087
+ }
2088
+ }
2089
+ } catch (err) {
2090
+ logger.error({ err, mapName: pushMapName }, "Failed to process ORMAP_PUSH_DIFF");
2091
+ }
2092
+ break;
2093
+ }
1903
2094
  default:
1904
2095
  logger.warn({ type: message.type }, "Unknown message type");
1905
2096
  }
@@ -2422,6 +2613,75 @@ var ServerCoordinator = class {
2422
2613
  this.reportLocalHlc();
2423
2614
  }, GC_INTERVAL_MS);
2424
2615
  }
2616
+ // ============ Heartbeat Methods ============
2617
+ /**
2618
+ * Starts the periodic check for dead clients (those that haven't sent PING).
2619
+ */
2620
+ startHeartbeatCheck() {
2621
+ this.heartbeatCheckInterval = setInterval(() => {
2622
+ this.evictDeadClients();
2623
+ }, CLIENT_HEARTBEAT_CHECK_INTERVAL_MS);
2624
+ }
2625
+ /**
2626
+ * Handles incoming PING message from client.
2627
+ * Responds with PONG immediately.
2628
+ */
2629
+ handlePing(client, clientTimestamp) {
2630
+ client.lastPingReceived = Date.now();
2631
+ const pongMessage = {
2632
+ type: "PONG",
2633
+ timestamp: clientTimestamp,
2634
+ serverTime: Date.now()
2635
+ };
2636
+ if (client.socket.readyState === import_ws2.WebSocket.OPEN) {
2637
+ client.socket.send((0, import_core4.serialize)(pongMessage));
2638
+ }
2639
+ }
2640
+ /**
2641
+ * Checks if a client is still alive based on heartbeat.
2642
+ */
2643
+ isClientAlive(clientId) {
2644
+ const client = this.clients.get(clientId);
2645
+ if (!client) return false;
2646
+ const idleTime = Date.now() - client.lastPingReceived;
2647
+ return idleTime < CLIENT_HEARTBEAT_TIMEOUT_MS;
2648
+ }
2649
+ /**
2650
+ * Returns how long the client has been idle (no PING received).
2651
+ */
2652
+ getClientIdleTime(clientId) {
2653
+ const client = this.clients.get(clientId);
2654
+ if (!client) return Infinity;
2655
+ return Date.now() - client.lastPingReceived;
2656
+ }
2657
+ /**
2658
+ * Evicts clients that haven't sent a PING within the timeout period.
2659
+ */
2660
+ evictDeadClients() {
2661
+ const now = Date.now();
2662
+ const deadClients = [];
2663
+ for (const [clientId, client] of this.clients) {
2664
+ if (client.isAuthenticated) {
2665
+ const idleTime = now - client.lastPingReceived;
2666
+ if (idleTime > CLIENT_HEARTBEAT_TIMEOUT_MS) {
2667
+ deadClients.push(clientId);
2668
+ }
2669
+ }
2670
+ }
2671
+ for (const clientId of deadClients) {
2672
+ const client = this.clients.get(clientId);
2673
+ if (client) {
2674
+ logger.warn({
2675
+ clientId,
2676
+ idleTime: now - client.lastPingReceived,
2677
+ timeoutMs: CLIENT_HEARTBEAT_TIMEOUT_MS
2678
+ }, "Evicting dead client (heartbeat timeout)");
2679
+ if (client.socket.readyState === import_ws2.WebSocket.OPEN) {
2680
+ client.socket.close(4002, "Heartbeat timeout");
2681
+ }
2682
+ }
2683
+ }
2684
+ }
2425
2685
  reportLocalHlc() {
2426
2686
  let minHlc = this.hlc.now();
2427
2687
  for (const client of this.clients.values()) {