@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.js
CHANGED
|
@@ -36,6 +36,7 @@ __export(index_exports, {
|
|
|
36
36
|
ClusterClient: () => ClusterClient,
|
|
37
37
|
ConflictResolverClient: () => ConflictResolverClient,
|
|
38
38
|
ConnectionPool: () => ConnectionPool,
|
|
39
|
+
ConnectionReadyState: () => ConnectionReadyState,
|
|
39
40
|
DEFAULT_BACKPRESSURE_CONFIG: () => DEFAULT_BACKPRESSURE_CONFIG,
|
|
40
41
|
DEFAULT_CLUSTER_CONFIG: () => DEFAULT_CLUSTER_CONFIG,
|
|
41
42
|
EncryptedStorageAdapter: () => EncryptedStorageAdapter,
|
|
@@ -57,6 +58,7 @@ __export(index_exports, {
|
|
|
57
58
|
TopGunClient: () => TopGunClient,
|
|
58
59
|
TopicHandle: () => TopicHandle,
|
|
59
60
|
VALID_TRANSITIONS: () => VALID_TRANSITIONS,
|
|
61
|
+
WebSocketConnection: () => WebSocketConnection,
|
|
60
62
|
isValidTransition: () => isValidTransition,
|
|
61
63
|
logger: () => logger
|
|
62
64
|
});
|
|
@@ -720,9 +722,7 @@ var WebSocketManager = class {
|
|
|
720
722
|
timeoutMs: this.config.heartbeatConfig.timeoutMs
|
|
721
723
|
}, "Heartbeat timeout - triggering reconnection");
|
|
722
724
|
this.stopHeartbeat();
|
|
723
|
-
this.connectionProvider.
|
|
724
|
-
logger.error({ err }, "Error closing ConnectionProvider on heartbeat timeout");
|
|
725
|
-
});
|
|
725
|
+
this.connectionProvider.forceReconnect();
|
|
726
726
|
}
|
|
727
727
|
}
|
|
728
728
|
/**
|
|
@@ -1409,7 +1409,7 @@ var LockManager = class {
|
|
|
1409
1409
|
/**
|
|
1410
1410
|
* Handle lock granted message from server.
|
|
1411
1411
|
*/
|
|
1412
|
-
handleLockGranted(requestId, fencingToken) {
|
|
1412
|
+
handleLockGranted(requestId, _name, fencingToken) {
|
|
1413
1413
|
const req = this.pendingLockRequests.get(requestId);
|
|
1414
1414
|
if (req) {
|
|
1415
1415
|
clearTimeout(req.timer);
|
|
@@ -1420,7 +1420,7 @@ var LockManager = class {
|
|
|
1420
1420
|
/**
|
|
1421
1421
|
* Handle lock released message from server.
|
|
1422
1422
|
*/
|
|
1423
|
-
handleLockReleased(requestId, success) {
|
|
1423
|
+
handleLockReleased(requestId, _name, success) {
|
|
1424
1424
|
const req = this.pendingLockRequests.get(requestId);
|
|
1425
1425
|
if (req) {
|
|
1426
1426
|
clearTimeout(req.timer);
|
|
@@ -1837,6 +1837,8 @@ var import_core3 = require("@topgunbuild/core");
|
|
|
1837
1837
|
var MerkleSyncHandler = class {
|
|
1838
1838
|
constructor(config) {
|
|
1839
1839
|
this.lastSyncTimestamp = 0;
|
|
1840
|
+
/** Accumulated sync stats per map, flushed after a quiet period */
|
|
1841
|
+
this.syncStats = /* @__PURE__ */ new Map();
|
|
1840
1842
|
this.config = config;
|
|
1841
1843
|
}
|
|
1842
1844
|
/**
|
|
@@ -1858,7 +1860,8 @@ var MerkleSyncHandler = class {
|
|
|
1858
1860
|
* Compares root hashes and requests buckets if mismatch detected.
|
|
1859
1861
|
*/
|
|
1860
1862
|
async handleSyncRespRoot(payload) {
|
|
1861
|
-
const { mapName,
|
|
1863
|
+
const { mapName, timestamp } = payload;
|
|
1864
|
+
const rootHash = Number(payload.rootHash);
|
|
1862
1865
|
const map = this.config.getMap(mapName);
|
|
1863
1866
|
if (map instanceof import_core3.LWWMap) {
|
|
1864
1867
|
const localRootHash = map.getMerkleTree().getRootHash();
|
|
@@ -1886,9 +1889,11 @@ var MerkleSyncHandler = class {
|
|
|
1886
1889
|
if (map instanceof import_core3.LWWMap) {
|
|
1887
1890
|
const tree = map.getMerkleTree();
|
|
1888
1891
|
const localBuckets = tree.getBuckets(path);
|
|
1892
|
+
let mismatchCount = 0;
|
|
1889
1893
|
for (const [bucketKey, remoteHash] of Object.entries(buckets)) {
|
|
1890
1894
|
const localHash = localBuckets[bucketKey] || 0;
|
|
1891
1895
|
if (localHash !== remoteHash) {
|
|
1896
|
+
mismatchCount++;
|
|
1892
1897
|
const newPath = path + bucketKey;
|
|
1893
1898
|
this.config.sendMessage({
|
|
1894
1899
|
type: "MERKLE_REQ_BUCKET",
|
|
@@ -1915,7 +1920,17 @@ var MerkleSyncHandler = class {
|
|
|
1915
1920
|
}
|
|
1916
1921
|
}
|
|
1917
1922
|
if (updateCount > 0) {
|
|
1918
|
-
|
|
1923
|
+
const existing = this.syncStats.get(mapName);
|
|
1924
|
+
if (existing) {
|
|
1925
|
+
existing.count += updateCount;
|
|
1926
|
+
clearTimeout(existing.timer);
|
|
1927
|
+
}
|
|
1928
|
+
const stats = existing ?? { count: updateCount, timer: void 0 };
|
|
1929
|
+
if (!existing) this.syncStats.set(mapName, stats);
|
|
1930
|
+
stats.timer = setTimeout(() => {
|
|
1931
|
+
logger.info({ mapName, count: stats.count }, "Synced records from server");
|
|
1932
|
+
this.syncStats.delete(mapName);
|
|
1933
|
+
}, 100);
|
|
1919
1934
|
}
|
|
1920
1935
|
}
|
|
1921
1936
|
}
|
|
@@ -2198,6 +2213,8 @@ function registerClientMessageHandlers(router, delegates, managers) {
|
|
|
2198
2213
|
},
|
|
2199
2214
|
// SYNC handlers
|
|
2200
2215
|
"OP_ACK": (msg) => delegates.handleOpAck(msg),
|
|
2216
|
+
"OP_REJECTED": (msg) => delegates.handleOpRejected(msg),
|
|
2217
|
+
"ERROR": (msg) => delegates.handleError(msg),
|
|
2201
2218
|
"SYNC_RESP_ROOT": (msg) => managers.merkleSyncHandler.handleSyncRespRoot(msg.payload),
|
|
2202
2219
|
"SYNC_RESP_BUCKETS": (msg) => managers.merkleSyncHandler.handleSyncRespBuckets(msg.payload),
|
|
2203
2220
|
"SYNC_RESP_LEAF": (msg) => managers.merkleSyncHandler.handleSyncRespLeaf(msg.payload),
|
|
@@ -2220,12 +2237,12 @@ function registerClientMessageHandlers(router, delegates, managers) {
|
|
|
2220
2237
|
},
|
|
2221
2238
|
// LOCK handlers
|
|
2222
2239
|
"LOCK_GRANTED": (msg) => {
|
|
2223
|
-
const { requestId, fencingToken } = msg.payload;
|
|
2224
|
-
managers.lockManager.handleLockGranted(requestId, fencingToken);
|
|
2240
|
+
const { requestId, name, fencingToken } = msg.payload;
|
|
2241
|
+
managers.lockManager.handleLockGranted(requestId, name, fencingToken);
|
|
2225
2242
|
},
|
|
2226
2243
|
"LOCK_RELEASED": (msg) => {
|
|
2227
|
-
const { requestId, success } = msg.payload;
|
|
2228
|
-
managers.lockManager.handleLockReleased(requestId, success);
|
|
2244
|
+
const { requestId, name, success } = msg.payload;
|
|
2245
|
+
managers.lockManager.handleLockReleased(requestId, name, success);
|
|
2229
2246
|
},
|
|
2230
2247
|
// GC handler
|
|
2231
2248
|
"GC_PRUNE": (msg) => delegates.handleGcPrune(msg),
|
|
@@ -2263,10 +2280,7 @@ function registerClientMessageHandlers(router, delegates, managers) {
|
|
|
2263
2280
|
managers.searchClient.handleSearchResponse(msg.payload);
|
|
2264
2281
|
},
|
|
2265
2282
|
"SEARCH_UPDATE": () => {
|
|
2266
|
-
}
|
|
2267
|
-
// HYBRID handlers
|
|
2268
|
-
"HYBRID_QUERY_RESP": (msg) => delegates.handleHybridQueryResponse(msg.payload),
|
|
2269
|
-
"HYBRID_QUERY_DELTA": (msg) => delegates.handleHybridQueryDelta(msg.payload)
|
|
2283
|
+
}
|
|
2270
2284
|
});
|
|
2271
2285
|
}
|
|
2272
2286
|
|
|
@@ -2401,8 +2415,8 @@ var SyncEngine = class {
|
|
|
2401
2415
|
handleServerEvent: (msg) => this.handleServerEvent(msg),
|
|
2402
2416
|
handleServerBatchEvent: (msg) => this.handleServerBatchEvent(msg),
|
|
2403
2417
|
handleGcPrune: (msg) => this.handleGcPrune(msg),
|
|
2404
|
-
|
|
2405
|
-
|
|
2418
|
+
handleOpRejected: (msg) => this.handleOpRejected(msg),
|
|
2419
|
+
handleError: (msg) => this.handleError(msg)
|
|
2406
2420
|
},
|
|
2407
2421
|
{
|
|
2408
2422
|
topicManager: this.topicManager,
|
|
@@ -2555,6 +2569,15 @@ var SyncEngine = class {
|
|
|
2555
2569
|
const pending = this.opLog.filter((op) => !op.synced);
|
|
2556
2570
|
if (pending.length === 0) return;
|
|
2557
2571
|
logger.info({ count: pending.length }, "Syncing pending operations");
|
|
2572
|
+
const connectionProvider = this.webSocketManager.getConnectionProvider();
|
|
2573
|
+
if (connectionProvider.sendBatch) {
|
|
2574
|
+
const results = connectionProvider.sendBatch(pending.map((op) => ({ key: op.key, message: op })));
|
|
2575
|
+
const failedKeys = [...results.entries()].filter(([, success]) => !success).map(([key]) => key);
|
|
2576
|
+
if (failedKeys.length > 0) {
|
|
2577
|
+
logger.warn({ failedKeys, count: failedKeys.length }, "Some batch operations failed to send");
|
|
2578
|
+
}
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2558
2581
|
this.sendMessage({
|
|
2559
2582
|
type: "OP_BATCH",
|
|
2560
2583
|
payload: {
|
|
@@ -2575,7 +2598,7 @@ var SyncEngine = class {
|
|
|
2575
2598
|
this.authToken = token;
|
|
2576
2599
|
this.tokenProvider = null;
|
|
2577
2600
|
const state = this.stateMachine.getState();
|
|
2578
|
-
if (state === "AUTHENTICATING" /* AUTHENTICATING */
|
|
2601
|
+
if (state === "AUTHENTICATING" /* AUTHENTICATING */) {
|
|
2579
2602
|
this.sendAuth();
|
|
2580
2603
|
} else if (state === "BACKOFF" /* BACKOFF */ || state === "DISCONNECTED" /* DISCONNECTED */) {
|
|
2581
2604
|
logger.info("Auth token set during backoff/disconnect. Reconnecting immediately.");
|
|
@@ -2680,9 +2703,10 @@ var SyncEngine = class {
|
|
|
2680
2703
|
return;
|
|
2681
2704
|
}
|
|
2682
2705
|
await this.messageRouter.route(message);
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
this.
|
|
2706
|
+
const ts = message.timestamp;
|
|
2707
|
+
if (ts && typeof ts === "object" && "millis" in ts && "counter" in ts && "nodeId" in ts) {
|
|
2708
|
+
this.hlc.update(ts);
|
|
2709
|
+
this.lastSyncTimestamp = Number(ts.millis);
|
|
2686
2710
|
await this.saveOpLog();
|
|
2687
2711
|
}
|
|
2688
2712
|
}
|
|
@@ -2736,20 +2760,37 @@ var SyncEngine = class {
|
|
|
2736
2760
|
this.writeConcernManager.resolveWriteConcernPromise(result.opId, result);
|
|
2737
2761
|
}
|
|
2738
2762
|
}
|
|
2763
|
+
const lastIdNum = parseInt(lastId, 10);
|
|
2739
2764
|
let maxSyncedId = -1;
|
|
2740
2765
|
let ackedCount = 0;
|
|
2741
|
-
|
|
2742
|
-
|
|
2766
|
+
if (!isNaN(lastIdNum)) {
|
|
2767
|
+
this.opLog.forEach((op) => {
|
|
2768
|
+
if (op.id) {
|
|
2769
|
+
const opIdNum = parseInt(op.id, 10);
|
|
2770
|
+
if (!isNaN(opIdNum) && opIdNum <= lastIdNum) {
|
|
2771
|
+
if (!op.synced) {
|
|
2772
|
+
ackedCount++;
|
|
2773
|
+
}
|
|
2774
|
+
op.synced = true;
|
|
2775
|
+
if (opIdNum > maxSyncedId) {
|
|
2776
|
+
maxSyncedId = opIdNum;
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
});
|
|
2781
|
+
} else {
|
|
2782
|
+
logger.warn({ lastId }, "OP_ACK has non-numeric lastId \u2014 marking all pending ops as synced");
|
|
2783
|
+
this.opLog.forEach((op) => {
|
|
2743
2784
|
if (!op.synced) {
|
|
2744
2785
|
ackedCount++;
|
|
2786
|
+
op.synced = true;
|
|
2787
|
+
const opIdNum = parseInt(op.id, 10);
|
|
2788
|
+
if (!isNaN(opIdNum) && opIdNum > maxSyncedId) {
|
|
2789
|
+
maxSyncedId = opIdNum;
|
|
2790
|
+
}
|
|
2745
2791
|
}
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
if (!isNaN(idNum) && idNum > maxSyncedId) {
|
|
2749
|
-
maxSyncedId = idNum;
|
|
2750
|
-
}
|
|
2751
|
-
}
|
|
2752
|
-
});
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2753
2794
|
if (maxSyncedId !== -1) {
|
|
2754
2795
|
this.storageAdapter.markOpsSynced(maxSyncedId).catch((err) => logger.error({ err }, "Failed to mark ops synced"));
|
|
2755
2796
|
}
|
|
@@ -2766,10 +2807,10 @@ var SyncEngine = class {
|
|
|
2766
2807
|
}
|
|
2767
2808
|
}
|
|
2768
2809
|
handleQueryUpdate(message) {
|
|
2769
|
-
const { queryId, key, value,
|
|
2810
|
+
const { queryId, key, value, changeType } = message.payload;
|
|
2770
2811
|
const query = this.queryManager.getQueries().get(queryId);
|
|
2771
2812
|
if (query) {
|
|
2772
|
-
query.onUpdate(key,
|
|
2813
|
+
query.onUpdate(key, changeType === "LEAVE" ? null : value);
|
|
2773
2814
|
}
|
|
2774
2815
|
}
|
|
2775
2816
|
async handleServerEvent(message) {
|
|
@@ -3174,31 +3215,24 @@ var SyncEngine = class {
|
|
|
3174
3215
|
return this.queryManager.runLocalHybridQuery(mapName, filter);
|
|
3175
3216
|
}
|
|
3176
3217
|
/**
|
|
3177
|
-
* Handle
|
|
3218
|
+
* Handle operation rejected by server (permission denied, validation failure, etc.).
|
|
3178
3219
|
*/
|
|
3179
|
-
|
|
3180
|
-
const
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
}
|
|
3220
|
+
handleOpRejected(message) {
|
|
3221
|
+
const { opId, reason, code } = message.payload;
|
|
3222
|
+
logger.warn({ opId, reason, code }, "Operation rejected by server");
|
|
3223
|
+
this.writeConcernManager.resolveWriteConcernPromise(opId, {
|
|
3224
|
+
opId,
|
|
3225
|
+
success: false,
|
|
3226
|
+
achievedLevel: "FIRE_AND_FORGET",
|
|
3227
|
+
error: reason
|
|
3228
|
+
});
|
|
3189
3229
|
}
|
|
3190
3230
|
/**
|
|
3191
|
-
* Handle
|
|
3231
|
+
* Handle generic error message from server.
|
|
3192
3232
|
*/
|
|
3193
|
-
|
|
3194
|
-
const
|
|
3195
|
-
|
|
3196
|
-
if (payload.type === "LEAVE") {
|
|
3197
|
-
query.onUpdate(payload.key, null);
|
|
3198
|
-
} else {
|
|
3199
|
-
query.onUpdate(payload.key, payload.value, payload.score, payload.matchedTerms);
|
|
3200
|
-
}
|
|
3201
|
-
}
|
|
3233
|
+
handleError(message) {
|
|
3234
|
+
const { code, message: errorMessage, details } = message.payload;
|
|
3235
|
+
logger.error({ code, message: errorMessage, details }, "Server error received");
|
|
3202
3236
|
}
|
|
3203
3237
|
};
|
|
3204
3238
|
|
|
@@ -4082,8 +4116,8 @@ var SearchHandle = class {
|
|
|
4082
4116
|
handleSearchUpdate(message) {
|
|
4083
4117
|
if (message.type !== "SEARCH_UPDATE") return;
|
|
4084
4118
|
if (message.payload?.subscriptionId !== this.subscriptionId) return;
|
|
4085
|
-
const { key, value, score, matchedTerms,
|
|
4086
|
-
switch (
|
|
4119
|
+
const { key, value, score, matchedTerms, changeType } = message.payload;
|
|
4120
|
+
switch (changeType) {
|
|
4087
4121
|
case "ENTER":
|
|
4088
4122
|
this.results.set(key, {
|
|
4089
4123
|
key,
|
|
@@ -4400,6 +4434,30 @@ var import_core10 = require("@topgunbuild/core");
|
|
|
4400
4434
|
// src/cluster/ConnectionPool.ts
|
|
4401
4435
|
var import_core7 = require("@topgunbuild/core");
|
|
4402
4436
|
var import_core8 = require("@topgunbuild/core");
|
|
4437
|
+
|
|
4438
|
+
// src/connection/WebSocketConnection.ts
|
|
4439
|
+
var ConnectionReadyState = {
|
|
4440
|
+
CONNECTING: 0,
|
|
4441
|
+
OPEN: 1,
|
|
4442
|
+
CLOSING: 2,
|
|
4443
|
+
CLOSED: 3
|
|
4444
|
+
};
|
|
4445
|
+
var WebSocketConnection = class {
|
|
4446
|
+
constructor(ws) {
|
|
4447
|
+
this.ws = ws;
|
|
4448
|
+
}
|
|
4449
|
+
send(data) {
|
|
4450
|
+
this.ws.send(data);
|
|
4451
|
+
}
|
|
4452
|
+
close() {
|
|
4453
|
+
this.ws.close();
|
|
4454
|
+
}
|
|
4455
|
+
get readyState() {
|
|
4456
|
+
return this.ws.readyState;
|
|
4457
|
+
}
|
|
4458
|
+
};
|
|
4459
|
+
|
|
4460
|
+
// src/cluster/ConnectionPool.ts
|
|
4403
4461
|
var ConnectionPool = class {
|
|
4404
4462
|
constructor(config = {}) {
|
|
4405
4463
|
this.listeners = /* @__PURE__ */ new Map();
|
|
@@ -4471,10 +4529,17 @@ var ConnectionPool = class {
|
|
|
4471
4529
|
return;
|
|
4472
4530
|
}
|
|
4473
4531
|
}
|
|
4532
|
+
for (const [existingId, existingConn] of this.connections) {
|
|
4533
|
+
if (existingConn.endpoint === endpoint && existingId !== nodeId) {
|
|
4534
|
+
this.remapNodeId(existingId, nodeId);
|
|
4535
|
+
return;
|
|
4536
|
+
}
|
|
4537
|
+
}
|
|
4474
4538
|
const connection = {
|
|
4475
4539
|
nodeId,
|
|
4476
4540
|
endpoint,
|
|
4477
4541
|
socket: null,
|
|
4542
|
+
cachedConnection: null,
|
|
4478
4543
|
state: "DISCONNECTED",
|
|
4479
4544
|
lastSeen: 0,
|
|
4480
4545
|
latencyMs: 0,
|
|
@@ -4509,15 +4574,34 @@ var ConnectionPool = class {
|
|
|
4509
4574
|
}
|
|
4510
4575
|
logger.info({ nodeId }, "Node removed from connection pool");
|
|
4511
4576
|
}
|
|
4577
|
+
/**
|
|
4578
|
+
* Remap a node from one ID to another, preserving the existing connection.
|
|
4579
|
+
* Used when the server-assigned node ID differs from the temporary seed ID.
|
|
4580
|
+
*/
|
|
4581
|
+
remapNodeId(oldId, newId) {
|
|
4582
|
+
const connection = this.connections.get(oldId);
|
|
4583
|
+
if (!connection) return;
|
|
4584
|
+
connection.nodeId = newId;
|
|
4585
|
+
this.connections.delete(oldId);
|
|
4586
|
+
this.connections.set(newId, connection);
|
|
4587
|
+
if (this.primaryNodeId === oldId) {
|
|
4588
|
+
this.primaryNodeId = newId;
|
|
4589
|
+
}
|
|
4590
|
+
logger.info({ oldId, newId }, "Node ID remapped");
|
|
4591
|
+
this.emit("node:remapped", oldId, newId);
|
|
4592
|
+
}
|
|
4512
4593
|
/**
|
|
4513
4594
|
* Get connection for a specific node
|
|
4514
4595
|
*/
|
|
4515
4596
|
getConnection(nodeId) {
|
|
4516
4597
|
const connection = this.connections.get(nodeId);
|
|
4517
|
-
if (!connection || connection.state !== "AUTHENTICATED") {
|
|
4598
|
+
if (!connection || connection.state !== "CONNECTED" && connection.state !== "AUTHENTICATED" || !connection.socket) {
|
|
4518
4599
|
return null;
|
|
4519
4600
|
}
|
|
4520
|
-
|
|
4601
|
+
if (!connection.cachedConnection) {
|
|
4602
|
+
connection.cachedConnection = new WebSocketConnection(connection.socket);
|
|
4603
|
+
}
|
|
4604
|
+
return connection.cachedConnection;
|
|
4521
4605
|
}
|
|
4522
4606
|
/**
|
|
4523
4607
|
* Get primary connection (first/seed node)
|
|
@@ -4531,8 +4615,11 @@ var ConnectionPool = class {
|
|
|
4531
4615
|
*/
|
|
4532
4616
|
getAnyHealthyConnection() {
|
|
4533
4617
|
for (const [nodeId, conn] of this.connections) {
|
|
4534
|
-
if (conn.state === "AUTHENTICATED" && conn.socket) {
|
|
4535
|
-
|
|
4618
|
+
if ((conn.state === "CONNECTED" || conn.state === "AUTHENTICATED") && conn.socket) {
|
|
4619
|
+
if (!conn.cachedConnection) {
|
|
4620
|
+
conn.cachedConnection = new WebSocketConnection(conn.socket);
|
|
4621
|
+
}
|
|
4622
|
+
return { nodeId, connection: conn.cachedConnection };
|
|
4536
4623
|
}
|
|
4537
4624
|
}
|
|
4538
4625
|
return null;
|
|
@@ -4588,7 +4675,7 @@ var ConnectionPool = class {
|
|
|
4588
4675
|
* Get list of connected node IDs
|
|
4589
4676
|
*/
|
|
4590
4677
|
getConnectedNodes() {
|
|
4591
|
-
return Array.from(this.connections.entries()).filter(([_, conn]) => conn.state === "AUTHENTICATED").map(([nodeId]) => nodeId);
|
|
4678
|
+
return Array.from(this.connections.entries()).filter(([_, conn]) => conn.state === "CONNECTED" || conn.state === "AUTHENTICATED").map(([nodeId]) => nodeId);
|
|
4592
4679
|
}
|
|
4593
4680
|
/**
|
|
4594
4681
|
* Get all node IDs
|
|
@@ -4597,11 +4684,11 @@ var ConnectionPool = class {
|
|
|
4597
4684
|
return Array.from(this.connections.keys());
|
|
4598
4685
|
}
|
|
4599
4686
|
/**
|
|
4600
|
-
* Check if node
|
|
4687
|
+
* Check if node has an open WebSocket connection
|
|
4601
4688
|
*/
|
|
4602
4689
|
isNodeConnected(nodeId) {
|
|
4603
4690
|
const conn = this.connections.get(nodeId);
|
|
4604
|
-
return conn?.state === "AUTHENTICATED";
|
|
4691
|
+
return conn?.state === "CONNECTED" || conn?.state === "AUTHENTICATED";
|
|
4605
4692
|
}
|
|
4606
4693
|
/**
|
|
4607
4694
|
* Check if connected to a specific node.
|
|
@@ -4666,25 +4753,26 @@ var ConnectionPool = class {
|
|
|
4666
4753
|
};
|
|
4667
4754
|
socket.onmessage = (event) => {
|
|
4668
4755
|
connection.lastSeen = Date.now();
|
|
4669
|
-
this.handleMessage(nodeId, event);
|
|
4756
|
+
this.handleMessage(connection.nodeId, event);
|
|
4670
4757
|
};
|
|
4671
4758
|
socket.onerror = (error) => {
|
|
4672
|
-
logger.error({ nodeId, error }, "WebSocket error");
|
|
4673
|
-
this.emit("error", nodeId, error instanceof Error ? error : new Error("WebSocket error"));
|
|
4759
|
+
logger.error({ nodeId: connection.nodeId, error }, "WebSocket error");
|
|
4760
|
+
this.emit("error", connection.nodeId, error instanceof Error ? error : new Error("WebSocket error"));
|
|
4674
4761
|
};
|
|
4675
4762
|
socket.onclose = () => {
|
|
4676
4763
|
const wasConnected = connection.state === "AUTHENTICATED";
|
|
4677
4764
|
connection.state = "DISCONNECTED";
|
|
4678
4765
|
connection.socket = null;
|
|
4766
|
+
connection.cachedConnection = null;
|
|
4679
4767
|
if (wasConnected) {
|
|
4680
|
-
this.emit("node:disconnected", nodeId, "Connection closed");
|
|
4768
|
+
this.emit("node:disconnected", connection.nodeId, "Connection closed");
|
|
4681
4769
|
}
|
|
4682
|
-
this.scheduleReconnect(nodeId);
|
|
4770
|
+
this.scheduleReconnect(connection.nodeId);
|
|
4683
4771
|
};
|
|
4684
4772
|
} catch (error) {
|
|
4685
4773
|
connection.state = "FAILED";
|
|
4686
|
-
logger.error({ nodeId, error }, "Failed to connect");
|
|
4687
|
-
this.scheduleReconnect(nodeId);
|
|
4774
|
+
logger.error({ nodeId: connection.nodeId, error }, "Failed to connect");
|
|
4775
|
+
this.scheduleReconnect(connection.nodeId);
|
|
4688
4776
|
}
|
|
4689
4777
|
}
|
|
4690
4778
|
sendAuth(connection) {
|
|
@@ -4713,18 +4801,15 @@ var ConnectionPool = class {
|
|
|
4713
4801
|
logger.info({ nodeId }, "Authenticated with node");
|
|
4714
4802
|
this.emit("node:healthy", nodeId);
|
|
4715
4803
|
this.flushPendingMessages(connection);
|
|
4716
|
-
return;
|
|
4717
4804
|
}
|
|
4718
4805
|
if (message.type === "AUTH_REQUIRED") {
|
|
4719
4806
|
if (this.authToken) {
|
|
4720
4807
|
this.sendAuth(connection);
|
|
4721
4808
|
}
|
|
4722
|
-
return;
|
|
4723
4809
|
}
|
|
4724
4810
|
if (message.type === "AUTH_FAIL") {
|
|
4725
4811
|
logger.error({ nodeId, error: message.error }, "Authentication failed");
|
|
4726
4812
|
connection.state = "FAILED";
|
|
4727
|
-
return;
|
|
4728
4813
|
}
|
|
4729
4814
|
if (message.type === "PONG") {
|
|
4730
4815
|
if (message.timestamp) {
|
|
@@ -4732,10 +4817,6 @@ var ConnectionPool = class {
|
|
|
4732
4817
|
}
|
|
4733
4818
|
return;
|
|
4734
4819
|
}
|
|
4735
|
-
if (message.type === "PARTITION_MAP" || message.type === "PARTITION_MAP_DELTA") {
|
|
4736
|
-
this.emit("message", nodeId, message);
|
|
4737
|
-
return;
|
|
4738
|
-
}
|
|
4739
4820
|
this.emit("message", nodeId, message);
|
|
4740
4821
|
}
|
|
4741
4822
|
flushPendingMessages(connection) {
|
|
@@ -4902,17 +4983,17 @@ var PartitionRouter = class {
|
|
|
4902
4983
|
}
|
|
4903
4984
|
return null;
|
|
4904
4985
|
}
|
|
4905
|
-
const
|
|
4906
|
-
if (
|
|
4907
|
-
return { nodeId: routing.nodeId,
|
|
4986
|
+
const connection = this.connectionPool.getConnection(routing.nodeId);
|
|
4987
|
+
if (connection) {
|
|
4988
|
+
return { nodeId: routing.nodeId, connection };
|
|
4908
4989
|
}
|
|
4909
4990
|
const partition = this.partitionMap.partitions.find((p) => p.partitionId === routing.partitionId);
|
|
4910
4991
|
if (partition) {
|
|
4911
4992
|
for (const backupId of partition.backupNodeIds) {
|
|
4912
|
-
const
|
|
4913
|
-
if (
|
|
4993
|
+
const backupConnection = this.connectionPool.getConnection(backupId);
|
|
4994
|
+
if (backupConnection) {
|
|
4914
4995
|
logger.debug({ key, owner: routing.nodeId, backup: backupId }, "Using backup node");
|
|
4915
|
-
return { nodeId: backupId,
|
|
4996
|
+
return { nodeId: backupId, connection: backupConnection };
|
|
4916
4997
|
}
|
|
4917
4998
|
}
|
|
4918
4999
|
}
|
|
@@ -5188,7 +5269,7 @@ var PartitionRouter = class {
|
|
|
5188
5269
|
};
|
|
5189
5270
|
|
|
5190
5271
|
// src/cluster/ClusterClient.ts
|
|
5191
|
-
var
|
|
5272
|
+
var _ClusterClient = class _ClusterClient {
|
|
5192
5273
|
constructor(config) {
|
|
5193
5274
|
this.listeners = /* @__PURE__ */ new Map();
|
|
5194
5275
|
this.initialized = false;
|
|
@@ -5201,6 +5282,8 @@ var ClusterClient = class {
|
|
|
5201
5282
|
};
|
|
5202
5283
|
// Circuit breaker state per node
|
|
5203
5284
|
this.circuits = /* @__PURE__ */ new Map();
|
|
5285
|
+
// Debounce timer for partition map requests on reconnect
|
|
5286
|
+
this.partitionMapRequestTimer = null;
|
|
5204
5287
|
this.config = config;
|
|
5205
5288
|
this.circuitBreakerConfig = {
|
|
5206
5289
|
...import_core10.DEFAULT_CIRCUIT_BREAKER_CONFIG,
|
|
@@ -5292,14 +5375,14 @@ var ClusterClient = class {
|
|
|
5292
5375
|
this.requestPartitionMapRefresh();
|
|
5293
5376
|
return this.getFallbackConnection();
|
|
5294
5377
|
}
|
|
5295
|
-
const
|
|
5296
|
-
if (!
|
|
5378
|
+
const connection = this.connectionPool.getConnection(owner);
|
|
5379
|
+
if (!connection) {
|
|
5297
5380
|
this.routingMetrics.fallbackRoutes++;
|
|
5298
5381
|
logger.debug({ key, owner }, "Could not get connection to owner, using fallback");
|
|
5299
5382
|
return this.getFallbackConnection();
|
|
5300
5383
|
}
|
|
5301
5384
|
this.routingMetrics.directRoutes++;
|
|
5302
|
-
return
|
|
5385
|
+
return connection;
|
|
5303
5386
|
}
|
|
5304
5387
|
/**
|
|
5305
5388
|
* Get fallback connection when owner is unavailable.
|
|
@@ -5307,10 +5390,10 @@ var ClusterClient = class {
|
|
|
5307
5390
|
*/
|
|
5308
5391
|
getFallbackConnection() {
|
|
5309
5392
|
const conn = this.connectionPool.getAnyHealthyConnection();
|
|
5310
|
-
if (!conn?.
|
|
5393
|
+
if (!conn?.connection) {
|
|
5311
5394
|
throw new Error("No healthy connection available");
|
|
5312
5395
|
}
|
|
5313
|
-
return conn.
|
|
5396
|
+
return conn.connection;
|
|
5314
5397
|
}
|
|
5315
5398
|
/**
|
|
5316
5399
|
* Request a partition map refresh in the background.
|
|
@@ -5321,9 +5404,23 @@ var ClusterClient = class {
|
|
|
5321
5404
|
logger.error({ err }, "Failed to refresh partition map");
|
|
5322
5405
|
});
|
|
5323
5406
|
}
|
|
5407
|
+
/**
|
|
5408
|
+
* Debounce partition map requests to prevent flooding when multiple nodes
|
|
5409
|
+
* reconnect simultaneously. Coalesces rapid requests into a single request
|
|
5410
|
+
* sent to the most recently connected node.
|
|
5411
|
+
*/
|
|
5412
|
+
debouncedPartitionMapRequest(nodeId) {
|
|
5413
|
+
if (this.partitionMapRequestTimer) {
|
|
5414
|
+
clearTimeout(this.partitionMapRequestTimer);
|
|
5415
|
+
}
|
|
5416
|
+
this.partitionMapRequestTimer = setTimeout(() => {
|
|
5417
|
+
this.partitionMapRequestTimer = null;
|
|
5418
|
+
this.requestPartitionMapFromNode(nodeId);
|
|
5419
|
+
}, _ClusterClient.PARTITION_MAP_REQUEST_DEBOUNCE_MS);
|
|
5420
|
+
}
|
|
5324
5421
|
/**
|
|
5325
5422
|
* Request partition map from a specific node.
|
|
5326
|
-
* Called on
|
|
5423
|
+
* Called on node connection via debounced handler.
|
|
5327
5424
|
*/
|
|
5328
5425
|
requestPartitionMapFromNode(nodeId) {
|
|
5329
5426
|
const socket = this.connectionPool.getConnection(nodeId);
|
|
@@ -5492,8 +5589,8 @@ var ClusterClient = class {
|
|
|
5492
5589
|
* Send directly to partition owner
|
|
5493
5590
|
*/
|
|
5494
5591
|
sendDirect(key, message) {
|
|
5495
|
-
const
|
|
5496
|
-
if (!
|
|
5592
|
+
const route = this.partitionRouter.routeToConnection(key);
|
|
5593
|
+
if (!route) {
|
|
5497
5594
|
logger.warn({ key }, "No route available for key");
|
|
5498
5595
|
return false;
|
|
5499
5596
|
}
|
|
@@ -5504,7 +5601,7 @@ var ClusterClient = class {
|
|
|
5504
5601
|
mapVersion: this.partitionRouter.getMapVersion()
|
|
5505
5602
|
}
|
|
5506
5603
|
};
|
|
5507
|
-
connection.
|
|
5604
|
+
route.connection.send((0, import_core10.serialize)(routedMessage));
|
|
5508
5605
|
return true;
|
|
5509
5606
|
}
|
|
5510
5607
|
/**
|
|
@@ -5608,10 +5705,23 @@ var ClusterClient = class {
|
|
|
5608
5705
|
async refreshPartitionMap() {
|
|
5609
5706
|
await this.partitionRouter.refreshPartitionMap();
|
|
5610
5707
|
}
|
|
5708
|
+
/**
|
|
5709
|
+
* Force reconnect all connections in the pool.
|
|
5710
|
+
*/
|
|
5711
|
+
forceReconnect() {
|
|
5712
|
+
this.connectionPool.close();
|
|
5713
|
+
this.connect().catch((err) => {
|
|
5714
|
+
logger.error({ err }, "ClusterClient forceReconnect failed");
|
|
5715
|
+
});
|
|
5716
|
+
}
|
|
5611
5717
|
/**
|
|
5612
5718
|
* Shutdown cluster client (IConnectionProvider interface).
|
|
5613
5719
|
*/
|
|
5614
5720
|
async close() {
|
|
5721
|
+
if (this.partitionMapRequestTimer) {
|
|
5722
|
+
clearTimeout(this.partitionMapRequestTimer);
|
|
5723
|
+
this.partitionMapRequestTimer = null;
|
|
5724
|
+
}
|
|
5615
5725
|
this.partitionRouter.close();
|
|
5616
5726
|
this.connectionPool.close();
|
|
5617
5727
|
this.initialized = false;
|
|
@@ -5634,23 +5744,23 @@ var ClusterClient = class {
|
|
|
5634
5744
|
return this.partitionRouter;
|
|
5635
5745
|
}
|
|
5636
5746
|
/**
|
|
5637
|
-
* Get any healthy
|
|
5747
|
+
* Get any healthy connection (IConnectionProvider interface).
|
|
5638
5748
|
* @throws Error if not connected
|
|
5639
5749
|
*/
|
|
5640
5750
|
getAnyConnection() {
|
|
5641
5751
|
const conn = this.connectionPool.getAnyHealthyConnection();
|
|
5642
|
-
if (!conn?.
|
|
5752
|
+
if (!conn?.connection) {
|
|
5643
5753
|
throw new Error("No healthy connection available");
|
|
5644
5754
|
}
|
|
5645
|
-
return conn.
|
|
5755
|
+
return conn.connection;
|
|
5646
5756
|
}
|
|
5647
5757
|
/**
|
|
5648
|
-
* Get any healthy
|
|
5758
|
+
* Get any healthy connection, or null if none available.
|
|
5649
5759
|
* Use this for optional connection checks.
|
|
5650
5760
|
*/
|
|
5651
5761
|
getAnyConnectionOrNull() {
|
|
5652
5762
|
const conn = this.connectionPool.getAnyHealthyConnection();
|
|
5653
|
-
return conn?.
|
|
5763
|
+
return conn?.connection ?? null;
|
|
5654
5764
|
}
|
|
5655
5765
|
// ============================================
|
|
5656
5766
|
// Circuit Breaker Methods
|
|
@@ -5741,9 +5851,7 @@ var ClusterClient = class {
|
|
|
5741
5851
|
setupEventHandlers() {
|
|
5742
5852
|
this.connectionPool.on("node:connected", (nodeId) => {
|
|
5743
5853
|
logger.debug({ nodeId }, "Node connected");
|
|
5744
|
-
|
|
5745
|
-
this.requestPartitionMapFromNode(nodeId);
|
|
5746
|
-
}
|
|
5854
|
+
this.debouncedPartitionMapRequest(nodeId);
|
|
5747
5855
|
if (this.connectionPool.getConnectedNodes().length === 1) {
|
|
5748
5856
|
this.emit("connected");
|
|
5749
5857
|
}
|
|
@@ -5798,6 +5906,8 @@ var ClusterClient = class {
|
|
|
5798
5906
|
});
|
|
5799
5907
|
}
|
|
5800
5908
|
};
|
|
5909
|
+
_ClusterClient.PARTITION_MAP_REQUEST_DEBOUNCE_MS = 500;
|
|
5910
|
+
var ClusterClient = _ClusterClient;
|
|
5801
5911
|
|
|
5802
5912
|
// src/connection/SingleServerProvider.ts
|
|
5803
5913
|
var DEFAULT_CONFIG = {
|
|
@@ -5813,14 +5923,18 @@ var SingleServerProvider = class {
|
|
|
5813
5923
|
this.reconnectTimer = null;
|
|
5814
5924
|
this.isClosing = false;
|
|
5815
5925
|
this.listeners = /* @__PURE__ */ new Map();
|
|
5926
|
+
this.onlineHandler = null;
|
|
5927
|
+
this.offlineHandler = null;
|
|
5816
5928
|
this.url = config.url;
|
|
5817
5929
|
this.config = {
|
|
5818
5930
|
url: config.url,
|
|
5819
5931
|
maxReconnectAttempts: config.maxReconnectAttempts ?? DEFAULT_CONFIG.maxReconnectAttempts,
|
|
5820
5932
|
reconnectDelayMs: config.reconnectDelayMs ?? DEFAULT_CONFIG.reconnectDelayMs,
|
|
5821
5933
|
backoffMultiplier: config.backoffMultiplier ?? DEFAULT_CONFIG.backoffMultiplier,
|
|
5822
|
-
maxReconnectDelayMs: config.maxReconnectDelayMs ?? DEFAULT_CONFIG.maxReconnectDelayMs
|
|
5934
|
+
maxReconnectDelayMs: config.maxReconnectDelayMs ?? DEFAULT_CONFIG.maxReconnectDelayMs,
|
|
5935
|
+
listenNetworkEvents: config.listenNetworkEvents ?? true
|
|
5823
5936
|
};
|
|
5937
|
+
this.setupNetworkListeners();
|
|
5824
5938
|
}
|
|
5825
5939
|
/**
|
|
5826
5940
|
* Connect to the WebSocket server.
|
|
@@ -5829,6 +5943,9 @@ var SingleServerProvider = class {
|
|
|
5829
5943
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
5830
5944
|
return;
|
|
5831
5945
|
}
|
|
5946
|
+
if (typeof globalThis.navigator !== "undefined" && globalThis.navigator.onLine === false) {
|
|
5947
|
+
throw new Error("Browser is offline \u2014 skipping connection attempt");
|
|
5948
|
+
}
|
|
5832
5949
|
this.isClosing = false;
|
|
5833
5950
|
return new Promise((resolve, reject) => {
|
|
5834
5951
|
try {
|
|
@@ -5881,7 +5998,7 @@ var SingleServerProvider = class {
|
|
|
5881
5998
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
5882
5999
|
throw new Error("Not connected");
|
|
5883
6000
|
}
|
|
5884
|
-
return this.ws;
|
|
6001
|
+
return new WebSocketConnection(this.ws);
|
|
5885
6002
|
}
|
|
5886
6003
|
/**
|
|
5887
6004
|
* Get any available connection.
|
|
@@ -5932,6 +6049,7 @@ var SingleServerProvider = class {
|
|
|
5932
6049
|
*/
|
|
5933
6050
|
async close() {
|
|
5934
6051
|
this.isClosing = true;
|
|
6052
|
+
this.teardownNetworkListeners();
|
|
5935
6053
|
if (this.reconnectTimer) {
|
|
5936
6054
|
clearTimeout(this.reconnectTimer);
|
|
5937
6055
|
this.reconnectTimer = null;
|
|
@@ -5970,6 +6088,10 @@ var SingleServerProvider = class {
|
|
|
5970
6088
|
clearTimeout(this.reconnectTimer);
|
|
5971
6089
|
this.reconnectTimer = null;
|
|
5972
6090
|
}
|
|
6091
|
+
if (typeof globalThis.navigator !== "undefined" && globalThis.navigator.onLine === false && this.config.listenNetworkEvents) {
|
|
6092
|
+
logger.info({ url: this.url }, "Browser offline \u2014 waiting for online event instead of polling");
|
|
6093
|
+
return;
|
|
6094
|
+
}
|
|
5973
6095
|
if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
|
|
5974
6096
|
logger.error(
|
|
5975
6097
|
{ attempts: this.reconnectAttempts, url: this.url },
|
|
@@ -6005,6 +6127,37 @@ var SingleServerProvider = class {
|
|
|
6005
6127
|
delay = delay * (0.5 + Math.random());
|
|
6006
6128
|
return Math.floor(delay);
|
|
6007
6129
|
}
|
|
6130
|
+
/**
|
|
6131
|
+
* Force-close the current WebSocket and immediately schedule reconnection.
|
|
6132
|
+
* Unlike close(), this does NOT set isClosing and preserves reconnect behavior.
|
|
6133
|
+
* Resets the reconnect counter so the full backoff budget is available.
|
|
6134
|
+
*
|
|
6135
|
+
* Critically, this does NOT wait for the TCP close handshake (which can
|
|
6136
|
+
* hang 20+ seconds on a dead network). Instead it strips all handlers from
|
|
6137
|
+
* the old WebSocket, fires a best-effort close(), nulls the reference, and
|
|
6138
|
+
* schedules reconnect right away.
|
|
6139
|
+
*/
|
|
6140
|
+
forceReconnect() {
|
|
6141
|
+
this.reconnectAttempts = 0;
|
|
6142
|
+
this.isClosing = false;
|
|
6143
|
+
if (this.reconnectTimer) {
|
|
6144
|
+
clearTimeout(this.reconnectTimer);
|
|
6145
|
+
this.reconnectTimer = null;
|
|
6146
|
+
}
|
|
6147
|
+
if (this.ws) {
|
|
6148
|
+
this.ws.onopen = null;
|
|
6149
|
+
this.ws.onclose = null;
|
|
6150
|
+
this.ws.onerror = null;
|
|
6151
|
+
this.ws.onmessage = null;
|
|
6152
|
+
try {
|
|
6153
|
+
this.ws.close();
|
|
6154
|
+
} catch {
|
|
6155
|
+
}
|
|
6156
|
+
this.ws = null;
|
|
6157
|
+
}
|
|
6158
|
+
this.emit("disconnected", "default");
|
|
6159
|
+
this.scheduleReconnect();
|
|
6160
|
+
}
|
|
6008
6161
|
/**
|
|
6009
6162
|
* Get the WebSocket URL this provider connects to.
|
|
6010
6163
|
*/
|
|
@@ -6024,6 +6177,43 @@ var SingleServerProvider = class {
|
|
|
6024
6177
|
resetReconnectAttempts() {
|
|
6025
6178
|
this.reconnectAttempts = 0;
|
|
6026
6179
|
}
|
|
6180
|
+
/**
|
|
6181
|
+
* Listen for browser 'online' event to trigger instant reconnect
|
|
6182
|
+
* when network comes back. Only active in browser environments.
|
|
6183
|
+
*/
|
|
6184
|
+
setupNetworkListeners() {
|
|
6185
|
+
if (!this.config.listenNetworkEvents) return;
|
|
6186
|
+
if (typeof globalThis.addEventListener !== "function") return;
|
|
6187
|
+
this.onlineHandler = () => {
|
|
6188
|
+
if (this.isClosing) return;
|
|
6189
|
+
if (this.isConnected()) return;
|
|
6190
|
+
logger.info({ url: this.url }, "Network online detected \u2014 forcing reconnect");
|
|
6191
|
+
this.forceReconnect();
|
|
6192
|
+
};
|
|
6193
|
+
this.offlineHandler = () => {
|
|
6194
|
+
if (this.isClosing) return;
|
|
6195
|
+
if (!this.isConnected()) return;
|
|
6196
|
+
logger.info({ url: this.url }, "Network offline detected \u2014 disconnecting immediately");
|
|
6197
|
+
this.forceReconnect();
|
|
6198
|
+
};
|
|
6199
|
+
globalThis.addEventListener("online", this.onlineHandler);
|
|
6200
|
+
globalThis.addEventListener("offline", this.offlineHandler);
|
|
6201
|
+
}
|
|
6202
|
+
/**
|
|
6203
|
+
* Remove browser network event listeners.
|
|
6204
|
+
*/
|
|
6205
|
+
teardownNetworkListeners() {
|
|
6206
|
+
if (typeof globalThis.removeEventListener === "function") {
|
|
6207
|
+
if (this.onlineHandler) {
|
|
6208
|
+
globalThis.removeEventListener("online", this.onlineHandler);
|
|
6209
|
+
this.onlineHandler = null;
|
|
6210
|
+
}
|
|
6211
|
+
if (this.offlineHandler) {
|
|
6212
|
+
globalThis.removeEventListener("offline", this.offlineHandler);
|
|
6213
|
+
this.offlineHandler = null;
|
|
6214
|
+
}
|
|
6215
|
+
}
|
|
6216
|
+
}
|
|
6027
6217
|
};
|
|
6028
6218
|
|
|
6029
6219
|
// src/TopGunClient.ts
|
|
@@ -6081,7 +6271,13 @@ var TopGunClient = class {
|
|
|
6081
6271
|
});
|
|
6082
6272
|
logger.info({ seeds: this.clusterConfig.seeds }, "TopGunClient initialized in cluster mode");
|
|
6083
6273
|
} else {
|
|
6084
|
-
const singleServerProvider = new SingleServerProvider({
|
|
6274
|
+
const singleServerProvider = new SingleServerProvider({
|
|
6275
|
+
url: config.serverUrl,
|
|
6276
|
+
maxReconnectAttempts: config.backoff?.maxRetries,
|
|
6277
|
+
reconnectDelayMs: config.backoff?.initialDelayMs,
|
|
6278
|
+
backoffMultiplier: config.backoff?.multiplier,
|
|
6279
|
+
maxReconnectDelayMs: config.backoff?.maxDelayMs
|
|
6280
|
+
});
|
|
6085
6281
|
this.syncEngine = new SyncEngine({
|
|
6086
6282
|
nodeId: this.nodeId,
|
|
6087
6283
|
connectionProvider: singleServerProvider,
|
|
@@ -7131,6 +7327,24 @@ var import_core14 = require("@topgunbuild/core");
|
|
|
7131
7327
|
|
|
7132
7328
|
// src/connection/HttpSyncProvider.ts
|
|
7133
7329
|
var import_core13 = require("@topgunbuild/core");
|
|
7330
|
+
var HttpConnection = class {
|
|
7331
|
+
constructor(provider) {
|
|
7332
|
+
this.provider = provider;
|
|
7333
|
+
}
|
|
7334
|
+
send(data) {
|
|
7335
|
+
if (typeof data === "string") {
|
|
7336
|
+
const encoder = new TextEncoder();
|
|
7337
|
+
this.provider.send(encoder.encode(data));
|
|
7338
|
+
} else {
|
|
7339
|
+
this.provider.send(data instanceof ArrayBuffer ? new Uint8Array(data) : data);
|
|
7340
|
+
}
|
|
7341
|
+
}
|
|
7342
|
+
close() {
|
|
7343
|
+
}
|
|
7344
|
+
get readyState() {
|
|
7345
|
+
return this.provider.isConnected() ? ConnectionReadyState.OPEN : ConnectionReadyState.CLOSED;
|
|
7346
|
+
}
|
|
7347
|
+
};
|
|
7134
7348
|
var HttpSyncProvider = class {
|
|
7135
7349
|
constructor(config) {
|
|
7136
7350
|
this.listeners = /* @__PURE__ */ new Map();
|
|
@@ -7172,17 +7386,17 @@ var HttpSyncProvider = class {
|
|
|
7172
7386
|
}
|
|
7173
7387
|
/**
|
|
7174
7388
|
* Get connection for a specific key.
|
|
7175
|
-
*
|
|
7389
|
+
* Returns an HttpConnection that queues operations for the next poll cycle.
|
|
7176
7390
|
*/
|
|
7177
7391
|
getConnection(_key) {
|
|
7178
|
-
|
|
7392
|
+
return new HttpConnection(this);
|
|
7179
7393
|
}
|
|
7180
7394
|
/**
|
|
7181
7395
|
* Get any available connection.
|
|
7182
|
-
*
|
|
7396
|
+
* Returns an HttpConnection that queues operations for the next poll cycle.
|
|
7183
7397
|
*/
|
|
7184
7398
|
getAnyConnection() {
|
|
7185
|
-
|
|
7399
|
+
return new HttpConnection(this);
|
|
7186
7400
|
}
|
|
7187
7401
|
/**
|
|
7188
7402
|
* Check if connected (last HTTP request succeeded).
|
|
@@ -7263,6 +7477,17 @@ var HttpSyncProvider = class {
|
|
|
7263
7477
|
logger.warn({ err }, "HTTP sync provider: failed to deserialize message");
|
|
7264
7478
|
}
|
|
7265
7479
|
}
|
|
7480
|
+
/**
|
|
7481
|
+
* Force reconnect by restarting the polling loop.
|
|
7482
|
+
*/
|
|
7483
|
+
forceReconnect() {
|
|
7484
|
+
this.stopPolling();
|
|
7485
|
+
this.connected = false;
|
|
7486
|
+
this.emit("disconnected", "default");
|
|
7487
|
+
this.connect().catch((err) => {
|
|
7488
|
+
logger.error({ err }, "HttpSyncProvider forceReconnect failed");
|
|
7489
|
+
});
|
|
7490
|
+
}
|
|
7266
7491
|
/**
|
|
7267
7492
|
* Close the HTTP sync provider.
|
|
7268
7493
|
* Stops the polling loop, clears queued operations, and sets disconnected state.
|
|
@@ -7535,6 +7760,14 @@ var AutoConnectionProvider = class {
|
|
|
7535
7760
|
}
|
|
7536
7761
|
this.activeProvider.send(data, key);
|
|
7537
7762
|
}
|
|
7763
|
+
/**
|
|
7764
|
+
* Force reconnect by delegating to the active provider.
|
|
7765
|
+
*/
|
|
7766
|
+
forceReconnect() {
|
|
7767
|
+
if (this.activeProvider) {
|
|
7768
|
+
this.activeProvider.forceReconnect();
|
|
7769
|
+
}
|
|
7770
|
+
}
|
|
7538
7771
|
/**
|
|
7539
7772
|
* Close the active underlying provider.
|
|
7540
7773
|
*/
|
|
@@ -7592,6 +7825,7 @@ var AutoConnectionProvider = class {
|
|
|
7592
7825
|
ClusterClient,
|
|
7593
7826
|
ConflictResolverClient,
|
|
7594
7827
|
ConnectionPool,
|
|
7828
|
+
ConnectionReadyState,
|
|
7595
7829
|
DEFAULT_BACKPRESSURE_CONFIG,
|
|
7596
7830
|
DEFAULT_CLUSTER_CONFIG,
|
|
7597
7831
|
EncryptedStorageAdapter,
|
|
@@ -7613,6 +7847,7 @@ var AutoConnectionProvider = class {
|
|
|
7613
7847
|
TopGunClient,
|
|
7614
7848
|
TopicHandle,
|
|
7615
7849
|
VALID_TRANSITIONS,
|
|
7850
|
+
WebSocketConnection,
|
|
7616
7851
|
isValidTransition,
|
|
7617
7852
|
logger
|
|
7618
7853
|
});
|