@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 +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +261 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +261 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -1209,6 +1209,8 @@ var SystemManager = class {
|
|
|
1209
1209
|
// src/ServerCoordinator.ts
|
|
1210
1210
|
var GC_INTERVAL_MS = 60 * 60 * 1e3;
|
|
1211
1211
|
var GC_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
1212
|
+
var CLIENT_HEARTBEAT_TIMEOUT_MS = 2e4;
|
|
1213
|
+
var CLIENT_HEARTBEAT_CHECK_INTERVAL_MS = 5e3;
|
|
1212
1214
|
var ServerCoordinator = class {
|
|
1213
1215
|
constructor(config) {
|
|
1214
1216
|
this.clients = /* @__PURE__ */ new Map();
|
|
@@ -1329,6 +1331,7 @@ var ServerCoordinator = class {
|
|
|
1329
1331
|
});
|
|
1330
1332
|
}
|
|
1331
1333
|
this.startGarbageCollection();
|
|
1334
|
+
this.startHeartbeatCheck();
|
|
1332
1335
|
}
|
|
1333
1336
|
/** Wait for server to be fully ready (ports assigned) */
|
|
1334
1337
|
ready() {
|
|
@@ -1379,6 +1382,10 @@ var ServerCoordinator = class {
|
|
|
1379
1382
|
clearInterval(this.gcInterval);
|
|
1380
1383
|
this.gcInterval = void 0;
|
|
1381
1384
|
}
|
|
1385
|
+
if (this.heartbeatCheckInterval) {
|
|
1386
|
+
clearInterval(this.heartbeatCheckInterval);
|
|
1387
|
+
this.heartbeatCheckInterval = void 0;
|
|
1388
|
+
}
|
|
1382
1389
|
if (this.lockManager) {
|
|
1383
1390
|
this.lockManager.stop();
|
|
1384
1391
|
}
|
|
@@ -1395,8 +1402,10 @@ var ServerCoordinator = class {
|
|
|
1395
1402
|
socket: ws,
|
|
1396
1403
|
isAuthenticated: false,
|
|
1397
1404
|
subscriptions: /* @__PURE__ */ new Set(),
|
|
1398
|
-
lastActiveHlc: this.hlc.now()
|
|
1405
|
+
lastActiveHlc: this.hlc.now(),
|
|
1399
1406
|
// Initialize with current time
|
|
1407
|
+
lastPingReceived: Date.now()
|
|
1408
|
+
// Initialize heartbeat tracking
|
|
1400
1409
|
};
|
|
1401
1410
|
this.clients.set(clientId, connection);
|
|
1402
1411
|
this.metricsService.setConnectedClients(this.clients.size);
|
|
@@ -1492,6 +1501,10 @@ var ServerCoordinator = class {
|
|
|
1492
1501
|
return;
|
|
1493
1502
|
}
|
|
1494
1503
|
const message = parseResult.data;
|
|
1504
|
+
if (message.type === "PING") {
|
|
1505
|
+
this.handlePing(client, message.timestamp);
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1495
1508
|
this.updateClientHlc(client, message);
|
|
1496
1509
|
if (!client.isAuthenticated) {
|
|
1497
1510
|
if (message.type === "AUTH") {
|
|
@@ -1858,6 +1871,184 @@ var ServerCoordinator = class {
|
|
|
1858
1871
|
}
|
|
1859
1872
|
break;
|
|
1860
1873
|
}
|
|
1874
|
+
// ============ ORMap Sync Message Handlers ============
|
|
1875
|
+
case "ORMAP_SYNC_INIT": {
|
|
1876
|
+
if (!this.securityManager.checkPermission(client.principal, message.mapName, "READ")) {
|
|
1877
|
+
client.socket.send(serialize2({
|
|
1878
|
+
type: "ERROR",
|
|
1879
|
+
payload: { code: 403, message: `Access Denied for map ${message.mapName}` }
|
|
1880
|
+
}));
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
const lastSync = message.lastSyncTimestamp || 0;
|
|
1884
|
+
const now = Date.now();
|
|
1885
|
+
if (lastSync > 0 && now - lastSync > GC_AGE_MS) {
|
|
1886
|
+
logger.warn({ clientId: client.id, lastSync, age: now - lastSync }, "ORMap client too old, sending SYNC_RESET_REQUIRED");
|
|
1887
|
+
client.socket.send(serialize2({
|
|
1888
|
+
type: "SYNC_RESET_REQUIRED",
|
|
1889
|
+
payload: { mapName: message.mapName }
|
|
1890
|
+
}));
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
logger.info({ clientId: client.id, mapName: message.mapName }, "Client requested ORMap sync");
|
|
1894
|
+
this.metricsService.incOp("GET", message.mapName);
|
|
1895
|
+
try {
|
|
1896
|
+
const mapForSync = await this.getMapAsync(message.mapName, "OR");
|
|
1897
|
+
if (mapForSync instanceof ORMap2) {
|
|
1898
|
+
const tree = mapForSync.getMerkleTree();
|
|
1899
|
+
const rootHash = tree.getRootHash();
|
|
1900
|
+
client.socket.send(serialize2({
|
|
1901
|
+
type: "ORMAP_SYNC_RESP_ROOT",
|
|
1902
|
+
payload: {
|
|
1903
|
+
mapName: message.mapName,
|
|
1904
|
+
rootHash,
|
|
1905
|
+
timestamp: this.hlc.now()
|
|
1906
|
+
}
|
|
1907
|
+
}));
|
|
1908
|
+
} else {
|
|
1909
|
+
client.socket.send(serialize2({
|
|
1910
|
+
type: "ERROR",
|
|
1911
|
+
payload: { code: 400, message: `Map ${message.mapName} is not an ORMap` }
|
|
1912
|
+
}));
|
|
1913
|
+
}
|
|
1914
|
+
} catch (err) {
|
|
1915
|
+
logger.error({ err, mapName: message.mapName }, "Failed to load map for ORMAP_SYNC_INIT");
|
|
1916
|
+
client.socket.send(serialize2({
|
|
1917
|
+
type: "ERROR",
|
|
1918
|
+
payload: { code: 500, message: `Failed to load map ${message.mapName}` }
|
|
1919
|
+
}));
|
|
1920
|
+
}
|
|
1921
|
+
break;
|
|
1922
|
+
}
|
|
1923
|
+
case "ORMAP_MERKLE_REQ_BUCKET": {
|
|
1924
|
+
if (!this.securityManager.checkPermission(client.principal, message.payload.mapName, "READ")) {
|
|
1925
|
+
client.socket.send(serialize2({
|
|
1926
|
+
type: "ERROR",
|
|
1927
|
+
payload: { code: 403, message: `Access Denied for map ${message.payload.mapName}` }
|
|
1928
|
+
}));
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
const { mapName, path } = message.payload;
|
|
1932
|
+
try {
|
|
1933
|
+
const mapForBucket = await this.getMapAsync(mapName, "OR");
|
|
1934
|
+
if (mapForBucket instanceof ORMap2) {
|
|
1935
|
+
const tree = mapForBucket.getMerkleTree();
|
|
1936
|
+
const buckets = tree.getBuckets(path);
|
|
1937
|
+
const isLeaf = tree.isLeaf(path);
|
|
1938
|
+
if (isLeaf) {
|
|
1939
|
+
const keys = tree.getKeysInBucket(path);
|
|
1940
|
+
const entries = [];
|
|
1941
|
+
for (const key of keys) {
|
|
1942
|
+
const recordsMap = mapForBucket.getRecordsMap(key);
|
|
1943
|
+
if (recordsMap && recordsMap.size > 0) {
|
|
1944
|
+
entries.push({
|
|
1945
|
+
key,
|
|
1946
|
+
records: Array.from(recordsMap.values()),
|
|
1947
|
+
tombstones: mapForBucket.getTombstones()
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
client.socket.send(serialize2({
|
|
1952
|
+
type: "ORMAP_SYNC_RESP_LEAF",
|
|
1953
|
+
payload: { mapName, path, entries }
|
|
1954
|
+
}));
|
|
1955
|
+
} else {
|
|
1956
|
+
client.socket.send(serialize2({
|
|
1957
|
+
type: "ORMAP_SYNC_RESP_BUCKETS",
|
|
1958
|
+
payload: { mapName, path, buckets }
|
|
1959
|
+
}));
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
} catch (err) {
|
|
1963
|
+
logger.error({ err, mapName }, "Failed to load map for ORMAP_MERKLE_REQ_BUCKET");
|
|
1964
|
+
}
|
|
1965
|
+
break;
|
|
1966
|
+
}
|
|
1967
|
+
case "ORMAP_DIFF_REQUEST": {
|
|
1968
|
+
if (!this.securityManager.checkPermission(client.principal, message.payload.mapName, "READ")) {
|
|
1969
|
+
client.socket.send(serialize2({
|
|
1970
|
+
type: "ERROR",
|
|
1971
|
+
payload: { code: 403, message: `Access Denied for map ${message.payload.mapName}` }
|
|
1972
|
+
}));
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
const { mapName: diffMapName, keys } = message.payload;
|
|
1976
|
+
try {
|
|
1977
|
+
const mapForDiff = await this.getMapAsync(diffMapName, "OR");
|
|
1978
|
+
if (mapForDiff instanceof ORMap2) {
|
|
1979
|
+
const entries = [];
|
|
1980
|
+
const allTombstones = mapForDiff.getTombstones();
|
|
1981
|
+
for (const key of keys) {
|
|
1982
|
+
const recordsMap = mapForDiff.getRecordsMap(key);
|
|
1983
|
+
entries.push({
|
|
1984
|
+
key,
|
|
1985
|
+
records: recordsMap ? Array.from(recordsMap.values()) : [],
|
|
1986
|
+
tombstones: allTombstones
|
|
1987
|
+
});
|
|
1988
|
+
}
|
|
1989
|
+
client.socket.send(serialize2({
|
|
1990
|
+
type: "ORMAP_DIFF_RESPONSE",
|
|
1991
|
+
payload: { mapName: diffMapName, entries }
|
|
1992
|
+
}));
|
|
1993
|
+
}
|
|
1994
|
+
} catch (err) {
|
|
1995
|
+
logger.error({ err, mapName: diffMapName }, "Failed to load map for ORMAP_DIFF_REQUEST");
|
|
1996
|
+
}
|
|
1997
|
+
break;
|
|
1998
|
+
}
|
|
1999
|
+
case "ORMAP_PUSH_DIFF": {
|
|
2000
|
+
if (!this.securityManager.checkPermission(client.principal, message.payload.mapName, "PUT")) {
|
|
2001
|
+
client.socket.send(serialize2({
|
|
2002
|
+
type: "ERROR",
|
|
2003
|
+
payload: { code: 403, message: `Access Denied for map ${message.payload.mapName}` }
|
|
2004
|
+
}));
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
const { mapName: pushMapName, entries: pushEntries } = message.payload;
|
|
2008
|
+
try {
|
|
2009
|
+
const mapForPush = await this.getMapAsync(pushMapName, "OR");
|
|
2010
|
+
if (mapForPush instanceof ORMap2) {
|
|
2011
|
+
let totalAdded = 0;
|
|
2012
|
+
let totalUpdated = 0;
|
|
2013
|
+
for (const entry of pushEntries) {
|
|
2014
|
+
const { key, records, tombstones } = entry;
|
|
2015
|
+
const result = mapForPush.mergeKey(key, records, tombstones);
|
|
2016
|
+
totalAdded += result.added;
|
|
2017
|
+
totalUpdated += result.updated;
|
|
2018
|
+
}
|
|
2019
|
+
if (totalAdded > 0 || totalUpdated > 0) {
|
|
2020
|
+
logger.info({ mapName: pushMapName, added: totalAdded, updated: totalUpdated, clientId: client.id }, "Merged ORMap diff from client");
|
|
2021
|
+
for (const entry of pushEntries) {
|
|
2022
|
+
for (const record of entry.records) {
|
|
2023
|
+
this.broadcast({
|
|
2024
|
+
type: "SERVER_EVENT",
|
|
2025
|
+
payload: {
|
|
2026
|
+
mapName: pushMapName,
|
|
2027
|
+
eventType: "OR_ADD",
|
|
2028
|
+
key: entry.key,
|
|
2029
|
+
orRecord: record
|
|
2030
|
+
}
|
|
2031
|
+
}, client.id);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
if (this.storage) {
|
|
2035
|
+
for (const entry of pushEntries) {
|
|
2036
|
+
const recordsMap = mapForPush.getRecordsMap(entry.key);
|
|
2037
|
+
if (recordsMap && recordsMap.size > 0) {
|
|
2038
|
+
await this.storage.store(pushMapName, entry.key, {
|
|
2039
|
+
type: "OR",
|
|
2040
|
+
records: Array.from(recordsMap.values())
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
} catch (err) {
|
|
2048
|
+
logger.error({ err, mapName: pushMapName }, "Failed to process ORMAP_PUSH_DIFF");
|
|
2049
|
+
}
|
|
2050
|
+
break;
|
|
2051
|
+
}
|
|
1861
2052
|
default:
|
|
1862
2053
|
logger.warn({ type: message.type }, "Unknown message type");
|
|
1863
2054
|
}
|
|
@@ -2380,6 +2571,75 @@ var ServerCoordinator = class {
|
|
|
2380
2571
|
this.reportLocalHlc();
|
|
2381
2572
|
}, GC_INTERVAL_MS);
|
|
2382
2573
|
}
|
|
2574
|
+
// ============ Heartbeat Methods ============
|
|
2575
|
+
/**
|
|
2576
|
+
* Starts the periodic check for dead clients (those that haven't sent PING).
|
|
2577
|
+
*/
|
|
2578
|
+
startHeartbeatCheck() {
|
|
2579
|
+
this.heartbeatCheckInterval = setInterval(() => {
|
|
2580
|
+
this.evictDeadClients();
|
|
2581
|
+
}, CLIENT_HEARTBEAT_CHECK_INTERVAL_MS);
|
|
2582
|
+
}
|
|
2583
|
+
/**
|
|
2584
|
+
* Handles incoming PING message from client.
|
|
2585
|
+
* Responds with PONG immediately.
|
|
2586
|
+
*/
|
|
2587
|
+
handlePing(client, clientTimestamp) {
|
|
2588
|
+
client.lastPingReceived = Date.now();
|
|
2589
|
+
const pongMessage = {
|
|
2590
|
+
type: "PONG",
|
|
2591
|
+
timestamp: clientTimestamp,
|
|
2592
|
+
serverTime: Date.now()
|
|
2593
|
+
};
|
|
2594
|
+
if (client.socket.readyState === WebSocket2.OPEN) {
|
|
2595
|
+
client.socket.send(serialize2(pongMessage));
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
/**
|
|
2599
|
+
* Checks if a client is still alive based on heartbeat.
|
|
2600
|
+
*/
|
|
2601
|
+
isClientAlive(clientId) {
|
|
2602
|
+
const client = this.clients.get(clientId);
|
|
2603
|
+
if (!client) return false;
|
|
2604
|
+
const idleTime = Date.now() - client.lastPingReceived;
|
|
2605
|
+
return idleTime < CLIENT_HEARTBEAT_TIMEOUT_MS;
|
|
2606
|
+
}
|
|
2607
|
+
/**
|
|
2608
|
+
* Returns how long the client has been idle (no PING received).
|
|
2609
|
+
*/
|
|
2610
|
+
getClientIdleTime(clientId) {
|
|
2611
|
+
const client = this.clients.get(clientId);
|
|
2612
|
+
if (!client) return Infinity;
|
|
2613
|
+
return Date.now() - client.lastPingReceived;
|
|
2614
|
+
}
|
|
2615
|
+
/**
|
|
2616
|
+
* Evicts clients that haven't sent a PING within the timeout period.
|
|
2617
|
+
*/
|
|
2618
|
+
evictDeadClients() {
|
|
2619
|
+
const now = Date.now();
|
|
2620
|
+
const deadClients = [];
|
|
2621
|
+
for (const [clientId, client] of this.clients) {
|
|
2622
|
+
if (client.isAuthenticated) {
|
|
2623
|
+
const idleTime = now - client.lastPingReceived;
|
|
2624
|
+
if (idleTime > CLIENT_HEARTBEAT_TIMEOUT_MS) {
|
|
2625
|
+
deadClients.push(clientId);
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
for (const clientId of deadClients) {
|
|
2630
|
+
const client = this.clients.get(clientId);
|
|
2631
|
+
if (client) {
|
|
2632
|
+
logger.warn({
|
|
2633
|
+
clientId,
|
|
2634
|
+
idleTime: now - client.lastPingReceived,
|
|
2635
|
+
timeoutMs: CLIENT_HEARTBEAT_TIMEOUT_MS
|
|
2636
|
+
}, "Evicting dead client (heartbeat timeout)");
|
|
2637
|
+
if (client.socket.readyState === WebSocket2.OPEN) {
|
|
2638
|
+
client.socket.close(4002, "Heartbeat timeout");
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2383
2643
|
reportLocalHlc() {
|
|
2384
2644
|
let minHlc = this.hlc.now();
|
|
2385
2645
|
for (const client of this.clients.values()) {
|