@topgunbuild/server 0.10.1 → 0.11.0
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/{chunk-IQNKZPW3.mjs → chunk-Z3TONATT.mjs} +346 -10
- package/dist/chunk-Z3TONATT.mjs.map +1 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +379 -43
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/start-server.js +409 -73
- package/dist/start-server.js.map +1 -1
- package/dist/start-server.mjs +1 -1
- package/package.json +3 -3
- package/dist/chunk-IQNKZPW3.mjs.map +0 -1
|
@@ -24370,17 +24370,18 @@ function buildTLSOptions(config2) {
|
|
|
24370
24370
|
return options;
|
|
24371
24371
|
}
|
|
24372
24372
|
function createNetworkModule(config2, _deps) {
|
|
24373
|
+
let currentRequestHandler = (_req, res) => {
|
|
24374
|
+
res.writeHead(200);
|
|
24375
|
+
res.end(config2.tls?.enabled ? "TopGun Server Running (Secure)" : "TopGun Server Running");
|
|
24376
|
+
};
|
|
24377
|
+
const requestDispatcher = (req, res) => {
|
|
24378
|
+
currentRequestHandler(req, res);
|
|
24379
|
+
};
|
|
24373
24380
|
let httpServer;
|
|
24374
24381
|
if (config2.tls?.enabled) {
|
|
24375
|
-
httpServer = createHttpsServer(buildTLSOptions(config2.tls),
|
|
24376
|
-
res.writeHead(200);
|
|
24377
|
-
res.end("TopGun Server Running (Secure)");
|
|
24378
|
-
});
|
|
24382
|
+
httpServer = createHttpsServer(buildTLSOptions(config2.tls), requestDispatcher);
|
|
24379
24383
|
} else {
|
|
24380
|
-
httpServer = createHttpServer(
|
|
24381
|
-
res.writeHead(200);
|
|
24382
|
-
res.end("TopGun Server Running");
|
|
24383
|
-
});
|
|
24384
|
+
httpServer = createHttpServer(requestDispatcher);
|
|
24384
24385
|
}
|
|
24385
24386
|
httpServer.maxConnections = config2.maxConnections ?? 1e4;
|
|
24386
24387
|
httpServer.timeout = config2.serverTimeout ?? 12e4;
|
|
@@ -24418,6 +24419,11 @@ function createNetworkModule(config2, _deps) {
|
|
|
24418
24419
|
httpServer.listen(config2.port, () => {
|
|
24419
24420
|
logger.info({ port: config2.port }, "Server Coordinator listening");
|
|
24420
24421
|
});
|
|
24422
|
+
},
|
|
24423
|
+
// Deferred wiring: allows ServerFactory to inject the /sync handler
|
|
24424
|
+
// after HttpSyncHandler is assembled
|
|
24425
|
+
setHttpRequestHandler: (handler) => {
|
|
24426
|
+
currentRequestHandler = handler;
|
|
24421
24427
|
}
|
|
24422
24428
|
};
|
|
24423
24429
|
}
|
|
@@ -27480,6 +27486,253 @@ var QueryHandler = class {
|
|
|
27480
27486
|
}
|
|
27481
27487
|
};
|
|
27482
27488
|
|
|
27489
|
+
// src/coordinator/http-sync-handler.ts
|
|
27490
|
+
import { HLC as HLC8, LWWMap as LWWMap10 } from "@topgunbuild/core";
|
|
27491
|
+
var HttpSyncHandler = class {
|
|
27492
|
+
constructor(config2) {
|
|
27493
|
+
this.config = config2;
|
|
27494
|
+
}
|
|
27495
|
+
/**
|
|
27496
|
+
* Process a complete HTTP sync request and return a response.
|
|
27497
|
+
*
|
|
27498
|
+
* @param request - Parsed and validated HttpSyncRequest body
|
|
27499
|
+
* @param authToken - JWT token from Authorization header
|
|
27500
|
+
* @returns HttpSyncResponse with acks, deltas, query/search results
|
|
27501
|
+
* @throws Error with message starting with '401:' for auth failures
|
|
27502
|
+
* @throws Error with message starting with '403:' for permission failures
|
|
27503
|
+
*/
|
|
27504
|
+
async handleSyncRequest(request, authToken) {
|
|
27505
|
+
let principal;
|
|
27506
|
+
try {
|
|
27507
|
+
principal = this.config.authHandler.verifyToken(authToken);
|
|
27508
|
+
} catch (err) {
|
|
27509
|
+
throw new Error(`401: Authentication failed: ${err.message}`);
|
|
27510
|
+
}
|
|
27511
|
+
this.config.hlc.update({
|
|
27512
|
+
millis: request.clientHlc.millis,
|
|
27513
|
+
counter: request.clientHlc.counter,
|
|
27514
|
+
nodeId: request.clientHlc.nodeId
|
|
27515
|
+
});
|
|
27516
|
+
const response = {
|
|
27517
|
+
serverHlc: this.config.hlc.now()
|
|
27518
|
+
};
|
|
27519
|
+
const errors = [];
|
|
27520
|
+
if (request.operations && request.operations.length > 0) {
|
|
27521
|
+
const ackResults = await this.processOperations(request.operations, principal, errors);
|
|
27522
|
+
if (ackResults.processedCount > 0) {
|
|
27523
|
+
response.ack = {
|
|
27524
|
+
lastId: ackResults.lastId,
|
|
27525
|
+
results: ackResults.results.length > 0 ? ackResults.results : void 0
|
|
27526
|
+
};
|
|
27527
|
+
}
|
|
27528
|
+
}
|
|
27529
|
+
if (request.syncMaps && request.syncMaps.length > 0) {
|
|
27530
|
+
response.deltas = await this.computeDeltas(request.syncMaps, principal, errors);
|
|
27531
|
+
}
|
|
27532
|
+
if (request.queries && request.queries.length > 0) {
|
|
27533
|
+
response.queryResults = await this.executeQueries(request.queries, principal, errors);
|
|
27534
|
+
}
|
|
27535
|
+
if (request.searches && request.searches.length > 0) {
|
|
27536
|
+
response.searchResults = await this.executeSearches(request.searches, principal, errors);
|
|
27537
|
+
}
|
|
27538
|
+
if (errors.length > 0) {
|
|
27539
|
+
response.errors = errors;
|
|
27540
|
+
}
|
|
27541
|
+
response.serverHlc = this.config.hlc.now();
|
|
27542
|
+
return response;
|
|
27543
|
+
}
|
|
27544
|
+
/**
|
|
27545
|
+
* Process a batch of client operations.
|
|
27546
|
+
*/
|
|
27547
|
+
async processOperations(operations, principal, errors) {
|
|
27548
|
+
let lastId = "";
|
|
27549
|
+
let processedCount = 0;
|
|
27550
|
+
const results = [];
|
|
27551
|
+
for (const op of operations) {
|
|
27552
|
+
const opId = op.id || `http-op-${processedCount}`;
|
|
27553
|
+
const isRemove = op.opType === "REMOVE" || op.record && op.record.value === null;
|
|
27554
|
+
const action = isRemove ? "REMOVE" : "PUT";
|
|
27555
|
+
if (!this.config.securityManager.checkPermission(principal, op.mapName, action)) {
|
|
27556
|
+
errors.push({
|
|
27557
|
+
code: 403,
|
|
27558
|
+
message: "Access denied",
|
|
27559
|
+
context: `Operation on ${op.mapName}/${op.key}`
|
|
27560
|
+
});
|
|
27561
|
+
results.push({
|
|
27562
|
+
opId,
|
|
27563
|
+
success: false,
|
|
27564
|
+
achievedLevel: "FIRE_AND_FORGET",
|
|
27565
|
+
error: "Access denied"
|
|
27566
|
+
});
|
|
27567
|
+
continue;
|
|
27568
|
+
}
|
|
27569
|
+
try {
|
|
27570
|
+
const result = await this.config.operationHandler.applyOpToMap(op);
|
|
27571
|
+
if (result.rejected) {
|
|
27572
|
+
results.push({
|
|
27573
|
+
opId,
|
|
27574
|
+
success: false,
|
|
27575
|
+
achievedLevel: "FIRE_AND_FORGET",
|
|
27576
|
+
error: "Operation rejected by conflict resolver"
|
|
27577
|
+
});
|
|
27578
|
+
} else {
|
|
27579
|
+
results.push({
|
|
27580
|
+
opId,
|
|
27581
|
+
success: true,
|
|
27582
|
+
achievedLevel: "MEMORY"
|
|
27583
|
+
});
|
|
27584
|
+
}
|
|
27585
|
+
processedCount++;
|
|
27586
|
+
lastId = opId;
|
|
27587
|
+
} catch (err) {
|
|
27588
|
+
logger.error({ err, opId }, "HTTP sync operation failed");
|
|
27589
|
+
errors.push({
|
|
27590
|
+
code: 500,
|
|
27591
|
+
message: err.message || "Operation failed",
|
|
27592
|
+
context: `Operation ${opId} on ${op.mapName}/${op.key}`
|
|
27593
|
+
});
|
|
27594
|
+
results.push({
|
|
27595
|
+
opId,
|
|
27596
|
+
success: false,
|
|
27597
|
+
achievedLevel: "FIRE_AND_FORGET",
|
|
27598
|
+
error: err.message || "Operation failed"
|
|
27599
|
+
});
|
|
27600
|
+
}
|
|
27601
|
+
}
|
|
27602
|
+
return { processedCount, lastId, results };
|
|
27603
|
+
}
|
|
27604
|
+
/**
|
|
27605
|
+
* Compute deltas by iterating the in-memory LWWMap and filtering records
|
|
27606
|
+
* newer than the client's lastSyncTimestamp using HLC.compare().
|
|
27607
|
+
*/
|
|
27608
|
+
async computeDeltas(syncMaps, principal, errors) {
|
|
27609
|
+
const deltas = [];
|
|
27610
|
+
for (const { mapName, lastSyncTimestamp } of syncMaps) {
|
|
27611
|
+
if (!this.config.securityManager.checkPermission(principal, mapName, "READ")) {
|
|
27612
|
+
errors.push({
|
|
27613
|
+
code: 403,
|
|
27614
|
+
message: "Access denied",
|
|
27615
|
+
context: `Read deltas for ${mapName}`
|
|
27616
|
+
});
|
|
27617
|
+
continue;
|
|
27618
|
+
}
|
|
27619
|
+
try {
|
|
27620
|
+
const map2 = await this.config.storageManager.getMapAsync(mapName);
|
|
27621
|
+
if (!(map2 instanceof LWWMap10)) {
|
|
27622
|
+
errors.push({
|
|
27623
|
+
code: 400,
|
|
27624
|
+
message: "HTTP sync only supports LWWMap deltas",
|
|
27625
|
+
context: `Map ${mapName} is not an LWWMap`
|
|
27626
|
+
});
|
|
27627
|
+
continue;
|
|
27628
|
+
}
|
|
27629
|
+
const records = [];
|
|
27630
|
+
for (const key of map2.allKeys()) {
|
|
27631
|
+
const record2 = map2.getRecord(key);
|
|
27632
|
+
if (!record2) continue;
|
|
27633
|
+
if (HLC8.compare(record2.timestamp, lastSyncTimestamp) > 0) {
|
|
27634
|
+
records.push({
|
|
27635
|
+
key,
|
|
27636
|
+
record: record2,
|
|
27637
|
+
eventType: record2.value === null ? "REMOVE" : "PUT"
|
|
27638
|
+
});
|
|
27639
|
+
}
|
|
27640
|
+
}
|
|
27641
|
+
const serverSyncTimestamp = this.config.hlc.now();
|
|
27642
|
+
deltas.push({
|
|
27643
|
+
mapName,
|
|
27644
|
+
records,
|
|
27645
|
+
serverSyncTimestamp
|
|
27646
|
+
});
|
|
27647
|
+
} catch (err) {
|
|
27648
|
+
logger.error({ err, mapName }, "HTTP sync delta computation failed");
|
|
27649
|
+
errors.push({
|
|
27650
|
+
code: 500,
|
|
27651
|
+
message: err.message || "Delta computation failed",
|
|
27652
|
+
context: `Map ${mapName}`
|
|
27653
|
+
});
|
|
27654
|
+
}
|
|
27655
|
+
}
|
|
27656
|
+
return deltas.length > 0 ? deltas : void 0;
|
|
27657
|
+
}
|
|
27658
|
+
/**
|
|
27659
|
+
* Execute one-shot queries.
|
|
27660
|
+
*/
|
|
27661
|
+
async executeQueries(queries, principal, errors) {
|
|
27662
|
+
const results = [];
|
|
27663
|
+
for (const query of queries) {
|
|
27664
|
+
if (!this.config.securityManager.checkPermission(principal, query.mapName, "READ")) {
|
|
27665
|
+
errors.push({
|
|
27666
|
+
code: 403,
|
|
27667
|
+
message: "Access denied",
|
|
27668
|
+
context: `Query ${query.queryId} on ${query.mapName}`
|
|
27669
|
+
});
|
|
27670
|
+
continue;
|
|
27671
|
+
}
|
|
27672
|
+
try {
|
|
27673
|
+
const coreQuery = {
|
|
27674
|
+
where: query.filter,
|
|
27675
|
+
limit: query.limit
|
|
27676
|
+
};
|
|
27677
|
+
const queryResults = await this.config.queryConversionHandler.executeLocalQuery(
|
|
27678
|
+
query.mapName,
|
|
27679
|
+
coreQuery
|
|
27680
|
+
);
|
|
27681
|
+
const hasMore = query.limit ? queryResults.length >= query.limit : false;
|
|
27682
|
+
results.push({
|
|
27683
|
+
queryId: query.queryId,
|
|
27684
|
+
results: queryResults,
|
|
27685
|
+
hasMore
|
|
27686
|
+
});
|
|
27687
|
+
} catch (err) {
|
|
27688
|
+
logger.error({ err, queryId: query.queryId }, "HTTP sync query failed");
|
|
27689
|
+
errors.push({
|
|
27690
|
+
code: 500,
|
|
27691
|
+
message: err.message || "Query failed",
|
|
27692
|
+
context: `Query ${query.queryId} on ${query.mapName}`
|
|
27693
|
+
});
|
|
27694
|
+
}
|
|
27695
|
+
}
|
|
27696
|
+
return results.length > 0 ? results : void 0;
|
|
27697
|
+
}
|
|
27698
|
+
/**
|
|
27699
|
+
* Execute one-shot searches.
|
|
27700
|
+
*/
|
|
27701
|
+
async executeSearches(searches, principal, errors) {
|
|
27702
|
+
const results = [];
|
|
27703
|
+
for (const search of searches) {
|
|
27704
|
+
if (!this.config.securityManager.checkPermission(principal, search.mapName, "READ")) {
|
|
27705
|
+
errors.push({
|
|
27706
|
+
code: 403,
|
|
27707
|
+
message: "Access denied",
|
|
27708
|
+
context: `Search ${search.searchId} on ${search.mapName}`
|
|
27709
|
+
});
|
|
27710
|
+
continue;
|
|
27711
|
+
}
|
|
27712
|
+
try {
|
|
27713
|
+
const searchResult = this.config.searchCoordinator.search(
|
|
27714
|
+
search.mapName,
|
|
27715
|
+
search.query,
|
|
27716
|
+
search.options
|
|
27717
|
+
);
|
|
27718
|
+
results.push({
|
|
27719
|
+
searchId: search.searchId,
|
|
27720
|
+
results: searchResult.results || [],
|
|
27721
|
+
totalCount: searchResult.totalCount
|
|
27722
|
+
});
|
|
27723
|
+
} catch (err) {
|
|
27724
|
+
logger.error({ err, searchId: search.searchId }, "HTTP sync search failed");
|
|
27725
|
+
errors.push({
|
|
27726
|
+
code: 500,
|
|
27727
|
+
message: err.message || "Search failed",
|
|
27728
|
+
context: `Search ${search.searchId} on ${search.mapName}`
|
|
27729
|
+
});
|
|
27730
|
+
}
|
|
27731
|
+
}
|
|
27732
|
+
return results.length > 0 ? results : void 0;
|
|
27733
|
+
}
|
|
27734
|
+
};
|
|
27735
|
+
|
|
27483
27736
|
// src/coordinator/websocket-handler.ts
|
|
27484
27737
|
import * as crypto2 from "crypto";
|
|
27485
27738
|
import { serialize as serialize5, deserialize, MessageSchema } from "@topgunbuild/core";
|
|
@@ -30557,7 +30810,7 @@ function createBootstrapController(config2) {
|
|
|
30557
30810
|
|
|
30558
30811
|
// src/ServerFactory.ts
|
|
30559
30812
|
import { createServer as createHttpServer2 } from "http";
|
|
30560
|
-
import { PARTITION_COUNT as PARTITION_COUNT7 } from "@topgunbuild/core";
|
|
30813
|
+
import { PARTITION_COUNT as PARTITION_COUNT7, serialize as serialize7, deserialize as deserialize2, HttpSyncRequestSchema } from "@topgunbuild/core";
|
|
30561
30814
|
|
|
30562
30815
|
// src/debug/DebugEndpoints.ts
|
|
30563
30816
|
import {
|
|
@@ -31420,6 +31673,25 @@ var ServerFactory = class _ServerFactory {
|
|
|
31420
31673
|
journalSubscriptions
|
|
31421
31674
|
}
|
|
31422
31675
|
} = handlers;
|
|
31676
|
+
const httpSyncHandler = new HttpSyncHandler({
|
|
31677
|
+
authHandler,
|
|
31678
|
+
operationHandler,
|
|
31679
|
+
storageManager,
|
|
31680
|
+
queryConversionHandler,
|
|
31681
|
+
searchCoordinator,
|
|
31682
|
+
hlc,
|
|
31683
|
+
securityManager
|
|
31684
|
+
});
|
|
31685
|
+
if (network.setHttpRequestHandler) {
|
|
31686
|
+
network.setHttpRequestHandler((req, res) => {
|
|
31687
|
+
if (req.method === "POST" && req.url === "/sync") {
|
|
31688
|
+
_ServerFactory.handleHttpSync(req, res, httpSyncHandler);
|
|
31689
|
+
return;
|
|
31690
|
+
}
|
|
31691
|
+
res.writeHead(200);
|
|
31692
|
+
res.end(config2.tls?.enabled ? "TopGun Server Running (Secure)" : "TopGun Server Running");
|
|
31693
|
+
});
|
|
31694
|
+
}
|
|
31423
31695
|
const lifecycle = createLifecycleModule(
|
|
31424
31696
|
{ nodeId: config2.nodeId },
|
|
31425
31697
|
{
|
|
@@ -31522,6 +31794,70 @@ var ServerFactory = class _ServerFactory {
|
|
|
31522
31794
|
}
|
|
31523
31795
|
return coordinator;
|
|
31524
31796
|
}
|
|
31797
|
+
/**
|
|
31798
|
+
* Handle an HTTP sync request by parsing the body, validating auth,
|
|
31799
|
+
* delegating to HttpSyncHandler, and serializing the response.
|
|
31800
|
+
*/
|
|
31801
|
+
static handleHttpSync(req, res, handler) {
|
|
31802
|
+
const authHeader = req.headers["authorization"] || "";
|
|
31803
|
+
const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
|
|
31804
|
+
if (!token) {
|
|
31805
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
31806
|
+
res.end(JSON.stringify({ error: "Missing Authorization header" }));
|
|
31807
|
+
return;
|
|
31808
|
+
}
|
|
31809
|
+
const chunks = [];
|
|
31810
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
31811
|
+
req.on("end", async () => {
|
|
31812
|
+
try {
|
|
31813
|
+
const body = Buffer.concat(chunks);
|
|
31814
|
+
const contentType = req.headers["content-type"] || "";
|
|
31815
|
+
const isJson = contentType.includes("application/json");
|
|
31816
|
+
let parsed;
|
|
31817
|
+
if (isJson) {
|
|
31818
|
+
parsed = JSON.parse(body.toString("utf-8"));
|
|
31819
|
+
} else {
|
|
31820
|
+
parsed = deserialize2(body);
|
|
31821
|
+
}
|
|
31822
|
+
const validation = HttpSyncRequestSchema.safeParse(parsed);
|
|
31823
|
+
if (!validation.success) {
|
|
31824
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
31825
|
+
res.end(JSON.stringify({
|
|
31826
|
+
error: "Invalid request body",
|
|
31827
|
+
details: validation.error.issues
|
|
31828
|
+
}));
|
|
31829
|
+
return;
|
|
31830
|
+
}
|
|
31831
|
+
const response = await handler.handleSyncRequest(validation.data, token);
|
|
31832
|
+
if (isJson) {
|
|
31833
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
31834
|
+
res.end(JSON.stringify(response));
|
|
31835
|
+
} else {
|
|
31836
|
+
const responseBytes = serialize7(response);
|
|
31837
|
+
res.writeHead(200, { "Content-Type": "application/x-msgpack" });
|
|
31838
|
+
res.end(Buffer.from(responseBytes));
|
|
31839
|
+
}
|
|
31840
|
+
} catch (err) {
|
|
31841
|
+
const message = err.message || "Internal server error";
|
|
31842
|
+
if (message.startsWith("401:")) {
|
|
31843
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
31844
|
+
res.end(JSON.stringify({ error: message.slice(5).trim() }));
|
|
31845
|
+
} else if (message.startsWith("403:")) {
|
|
31846
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
31847
|
+
res.end(JSON.stringify({ error: message.slice(5).trim() }));
|
|
31848
|
+
} else {
|
|
31849
|
+
logger.error({ err }, "HTTP sync request failed");
|
|
31850
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
31851
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
31852
|
+
}
|
|
31853
|
+
}
|
|
31854
|
+
});
|
|
31855
|
+
req.on("error", (err) => {
|
|
31856
|
+
logger.error({ err }, "HTTP sync request stream error");
|
|
31857
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
31858
|
+
res.end(JSON.stringify({ error: "Request stream error" }));
|
|
31859
|
+
});
|
|
31860
|
+
}
|
|
31525
31861
|
static createMetricsServer(bootstrap, settings, debug, metrics) {
|
|
31526
31862
|
const server = createHttpServer2(async (req, res) => {
|
|
31527
31863
|
const bootstrapHandled = await bootstrap.handle(req, res);
|
|
@@ -31657,4 +31993,4 @@ export {
|
|
|
31657
31993
|
createBootstrapController,
|
|
31658
31994
|
ServerFactory
|
|
31659
31995
|
};
|
|
31660
|
-
//# sourceMappingURL=chunk-
|
|
31996
|
+
//# sourceMappingURL=chunk-Z3TONATT.mjs.map
|