@topgunbuild/client 0.11.0 → 0.12.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/index.d.mts +134 -31
- package/dist/index.d.ts +134 -31
- package/dist/index.js +342 -107
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +340 -107
- package/dist/index.mjs.map +1 -1
- package/package.json +14 -9
- package/LICENSE +0 -97
package/dist/index.mjs
CHANGED
|
@@ -656,9 +656,7 @@ var WebSocketManager = class {
|
|
|
656
656
|
timeoutMs: this.config.heartbeatConfig.timeoutMs
|
|
657
657
|
}, "Heartbeat timeout - triggering reconnection");
|
|
658
658
|
this.stopHeartbeat();
|
|
659
|
-
this.connectionProvider.
|
|
660
|
-
logger.error({ err }, "Error closing ConnectionProvider on heartbeat timeout");
|
|
661
|
-
});
|
|
659
|
+
this.connectionProvider.forceReconnect();
|
|
662
660
|
}
|
|
663
661
|
}
|
|
664
662
|
/**
|
|
@@ -1345,7 +1343,7 @@ var LockManager = class {
|
|
|
1345
1343
|
/**
|
|
1346
1344
|
* Handle lock granted message from server.
|
|
1347
1345
|
*/
|
|
1348
|
-
handleLockGranted(requestId, fencingToken) {
|
|
1346
|
+
handleLockGranted(requestId, _name, fencingToken) {
|
|
1349
1347
|
const req = this.pendingLockRequests.get(requestId);
|
|
1350
1348
|
if (req) {
|
|
1351
1349
|
clearTimeout(req.timer);
|
|
@@ -1356,7 +1354,7 @@ var LockManager = class {
|
|
|
1356
1354
|
/**
|
|
1357
1355
|
* Handle lock released message from server.
|
|
1358
1356
|
*/
|
|
1359
|
-
handleLockReleased(requestId, success) {
|
|
1357
|
+
handleLockReleased(requestId, _name, success) {
|
|
1360
1358
|
const req = this.pendingLockRequests.get(requestId);
|
|
1361
1359
|
if (req) {
|
|
1362
1360
|
clearTimeout(req.timer);
|
|
@@ -1773,6 +1771,8 @@ import { LWWMap } from "@topgunbuild/core";
|
|
|
1773
1771
|
var MerkleSyncHandler = class {
|
|
1774
1772
|
constructor(config) {
|
|
1775
1773
|
this.lastSyncTimestamp = 0;
|
|
1774
|
+
/** Accumulated sync stats per map, flushed after a quiet period */
|
|
1775
|
+
this.syncStats = /* @__PURE__ */ new Map();
|
|
1776
1776
|
this.config = config;
|
|
1777
1777
|
}
|
|
1778
1778
|
/**
|
|
@@ -1794,7 +1794,8 @@ var MerkleSyncHandler = class {
|
|
|
1794
1794
|
* Compares root hashes and requests buckets if mismatch detected.
|
|
1795
1795
|
*/
|
|
1796
1796
|
async handleSyncRespRoot(payload) {
|
|
1797
|
-
const { mapName,
|
|
1797
|
+
const { mapName, timestamp } = payload;
|
|
1798
|
+
const rootHash = Number(payload.rootHash);
|
|
1798
1799
|
const map = this.config.getMap(mapName);
|
|
1799
1800
|
if (map instanceof LWWMap) {
|
|
1800
1801
|
const localRootHash = map.getMerkleTree().getRootHash();
|
|
@@ -1822,9 +1823,11 @@ var MerkleSyncHandler = class {
|
|
|
1822
1823
|
if (map instanceof LWWMap) {
|
|
1823
1824
|
const tree = map.getMerkleTree();
|
|
1824
1825
|
const localBuckets = tree.getBuckets(path);
|
|
1826
|
+
let mismatchCount = 0;
|
|
1825
1827
|
for (const [bucketKey, remoteHash] of Object.entries(buckets)) {
|
|
1826
1828
|
const localHash = localBuckets[bucketKey] || 0;
|
|
1827
1829
|
if (localHash !== remoteHash) {
|
|
1830
|
+
mismatchCount++;
|
|
1828
1831
|
const newPath = path + bucketKey;
|
|
1829
1832
|
this.config.sendMessage({
|
|
1830
1833
|
type: "MERKLE_REQ_BUCKET",
|
|
@@ -1851,7 +1854,17 @@ var MerkleSyncHandler = class {
|
|
|
1851
1854
|
}
|
|
1852
1855
|
}
|
|
1853
1856
|
if (updateCount > 0) {
|
|
1854
|
-
|
|
1857
|
+
const existing = this.syncStats.get(mapName);
|
|
1858
|
+
if (existing) {
|
|
1859
|
+
existing.count += updateCount;
|
|
1860
|
+
clearTimeout(existing.timer);
|
|
1861
|
+
}
|
|
1862
|
+
const stats = existing ?? { count: updateCount, timer: void 0 };
|
|
1863
|
+
if (!existing) this.syncStats.set(mapName, stats);
|
|
1864
|
+
stats.timer = setTimeout(() => {
|
|
1865
|
+
logger.info({ mapName, count: stats.count }, "Synced records from server");
|
|
1866
|
+
this.syncStats.delete(mapName);
|
|
1867
|
+
}, 100);
|
|
1855
1868
|
}
|
|
1856
1869
|
}
|
|
1857
1870
|
}
|
|
@@ -2134,6 +2147,8 @@ function registerClientMessageHandlers(router, delegates, managers) {
|
|
|
2134
2147
|
},
|
|
2135
2148
|
// SYNC handlers
|
|
2136
2149
|
"OP_ACK": (msg) => delegates.handleOpAck(msg),
|
|
2150
|
+
"OP_REJECTED": (msg) => delegates.handleOpRejected(msg),
|
|
2151
|
+
"ERROR": (msg) => delegates.handleError(msg),
|
|
2137
2152
|
"SYNC_RESP_ROOT": (msg) => managers.merkleSyncHandler.handleSyncRespRoot(msg.payload),
|
|
2138
2153
|
"SYNC_RESP_BUCKETS": (msg) => managers.merkleSyncHandler.handleSyncRespBuckets(msg.payload),
|
|
2139
2154
|
"SYNC_RESP_LEAF": (msg) => managers.merkleSyncHandler.handleSyncRespLeaf(msg.payload),
|
|
@@ -2156,12 +2171,12 @@ function registerClientMessageHandlers(router, delegates, managers) {
|
|
|
2156
2171
|
},
|
|
2157
2172
|
// LOCK handlers
|
|
2158
2173
|
"LOCK_GRANTED": (msg) => {
|
|
2159
|
-
const { requestId, fencingToken } = msg.payload;
|
|
2160
|
-
managers.lockManager.handleLockGranted(requestId, fencingToken);
|
|
2174
|
+
const { requestId, name, fencingToken } = msg.payload;
|
|
2175
|
+
managers.lockManager.handleLockGranted(requestId, name, fencingToken);
|
|
2161
2176
|
},
|
|
2162
2177
|
"LOCK_RELEASED": (msg) => {
|
|
2163
|
-
const { requestId, success } = msg.payload;
|
|
2164
|
-
managers.lockManager.handleLockReleased(requestId, success);
|
|
2178
|
+
const { requestId, name, success } = msg.payload;
|
|
2179
|
+
managers.lockManager.handleLockReleased(requestId, name, success);
|
|
2165
2180
|
},
|
|
2166
2181
|
// GC handler
|
|
2167
2182
|
"GC_PRUNE": (msg) => delegates.handleGcPrune(msg),
|
|
@@ -2199,10 +2214,7 @@ function registerClientMessageHandlers(router, delegates, managers) {
|
|
|
2199
2214
|
managers.searchClient.handleSearchResponse(msg.payload);
|
|
2200
2215
|
},
|
|
2201
2216
|
"SEARCH_UPDATE": () => {
|
|
2202
|
-
}
|
|
2203
|
-
// HYBRID handlers
|
|
2204
|
-
"HYBRID_QUERY_RESP": (msg) => delegates.handleHybridQueryResponse(msg.payload),
|
|
2205
|
-
"HYBRID_QUERY_DELTA": (msg) => delegates.handleHybridQueryDelta(msg.payload)
|
|
2217
|
+
}
|
|
2206
2218
|
});
|
|
2207
2219
|
}
|
|
2208
2220
|
|
|
@@ -2337,8 +2349,8 @@ var SyncEngine = class {
|
|
|
2337
2349
|
handleServerEvent: (msg) => this.handleServerEvent(msg),
|
|
2338
2350
|
handleServerBatchEvent: (msg) => this.handleServerBatchEvent(msg),
|
|
2339
2351
|
handleGcPrune: (msg) => this.handleGcPrune(msg),
|
|
2340
|
-
|
|
2341
|
-
|
|
2352
|
+
handleOpRejected: (msg) => this.handleOpRejected(msg),
|
|
2353
|
+
handleError: (msg) => this.handleError(msg)
|
|
2342
2354
|
},
|
|
2343
2355
|
{
|
|
2344
2356
|
topicManager: this.topicManager,
|
|
@@ -2491,6 +2503,15 @@ var SyncEngine = class {
|
|
|
2491
2503
|
const pending = this.opLog.filter((op) => !op.synced);
|
|
2492
2504
|
if (pending.length === 0) return;
|
|
2493
2505
|
logger.info({ count: pending.length }, "Syncing pending operations");
|
|
2506
|
+
const connectionProvider = this.webSocketManager.getConnectionProvider();
|
|
2507
|
+
if (connectionProvider.sendBatch) {
|
|
2508
|
+
const results = connectionProvider.sendBatch(pending.map((op) => ({ key: op.key, message: op })));
|
|
2509
|
+
const failedKeys = [...results.entries()].filter(([, success]) => !success).map(([key]) => key);
|
|
2510
|
+
if (failedKeys.length > 0) {
|
|
2511
|
+
logger.warn({ failedKeys, count: failedKeys.length }, "Some batch operations failed to send");
|
|
2512
|
+
}
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2494
2515
|
this.sendMessage({
|
|
2495
2516
|
type: "OP_BATCH",
|
|
2496
2517
|
payload: {
|
|
@@ -2511,7 +2532,7 @@ var SyncEngine = class {
|
|
|
2511
2532
|
this.authToken = token;
|
|
2512
2533
|
this.tokenProvider = null;
|
|
2513
2534
|
const state = this.stateMachine.getState();
|
|
2514
|
-
if (state === "AUTHENTICATING" /* AUTHENTICATING */
|
|
2535
|
+
if (state === "AUTHENTICATING" /* AUTHENTICATING */) {
|
|
2515
2536
|
this.sendAuth();
|
|
2516
2537
|
} else if (state === "BACKOFF" /* BACKOFF */ || state === "DISCONNECTED" /* DISCONNECTED */) {
|
|
2517
2538
|
logger.info("Auth token set during backoff/disconnect. Reconnecting immediately.");
|
|
@@ -2616,9 +2637,10 @@ var SyncEngine = class {
|
|
|
2616
2637
|
return;
|
|
2617
2638
|
}
|
|
2618
2639
|
await this.messageRouter.route(message);
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
this.
|
|
2640
|
+
const ts = message.timestamp;
|
|
2641
|
+
if (ts && typeof ts === "object" && "millis" in ts && "counter" in ts && "nodeId" in ts) {
|
|
2642
|
+
this.hlc.update(ts);
|
|
2643
|
+
this.lastSyncTimestamp = Number(ts.millis);
|
|
2622
2644
|
await this.saveOpLog();
|
|
2623
2645
|
}
|
|
2624
2646
|
}
|
|
@@ -2672,20 +2694,37 @@ var SyncEngine = class {
|
|
|
2672
2694
|
this.writeConcernManager.resolveWriteConcernPromise(result.opId, result);
|
|
2673
2695
|
}
|
|
2674
2696
|
}
|
|
2697
|
+
const lastIdNum = parseInt(lastId, 10);
|
|
2675
2698
|
let maxSyncedId = -1;
|
|
2676
2699
|
let ackedCount = 0;
|
|
2677
|
-
|
|
2678
|
-
|
|
2700
|
+
if (!isNaN(lastIdNum)) {
|
|
2701
|
+
this.opLog.forEach((op) => {
|
|
2702
|
+
if (op.id) {
|
|
2703
|
+
const opIdNum = parseInt(op.id, 10);
|
|
2704
|
+
if (!isNaN(opIdNum) && opIdNum <= lastIdNum) {
|
|
2705
|
+
if (!op.synced) {
|
|
2706
|
+
ackedCount++;
|
|
2707
|
+
}
|
|
2708
|
+
op.synced = true;
|
|
2709
|
+
if (opIdNum > maxSyncedId) {
|
|
2710
|
+
maxSyncedId = opIdNum;
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
});
|
|
2715
|
+
} else {
|
|
2716
|
+
logger.warn({ lastId }, "OP_ACK has non-numeric lastId \u2014 marking all pending ops as synced");
|
|
2717
|
+
this.opLog.forEach((op) => {
|
|
2679
2718
|
if (!op.synced) {
|
|
2680
2719
|
ackedCount++;
|
|
2720
|
+
op.synced = true;
|
|
2721
|
+
const opIdNum = parseInt(op.id, 10);
|
|
2722
|
+
if (!isNaN(opIdNum) && opIdNum > maxSyncedId) {
|
|
2723
|
+
maxSyncedId = opIdNum;
|
|
2724
|
+
}
|
|
2681
2725
|
}
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
if (!isNaN(idNum) && idNum > maxSyncedId) {
|
|
2685
|
-
maxSyncedId = idNum;
|
|
2686
|
-
}
|
|
2687
|
-
}
|
|
2688
|
-
});
|
|
2726
|
+
});
|
|
2727
|
+
}
|
|
2689
2728
|
if (maxSyncedId !== -1) {
|
|
2690
2729
|
this.storageAdapter.markOpsSynced(maxSyncedId).catch((err) => logger.error({ err }, "Failed to mark ops synced"));
|
|
2691
2730
|
}
|
|
@@ -2702,10 +2741,10 @@ var SyncEngine = class {
|
|
|
2702
2741
|
}
|
|
2703
2742
|
}
|
|
2704
2743
|
handleQueryUpdate(message) {
|
|
2705
|
-
const { queryId, key, value,
|
|
2744
|
+
const { queryId, key, value, changeType } = message.payload;
|
|
2706
2745
|
const query = this.queryManager.getQueries().get(queryId);
|
|
2707
2746
|
if (query) {
|
|
2708
|
-
query.onUpdate(key,
|
|
2747
|
+
query.onUpdate(key, changeType === "LEAVE" ? null : value);
|
|
2709
2748
|
}
|
|
2710
2749
|
}
|
|
2711
2750
|
async handleServerEvent(message) {
|
|
@@ -3110,31 +3149,24 @@ var SyncEngine = class {
|
|
|
3110
3149
|
return this.queryManager.runLocalHybridQuery(mapName, filter);
|
|
3111
3150
|
}
|
|
3112
3151
|
/**
|
|
3113
|
-
* Handle
|
|
3152
|
+
* Handle operation rejected by server (permission denied, validation failure, etc.).
|
|
3114
3153
|
*/
|
|
3115
|
-
|
|
3116
|
-
const
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
}
|
|
3154
|
+
handleOpRejected(message) {
|
|
3155
|
+
const { opId, reason, code } = message.payload;
|
|
3156
|
+
logger.warn({ opId, reason, code }, "Operation rejected by server");
|
|
3157
|
+
this.writeConcernManager.resolveWriteConcernPromise(opId, {
|
|
3158
|
+
opId,
|
|
3159
|
+
success: false,
|
|
3160
|
+
achievedLevel: "FIRE_AND_FORGET",
|
|
3161
|
+
error: reason
|
|
3162
|
+
});
|
|
3125
3163
|
}
|
|
3126
3164
|
/**
|
|
3127
|
-
* Handle
|
|
3165
|
+
* Handle generic error message from server.
|
|
3128
3166
|
*/
|
|
3129
|
-
|
|
3130
|
-
const
|
|
3131
|
-
|
|
3132
|
-
if (payload.type === "LEAVE") {
|
|
3133
|
-
query.onUpdate(payload.key, null);
|
|
3134
|
-
} else {
|
|
3135
|
-
query.onUpdate(payload.key, payload.value, payload.score, payload.matchedTerms);
|
|
3136
|
-
}
|
|
3137
|
-
}
|
|
3167
|
+
handleError(message) {
|
|
3168
|
+
const { code, message: errorMessage, details } = message.payload;
|
|
3169
|
+
logger.error({ code, message: errorMessage, details }, "Server error received");
|
|
3138
3170
|
}
|
|
3139
3171
|
};
|
|
3140
3172
|
|
|
@@ -4018,8 +4050,8 @@ var SearchHandle = class {
|
|
|
4018
4050
|
handleSearchUpdate(message) {
|
|
4019
4051
|
if (message.type !== "SEARCH_UPDATE") return;
|
|
4020
4052
|
if (message.payload?.subscriptionId !== this.subscriptionId) return;
|
|
4021
|
-
const { key, value, score, matchedTerms,
|
|
4022
|
-
switch (
|
|
4053
|
+
const { key, value, score, matchedTerms, changeType } = message.payload;
|
|
4054
|
+
switch (changeType) {
|
|
4023
4055
|
case "ENTER":
|
|
4024
4056
|
this.results.set(key, {
|
|
4025
4057
|
key,
|
|
@@ -4343,6 +4375,30 @@ import {
|
|
|
4343
4375
|
DEFAULT_CONNECTION_POOL_CONFIG
|
|
4344
4376
|
} from "@topgunbuild/core";
|
|
4345
4377
|
import { serialize as serialize2, deserialize as deserialize3 } from "@topgunbuild/core";
|
|
4378
|
+
|
|
4379
|
+
// src/connection/WebSocketConnection.ts
|
|
4380
|
+
var ConnectionReadyState = {
|
|
4381
|
+
CONNECTING: 0,
|
|
4382
|
+
OPEN: 1,
|
|
4383
|
+
CLOSING: 2,
|
|
4384
|
+
CLOSED: 3
|
|
4385
|
+
};
|
|
4386
|
+
var WebSocketConnection = class {
|
|
4387
|
+
constructor(ws) {
|
|
4388
|
+
this.ws = ws;
|
|
4389
|
+
}
|
|
4390
|
+
send(data) {
|
|
4391
|
+
this.ws.send(data);
|
|
4392
|
+
}
|
|
4393
|
+
close() {
|
|
4394
|
+
this.ws.close();
|
|
4395
|
+
}
|
|
4396
|
+
get readyState() {
|
|
4397
|
+
return this.ws.readyState;
|
|
4398
|
+
}
|
|
4399
|
+
};
|
|
4400
|
+
|
|
4401
|
+
// src/cluster/ConnectionPool.ts
|
|
4346
4402
|
var ConnectionPool = class {
|
|
4347
4403
|
constructor(config = {}) {
|
|
4348
4404
|
this.listeners = /* @__PURE__ */ new Map();
|
|
@@ -4414,10 +4470,17 @@ var ConnectionPool = class {
|
|
|
4414
4470
|
return;
|
|
4415
4471
|
}
|
|
4416
4472
|
}
|
|
4473
|
+
for (const [existingId, existingConn] of this.connections) {
|
|
4474
|
+
if (existingConn.endpoint === endpoint && existingId !== nodeId) {
|
|
4475
|
+
this.remapNodeId(existingId, nodeId);
|
|
4476
|
+
return;
|
|
4477
|
+
}
|
|
4478
|
+
}
|
|
4417
4479
|
const connection = {
|
|
4418
4480
|
nodeId,
|
|
4419
4481
|
endpoint,
|
|
4420
4482
|
socket: null,
|
|
4483
|
+
cachedConnection: null,
|
|
4421
4484
|
state: "DISCONNECTED",
|
|
4422
4485
|
lastSeen: 0,
|
|
4423
4486
|
latencyMs: 0,
|
|
@@ -4452,15 +4515,34 @@ var ConnectionPool = class {
|
|
|
4452
4515
|
}
|
|
4453
4516
|
logger.info({ nodeId }, "Node removed from connection pool");
|
|
4454
4517
|
}
|
|
4518
|
+
/**
|
|
4519
|
+
* Remap a node from one ID to another, preserving the existing connection.
|
|
4520
|
+
* Used when the server-assigned node ID differs from the temporary seed ID.
|
|
4521
|
+
*/
|
|
4522
|
+
remapNodeId(oldId, newId) {
|
|
4523
|
+
const connection = this.connections.get(oldId);
|
|
4524
|
+
if (!connection) return;
|
|
4525
|
+
connection.nodeId = newId;
|
|
4526
|
+
this.connections.delete(oldId);
|
|
4527
|
+
this.connections.set(newId, connection);
|
|
4528
|
+
if (this.primaryNodeId === oldId) {
|
|
4529
|
+
this.primaryNodeId = newId;
|
|
4530
|
+
}
|
|
4531
|
+
logger.info({ oldId, newId }, "Node ID remapped");
|
|
4532
|
+
this.emit("node:remapped", oldId, newId);
|
|
4533
|
+
}
|
|
4455
4534
|
/**
|
|
4456
4535
|
* Get connection for a specific node
|
|
4457
4536
|
*/
|
|
4458
4537
|
getConnection(nodeId) {
|
|
4459
4538
|
const connection = this.connections.get(nodeId);
|
|
4460
|
-
if (!connection || connection.state !== "AUTHENTICATED") {
|
|
4539
|
+
if (!connection || connection.state !== "CONNECTED" && connection.state !== "AUTHENTICATED" || !connection.socket) {
|
|
4461
4540
|
return null;
|
|
4462
4541
|
}
|
|
4463
|
-
|
|
4542
|
+
if (!connection.cachedConnection) {
|
|
4543
|
+
connection.cachedConnection = new WebSocketConnection(connection.socket);
|
|
4544
|
+
}
|
|
4545
|
+
return connection.cachedConnection;
|
|
4464
4546
|
}
|
|
4465
4547
|
/**
|
|
4466
4548
|
* Get primary connection (first/seed node)
|
|
@@ -4474,8 +4556,11 @@ var ConnectionPool = class {
|
|
|
4474
4556
|
*/
|
|
4475
4557
|
getAnyHealthyConnection() {
|
|
4476
4558
|
for (const [nodeId, conn] of this.connections) {
|
|
4477
|
-
if (conn.state === "AUTHENTICATED" && conn.socket) {
|
|
4478
|
-
|
|
4559
|
+
if ((conn.state === "CONNECTED" || conn.state === "AUTHENTICATED") && conn.socket) {
|
|
4560
|
+
if (!conn.cachedConnection) {
|
|
4561
|
+
conn.cachedConnection = new WebSocketConnection(conn.socket);
|
|
4562
|
+
}
|
|
4563
|
+
return { nodeId, connection: conn.cachedConnection };
|
|
4479
4564
|
}
|
|
4480
4565
|
}
|
|
4481
4566
|
return null;
|
|
@@ -4531,7 +4616,7 @@ var ConnectionPool = class {
|
|
|
4531
4616
|
* Get list of connected node IDs
|
|
4532
4617
|
*/
|
|
4533
4618
|
getConnectedNodes() {
|
|
4534
|
-
return Array.from(this.connections.entries()).filter(([_, conn]) => conn.state === "AUTHENTICATED").map(([nodeId]) => nodeId);
|
|
4619
|
+
return Array.from(this.connections.entries()).filter(([_, conn]) => conn.state === "CONNECTED" || conn.state === "AUTHENTICATED").map(([nodeId]) => nodeId);
|
|
4535
4620
|
}
|
|
4536
4621
|
/**
|
|
4537
4622
|
* Get all node IDs
|
|
@@ -4540,11 +4625,11 @@ var ConnectionPool = class {
|
|
|
4540
4625
|
return Array.from(this.connections.keys());
|
|
4541
4626
|
}
|
|
4542
4627
|
/**
|
|
4543
|
-
* Check if node
|
|
4628
|
+
* Check if node has an open WebSocket connection
|
|
4544
4629
|
*/
|
|
4545
4630
|
isNodeConnected(nodeId) {
|
|
4546
4631
|
const conn = this.connections.get(nodeId);
|
|
4547
|
-
return conn?.state === "AUTHENTICATED";
|
|
4632
|
+
return conn?.state === "CONNECTED" || conn?.state === "AUTHENTICATED";
|
|
4548
4633
|
}
|
|
4549
4634
|
/**
|
|
4550
4635
|
* Check if connected to a specific node.
|
|
@@ -4609,25 +4694,26 @@ var ConnectionPool = class {
|
|
|
4609
4694
|
};
|
|
4610
4695
|
socket.onmessage = (event) => {
|
|
4611
4696
|
connection.lastSeen = Date.now();
|
|
4612
|
-
this.handleMessage(nodeId, event);
|
|
4697
|
+
this.handleMessage(connection.nodeId, event);
|
|
4613
4698
|
};
|
|
4614
4699
|
socket.onerror = (error) => {
|
|
4615
|
-
logger.error({ nodeId, error }, "WebSocket error");
|
|
4616
|
-
this.emit("error", nodeId, error instanceof Error ? error : new Error("WebSocket error"));
|
|
4700
|
+
logger.error({ nodeId: connection.nodeId, error }, "WebSocket error");
|
|
4701
|
+
this.emit("error", connection.nodeId, error instanceof Error ? error : new Error("WebSocket error"));
|
|
4617
4702
|
};
|
|
4618
4703
|
socket.onclose = () => {
|
|
4619
4704
|
const wasConnected = connection.state === "AUTHENTICATED";
|
|
4620
4705
|
connection.state = "DISCONNECTED";
|
|
4621
4706
|
connection.socket = null;
|
|
4707
|
+
connection.cachedConnection = null;
|
|
4622
4708
|
if (wasConnected) {
|
|
4623
|
-
this.emit("node:disconnected", nodeId, "Connection closed");
|
|
4709
|
+
this.emit("node:disconnected", connection.nodeId, "Connection closed");
|
|
4624
4710
|
}
|
|
4625
|
-
this.scheduleReconnect(nodeId);
|
|
4711
|
+
this.scheduleReconnect(connection.nodeId);
|
|
4626
4712
|
};
|
|
4627
4713
|
} catch (error) {
|
|
4628
4714
|
connection.state = "FAILED";
|
|
4629
|
-
logger.error({ nodeId, error }, "Failed to connect");
|
|
4630
|
-
this.scheduleReconnect(nodeId);
|
|
4715
|
+
logger.error({ nodeId: connection.nodeId, error }, "Failed to connect");
|
|
4716
|
+
this.scheduleReconnect(connection.nodeId);
|
|
4631
4717
|
}
|
|
4632
4718
|
}
|
|
4633
4719
|
sendAuth(connection) {
|
|
@@ -4656,18 +4742,15 @@ var ConnectionPool = class {
|
|
|
4656
4742
|
logger.info({ nodeId }, "Authenticated with node");
|
|
4657
4743
|
this.emit("node:healthy", nodeId);
|
|
4658
4744
|
this.flushPendingMessages(connection);
|
|
4659
|
-
return;
|
|
4660
4745
|
}
|
|
4661
4746
|
if (message.type === "AUTH_REQUIRED") {
|
|
4662
4747
|
if (this.authToken) {
|
|
4663
4748
|
this.sendAuth(connection);
|
|
4664
4749
|
}
|
|
4665
|
-
return;
|
|
4666
4750
|
}
|
|
4667
4751
|
if (message.type === "AUTH_FAIL") {
|
|
4668
4752
|
logger.error({ nodeId, error: message.error }, "Authentication failed");
|
|
4669
4753
|
connection.state = "FAILED";
|
|
4670
|
-
return;
|
|
4671
4754
|
}
|
|
4672
4755
|
if (message.type === "PONG") {
|
|
4673
4756
|
if (message.timestamp) {
|
|
@@ -4675,10 +4758,6 @@ var ConnectionPool = class {
|
|
|
4675
4758
|
}
|
|
4676
4759
|
return;
|
|
4677
4760
|
}
|
|
4678
|
-
if (message.type === "PARTITION_MAP" || message.type === "PARTITION_MAP_DELTA") {
|
|
4679
|
-
this.emit("message", nodeId, message);
|
|
4680
|
-
return;
|
|
4681
|
-
}
|
|
4682
4761
|
this.emit("message", nodeId, message);
|
|
4683
4762
|
}
|
|
4684
4763
|
flushPendingMessages(connection) {
|
|
@@ -4849,17 +4928,17 @@ var PartitionRouter = class {
|
|
|
4849
4928
|
}
|
|
4850
4929
|
return null;
|
|
4851
4930
|
}
|
|
4852
|
-
const
|
|
4853
|
-
if (
|
|
4854
|
-
return { nodeId: routing.nodeId,
|
|
4931
|
+
const connection = this.connectionPool.getConnection(routing.nodeId);
|
|
4932
|
+
if (connection) {
|
|
4933
|
+
return { nodeId: routing.nodeId, connection };
|
|
4855
4934
|
}
|
|
4856
4935
|
const partition = this.partitionMap.partitions.find((p) => p.partitionId === routing.partitionId);
|
|
4857
4936
|
if (partition) {
|
|
4858
4937
|
for (const backupId of partition.backupNodeIds) {
|
|
4859
|
-
const
|
|
4860
|
-
if (
|
|
4938
|
+
const backupConnection = this.connectionPool.getConnection(backupId);
|
|
4939
|
+
if (backupConnection) {
|
|
4861
4940
|
logger.debug({ key, owner: routing.nodeId, backup: backupId }, "Using backup node");
|
|
4862
|
-
return { nodeId: backupId,
|
|
4941
|
+
return { nodeId: backupId, connection: backupConnection };
|
|
4863
4942
|
}
|
|
4864
4943
|
}
|
|
4865
4944
|
}
|
|
@@ -5135,7 +5214,7 @@ var PartitionRouter = class {
|
|
|
5135
5214
|
};
|
|
5136
5215
|
|
|
5137
5216
|
// src/cluster/ClusterClient.ts
|
|
5138
|
-
var
|
|
5217
|
+
var _ClusterClient = class _ClusterClient {
|
|
5139
5218
|
constructor(config) {
|
|
5140
5219
|
this.listeners = /* @__PURE__ */ new Map();
|
|
5141
5220
|
this.initialized = false;
|
|
@@ -5148,6 +5227,8 @@ var ClusterClient = class {
|
|
|
5148
5227
|
};
|
|
5149
5228
|
// Circuit breaker state per node
|
|
5150
5229
|
this.circuits = /* @__PURE__ */ new Map();
|
|
5230
|
+
// Debounce timer for partition map requests on reconnect
|
|
5231
|
+
this.partitionMapRequestTimer = null;
|
|
5151
5232
|
this.config = config;
|
|
5152
5233
|
this.circuitBreakerConfig = {
|
|
5153
5234
|
...DEFAULT_CIRCUIT_BREAKER_CONFIG,
|
|
@@ -5239,14 +5320,14 @@ var ClusterClient = class {
|
|
|
5239
5320
|
this.requestPartitionMapRefresh();
|
|
5240
5321
|
return this.getFallbackConnection();
|
|
5241
5322
|
}
|
|
5242
|
-
const
|
|
5243
|
-
if (!
|
|
5323
|
+
const connection = this.connectionPool.getConnection(owner);
|
|
5324
|
+
if (!connection) {
|
|
5244
5325
|
this.routingMetrics.fallbackRoutes++;
|
|
5245
5326
|
logger.debug({ key, owner }, "Could not get connection to owner, using fallback");
|
|
5246
5327
|
return this.getFallbackConnection();
|
|
5247
5328
|
}
|
|
5248
5329
|
this.routingMetrics.directRoutes++;
|
|
5249
|
-
return
|
|
5330
|
+
return connection;
|
|
5250
5331
|
}
|
|
5251
5332
|
/**
|
|
5252
5333
|
* Get fallback connection when owner is unavailable.
|
|
@@ -5254,10 +5335,10 @@ var ClusterClient = class {
|
|
|
5254
5335
|
*/
|
|
5255
5336
|
getFallbackConnection() {
|
|
5256
5337
|
const conn = this.connectionPool.getAnyHealthyConnection();
|
|
5257
|
-
if (!conn?.
|
|
5338
|
+
if (!conn?.connection) {
|
|
5258
5339
|
throw new Error("No healthy connection available");
|
|
5259
5340
|
}
|
|
5260
|
-
return conn.
|
|
5341
|
+
return conn.connection;
|
|
5261
5342
|
}
|
|
5262
5343
|
/**
|
|
5263
5344
|
* Request a partition map refresh in the background.
|
|
@@ -5268,9 +5349,23 @@ var ClusterClient = class {
|
|
|
5268
5349
|
logger.error({ err }, "Failed to refresh partition map");
|
|
5269
5350
|
});
|
|
5270
5351
|
}
|
|
5352
|
+
/**
|
|
5353
|
+
* Debounce partition map requests to prevent flooding when multiple nodes
|
|
5354
|
+
* reconnect simultaneously. Coalesces rapid requests into a single request
|
|
5355
|
+
* sent to the most recently connected node.
|
|
5356
|
+
*/
|
|
5357
|
+
debouncedPartitionMapRequest(nodeId) {
|
|
5358
|
+
if (this.partitionMapRequestTimer) {
|
|
5359
|
+
clearTimeout(this.partitionMapRequestTimer);
|
|
5360
|
+
}
|
|
5361
|
+
this.partitionMapRequestTimer = setTimeout(() => {
|
|
5362
|
+
this.partitionMapRequestTimer = null;
|
|
5363
|
+
this.requestPartitionMapFromNode(nodeId);
|
|
5364
|
+
}, _ClusterClient.PARTITION_MAP_REQUEST_DEBOUNCE_MS);
|
|
5365
|
+
}
|
|
5271
5366
|
/**
|
|
5272
5367
|
* Request partition map from a specific node.
|
|
5273
|
-
* Called on
|
|
5368
|
+
* Called on node connection via debounced handler.
|
|
5274
5369
|
*/
|
|
5275
5370
|
requestPartitionMapFromNode(nodeId) {
|
|
5276
5371
|
const socket = this.connectionPool.getConnection(nodeId);
|
|
@@ -5439,8 +5534,8 @@ var ClusterClient = class {
|
|
|
5439
5534
|
* Send directly to partition owner
|
|
5440
5535
|
*/
|
|
5441
5536
|
sendDirect(key, message) {
|
|
5442
|
-
const
|
|
5443
|
-
if (!
|
|
5537
|
+
const route = this.partitionRouter.routeToConnection(key);
|
|
5538
|
+
if (!route) {
|
|
5444
5539
|
logger.warn({ key }, "No route available for key");
|
|
5445
5540
|
return false;
|
|
5446
5541
|
}
|
|
@@ -5451,7 +5546,7 @@ var ClusterClient = class {
|
|
|
5451
5546
|
mapVersion: this.partitionRouter.getMapVersion()
|
|
5452
5547
|
}
|
|
5453
5548
|
};
|
|
5454
|
-
connection.
|
|
5549
|
+
route.connection.send(serialize3(routedMessage));
|
|
5455
5550
|
return true;
|
|
5456
5551
|
}
|
|
5457
5552
|
/**
|
|
@@ -5555,10 +5650,23 @@ var ClusterClient = class {
|
|
|
5555
5650
|
async refreshPartitionMap() {
|
|
5556
5651
|
await this.partitionRouter.refreshPartitionMap();
|
|
5557
5652
|
}
|
|
5653
|
+
/**
|
|
5654
|
+
* Force reconnect all connections in the pool.
|
|
5655
|
+
*/
|
|
5656
|
+
forceReconnect() {
|
|
5657
|
+
this.connectionPool.close();
|
|
5658
|
+
this.connect().catch((err) => {
|
|
5659
|
+
logger.error({ err }, "ClusterClient forceReconnect failed");
|
|
5660
|
+
});
|
|
5661
|
+
}
|
|
5558
5662
|
/**
|
|
5559
5663
|
* Shutdown cluster client (IConnectionProvider interface).
|
|
5560
5664
|
*/
|
|
5561
5665
|
async close() {
|
|
5666
|
+
if (this.partitionMapRequestTimer) {
|
|
5667
|
+
clearTimeout(this.partitionMapRequestTimer);
|
|
5668
|
+
this.partitionMapRequestTimer = null;
|
|
5669
|
+
}
|
|
5562
5670
|
this.partitionRouter.close();
|
|
5563
5671
|
this.connectionPool.close();
|
|
5564
5672
|
this.initialized = false;
|
|
@@ -5581,23 +5689,23 @@ var ClusterClient = class {
|
|
|
5581
5689
|
return this.partitionRouter;
|
|
5582
5690
|
}
|
|
5583
5691
|
/**
|
|
5584
|
-
* Get any healthy
|
|
5692
|
+
* Get any healthy connection (IConnectionProvider interface).
|
|
5585
5693
|
* @throws Error if not connected
|
|
5586
5694
|
*/
|
|
5587
5695
|
getAnyConnection() {
|
|
5588
5696
|
const conn = this.connectionPool.getAnyHealthyConnection();
|
|
5589
|
-
if (!conn?.
|
|
5697
|
+
if (!conn?.connection) {
|
|
5590
5698
|
throw new Error("No healthy connection available");
|
|
5591
5699
|
}
|
|
5592
|
-
return conn.
|
|
5700
|
+
return conn.connection;
|
|
5593
5701
|
}
|
|
5594
5702
|
/**
|
|
5595
|
-
* Get any healthy
|
|
5703
|
+
* Get any healthy connection, or null if none available.
|
|
5596
5704
|
* Use this for optional connection checks.
|
|
5597
5705
|
*/
|
|
5598
5706
|
getAnyConnectionOrNull() {
|
|
5599
5707
|
const conn = this.connectionPool.getAnyHealthyConnection();
|
|
5600
|
-
return conn?.
|
|
5708
|
+
return conn?.connection ?? null;
|
|
5601
5709
|
}
|
|
5602
5710
|
// ============================================
|
|
5603
5711
|
// Circuit Breaker Methods
|
|
@@ -5688,9 +5796,7 @@ var ClusterClient = class {
|
|
|
5688
5796
|
setupEventHandlers() {
|
|
5689
5797
|
this.connectionPool.on("node:connected", (nodeId) => {
|
|
5690
5798
|
logger.debug({ nodeId }, "Node connected");
|
|
5691
|
-
|
|
5692
|
-
this.requestPartitionMapFromNode(nodeId);
|
|
5693
|
-
}
|
|
5799
|
+
this.debouncedPartitionMapRequest(nodeId);
|
|
5694
5800
|
if (this.connectionPool.getConnectedNodes().length === 1) {
|
|
5695
5801
|
this.emit("connected");
|
|
5696
5802
|
}
|
|
@@ -5745,6 +5851,8 @@ var ClusterClient = class {
|
|
|
5745
5851
|
});
|
|
5746
5852
|
}
|
|
5747
5853
|
};
|
|
5854
|
+
_ClusterClient.PARTITION_MAP_REQUEST_DEBOUNCE_MS = 500;
|
|
5855
|
+
var ClusterClient = _ClusterClient;
|
|
5748
5856
|
|
|
5749
5857
|
// src/connection/SingleServerProvider.ts
|
|
5750
5858
|
var DEFAULT_CONFIG = {
|
|
@@ -5760,14 +5868,18 @@ var SingleServerProvider = class {
|
|
|
5760
5868
|
this.reconnectTimer = null;
|
|
5761
5869
|
this.isClosing = false;
|
|
5762
5870
|
this.listeners = /* @__PURE__ */ new Map();
|
|
5871
|
+
this.onlineHandler = null;
|
|
5872
|
+
this.offlineHandler = null;
|
|
5763
5873
|
this.url = config.url;
|
|
5764
5874
|
this.config = {
|
|
5765
5875
|
url: config.url,
|
|
5766
5876
|
maxReconnectAttempts: config.maxReconnectAttempts ?? DEFAULT_CONFIG.maxReconnectAttempts,
|
|
5767
5877
|
reconnectDelayMs: config.reconnectDelayMs ?? DEFAULT_CONFIG.reconnectDelayMs,
|
|
5768
5878
|
backoffMultiplier: config.backoffMultiplier ?? DEFAULT_CONFIG.backoffMultiplier,
|
|
5769
|
-
maxReconnectDelayMs: config.maxReconnectDelayMs ?? DEFAULT_CONFIG.maxReconnectDelayMs
|
|
5879
|
+
maxReconnectDelayMs: config.maxReconnectDelayMs ?? DEFAULT_CONFIG.maxReconnectDelayMs,
|
|
5880
|
+
listenNetworkEvents: config.listenNetworkEvents ?? true
|
|
5770
5881
|
};
|
|
5882
|
+
this.setupNetworkListeners();
|
|
5771
5883
|
}
|
|
5772
5884
|
/**
|
|
5773
5885
|
* Connect to the WebSocket server.
|
|
@@ -5776,6 +5888,9 @@ var SingleServerProvider = class {
|
|
|
5776
5888
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
5777
5889
|
return;
|
|
5778
5890
|
}
|
|
5891
|
+
if (typeof globalThis.navigator !== "undefined" && globalThis.navigator.onLine === false) {
|
|
5892
|
+
throw new Error("Browser is offline \u2014 skipping connection attempt");
|
|
5893
|
+
}
|
|
5779
5894
|
this.isClosing = false;
|
|
5780
5895
|
return new Promise((resolve, reject) => {
|
|
5781
5896
|
try {
|
|
@@ -5828,7 +5943,7 @@ var SingleServerProvider = class {
|
|
|
5828
5943
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
5829
5944
|
throw new Error("Not connected");
|
|
5830
5945
|
}
|
|
5831
|
-
return this.ws;
|
|
5946
|
+
return new WebSocketConnection(this.ws);
|
|
5832
5947
|
}
|
|
5833
5948
|
/**
|
|
5834
5949
|
* Get any available connection.
|
|
@@ -5879,6 +5994,7 @@ var SingleServerProvider = class {
|
|
|
5879
5994
|
*/
|
|
5880
5995
|
async close() {
|
|
5881
5996
|
this.isClosing = true;
|
|
5997
|
+
this.teardownNetworkListeners();
|
|
5882
5998
|
if (this.reconnectTimer) {
|
|
5883
5999
|
clearTimeout(this.reconnectTimer);
|
|
5884
6000
|
this.reconnectTimer = null;
|
|
@@ -5917,6 +6033,10 @@ var SingleServerProvider = class {
|
|
|
5917
6033
|
clearTimeout(this.reconnectTimer);
|
|
5918
6034
|
this.reconnectTimer = null;
|
|
5919
6035
|
}
|
|
6036
|
+
if (typeof globalThis.navigator !== "undefined" && globalThis.navigator.onLine === false && this.config.listenNetworkEvents) {
|
|
6037
|
+
logger.info({ url: this.url }, "Browser offline \u2014 waiting for online event instead of polling");
|
|
6038
|
+
return;
|
|
6039
|
+
}
|
|
5920
6040
|
if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
|
|
5921
6041
|
logger.error(
|
|
5922
6042
|
{ attempts: this.reconnectAttempts, url: this.url },
|
|
@@ -5952,6 +6072,37 @@ var SingleServerProvider = class {
|
|
|
5952
6072
|
delay = delay * (0.5 + Math.random());
|
|
5953
6073
|
return Math.floor(delay);
|
|
5954
6074
|
}
|
|
6075
|
+
/**
|
|
6076
|
+
* Force-close the current WebSocket and immediately schedule reconnection.
|
|
6077
|
+
* Unlike close(), this does NOT set isClosing and preserves reconnect behavior.
|
|
6078
|
+
* Resets the reconnect counter so the full backoff budget is available.
|
|
6079
|
+
*
|
|
6080
|
+
* Critically, this does NOT wait for the TCP close handshake (which can
|
|
6081
|
+
* hang 20+ seconds on a dead network). Instead it strips all handlers from
|
|
6082
|
+
* the old WebSocket, fires a best-effort close(), nulls the reference, and
|
|
6083
|
+
* schedules reconnect right away.
|
|
6084
|
+
*/
|
|
6085
|
+
forceReconnect() {
|
|
6086
|
+
this.reconnectAttempts = 0;
|
|
6087
|
+
this.isClosing = false;
|
|
6088
|
+
if (this.reconnectTimer) {
|
|
6089
|
+
clearTimeout(this.reconnectTimer);
|
|
6090
|
+
this.reconnectTimer = null;
|
|
6091
|
+
}
|
|
6092
|
+
if (this.ws) {
|
|
6093
|
+
this.ws.onopen = null;
|
|
6094
|
+
this.ws.onclose = null;
|
|
6095
|
+
this.ws.onerror = null;
|
|
6096
|
+
this.ws.onmessage = null;
|
|
6097
|
+
try {
|
|
6098
|
+
this.ws.close();
|
|
6099
|
+
} catch {
|
|
6100
|
+
}
|
|
6101
|
+
this.ws = null;
|
|
6102
|
+
}
|
|
6103
|
+
this.emit("disconnected", "default");
|
|
6104
|
+
this.scheduleReconnect();
|
|
6105
|
+
}
|
|
5955
6106
|
/**
|
|
5956
6107
|
* Get the WebSocket URL this provider connects to.
|
|
5957
6108
|
*/
|
|
@@ -5971,6 +6122,43 @@ var SingleServerProvider = class {
|
|
|
5971
6122
|
resetReconnectAttempts() {
|
|
5972
6123
|
this.reconnectAttempts = 0;
|
|
5973
6124
|
}
|
|
6125
|
+
/**
|
|
6126
|
+
* Listen for browser 'online' event to trigger instant reconnect
|
|
6127
|
+
* when network comes back. Only active in browser environments.
|
|
6128
|
+
*/
|
|
6129
|
+
setupNetworkListeners() {
|
|
6130
|
+
if (!this.config.listenNetworkEvents) return;
|
|
6131
|
+
if (typeof globalThis.addEventListener !== "function") return;
|
|
6132
|
+
this.onlineHandler = () => {
|
|
6133
|
+
if (this.isClosing) return;
|
|
6134
|
+
if (this.isConnected()) return;
|
|
6135
|
+
logger.info({ url: this.url }, "Network online detected \u2014 forcing reconnect");
|
|
6136
|
+
this.forceReconnect();
|
|
6137
|
+
};
|
|
6138
|
+
this.offlineHandler = () => {
|
|
6139
|
+
if (this.isClosing) return;
|
|
6140
|
+
if (!this.isConnected()) return;
|
|
6141
|
+
logger.info({ url: this.url }, "Network offline detected \u2014 disconnecting immediately");
|
|
6142
|
+
this.forceReconnect();
|
|
6143
|
+
};
|
|
6144
|
+
globalThis.addEventListener("online", this.onlineHandler);
|
|
6145
|
+
globalThis.addEventListener("offline", this.offlineHandler);
|
|
6146
|
+
}
|
|
6147
|
+
/**
|
|
6148
|
+
* Remove browser network event listeners.
|
|
6149
|
+
*/
|
|
6150
|
+
teardownNetworkListeners() {
|
|
6151
|
+
if (typeof globalThis.removeEventListener === "function") {
|
|
6152
|
+
if (this.onlineHandler) {
|
|
6153
|
+
globalThis.removeEventListener("online", this.onlineHandler);
|
|
6154
|
+
this.onlineHandler = null;
|
|
6155
|
+
}
|
|
6156
|
+
if (this.offlineHandler) {
|
|
6157
|
+
globalThis.removeEventListener("offline", this.offlineHandler);
|
|
6158
|
+
this.offlineHandler = null;
|
|
6159
|
+
}
|
|
6160
|
+
}
|
|
6161
|
+
}
|
|
5974
6162
|
};
|
|
5975
6163
|
|
|
5976
6164
|
// src/TopGunClient.ts
|
|
@@ -6028,7 +6216,13 @@ var TopGunClient = class {
|
|
|
6028
6216
|
});
|
|
6029
6217
|
logger.info({ seeds: this.clusterConfig.seeds }, "TopGunClient initialized in cluster mode");
|
|
6030
6218
|
} else {
|
|
6031
|
-
const singleServerProvider = new SingleServerProvider({
|
|
6219
|
+
const singleServerProvider = new SingleServerProvider({
|
|
6220
|
+
url: config.serverUrl,
|
|
6221
|
+
maxReconnectAttempts: config.backoff?.maxRetries,
|
|
6222
|
+
reconnectDelayMs: config.backoff?.initialDelayMs,
|
|
6223
|
+
backoffMultiplier: config.backoff?.multiplier,
|
|
6224
|
+
maxReconnectDelayMs: config.backoff?.maxDelayMs
|
|
6225
|
+
});
|
|
6032
6226
|
this.syncEngine = new SyncEngine({
|
|
6033
6227
|
nodeId: this.nodeId,
|
|
6034
6228
|
connectionProvider: singleServerProvider,
|
|
@@ -7078,6 +7272,24 @@ import { LWWMap as LWWMap4, Predicates } from "@topgunbuild/core";
|
|
|
7078
7272
|
|
|
7079
7273
|
// src/connection/HttpSyncProvider.ts
|
|
7080
7274
|
import { serialize as serialize5, deserialize as deserialize5 } from "@topgunbuild/core";
|
|
7275
|
+
var HttpConnection = class {
|
|
7276
|
+
constructor(provider) {
|
|
7277
|
+
this.provider = provider;
|
|
7278
|
+
}
|
|
7279
|
+
send(data) {
|
|
7280
|
+
if (typeof data === "string") {
|
|
7281
|
+
const encoder = new TextEncoder();
|
|
7282
|
+
this.provider.send(encoder.encode(data));
|
|
7283
|
+
} else {
|
|
7284
|
+
this.provider.send(data instanceof ArrayBuffer ? new Uint8Array(data) : data);
|
|
7285
|
+
}
|
|
7286
|
+
}
|
|
7287
|
+
close() {
|
|
7288
|
+
}
|
|
7289
|
+
get readyState() {
|
|
7290
|
+
return this.provider.isConnected() ? ConnectionReadyState.OPEN : ConnectionReadyState.CLOSED;
|
|
7291
|
+
}
|
|
7292
|
+
};
|
|
7081
7293
|
var HttpSyncProvider = class {
|
|
7082
7294
|
constructor(config) {
|
|
7083
7295
|
this.listeners = /* @__PURE__ */ new Map();
|
|
@@ -7119,17 +7331,17 @@ var HttpSyncProvider = class {
|
|
|
7119
7331
|
}
|
|
7120
7332
|
/**
|
|
7121
7333
|
* Get connection for a specific key.
|
|
7122
|
-
*
|
|
7334
|
+
* Returns an HttpConnection that queues operations for the next poll cycle.
|
|
7123
7335
|
*/
|
|
7124
7336
|
getConnection(_key) {
|
|
7125
|
-
|
|
7337
|
+
return new HttpConnection(this);
|
|
7126
7338
|
}
|
|
7127
7339
|
/**
|
|
7128
7340
|
* Get any available connection.
|
|
7129
|
-
*
|
|
7341
|
+
* Returns an HttpConnection that queues operations for the next poll cycle.
|
|
7130
7342
|
*/
|
|
7131
7343
|
getAnyConnection() {
|
|
7132
|
-
|
|
7344
|
+
return new HttpConnection(this);
|
|
7133
7345
|
}
|
|
7134
7346
|
/**
|
|
7135
7347
|
* Check if connected (last HTTP request succeeded).
|
|
@@ -7210,6 +7422,17 @@ var HttpSyncProvider = class {
|
|
|
7210
7422
|
logger.warn({ err }, "HTTP sync provider: failed to deserialize message");
|
|
7211
7423
|
}
|
|
7212
7424
|
}
|
|
7425
|
+
/**
|
|
7426
|
+
* Force reconnect by restarting the polling loop.
|
|
7427
|
+
*/
|
|
7428
|
+
forceReconnect() {
|
|
7429
|
+
this.stopPolling();
|
|
7430
|
+
this.connected = false;
|
|
7431
|
+
this.emit("disconnected", "default");
|
|
7432
|
+
this.connect().catch((err) => {
|
|
7433
|
+
logger.error({ err }, "HttpSyncProvider forceReconnect failed");
|
|
7434
|
+
});
|
|
7435
|
+
}
|
|
7213
7436
|
/**
|
|
7214
7437
|
* Close the HTTP sync provider.
|
|
7215
7438
|
* Stops the polling loop, clears queued operations, and sets disconnected state.
|
|
@@ -7482,6 +7705,14 @@ var AutoConnectionProvider = class {
|
|
|
7482
7705
|
}
|
|
7483
7706
|
this.activeProvider.send(data, key);
|
|
7484
7707
|
}
|
|
7708
|
+
/**
|
|
7709
|
+
* Force reconnect by delegating to the active provider.
|
|
7710
|
+
*/
|
|
7711
|
+
forceReconnect() {
|
|
7712
|
+
if (this.activeProvider) {
|
|
7713
|
+
this.activeProvider.forceReconnect();
|
|
7714
|
+
}
|
|
7715
|
+
}
|
|
7485
7716
|
/**
|
|
7486
7717
|
* Close the active underlying provider.
|
|
7487
7718
|
*/
|
|
@@ -7538,6 +7769,7 @@ export {
|
|
|
7538
7769
|
ClusterClient,
|
|
7539
7770
|
ConflictResolverClient,
|
|
7540
7771
|
ConnectionPool,
|
|
7772
|
+
ConnectionReadyState,
|
|
7541
7773
|
DEFAULT_BACKPRESSURE_CONFIG,
|
|
7542
7774
|
DEFAULT_CLUSTER_CONFIG,
|
|
7543
7775
|
EncryptedStorageAdapter,
|
|
@@ -7559,6 +7791,7 @@ export {
|
|
|
7559
7791
|
TopGunClient,
|
|
7560
7792
|
TopicHandle,
|
|
7561
7793
|
VALID_TRANSITIONS,
|
|
7794
|
+
WebSocketConnection,
|
|
7562
7795
|
isValidTransition,
|
|
7563
7796
|
logger
|
|
7564
7797
|
};
|