@topgunbuild/client 0.10.1 → 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 +329 -25
- package/dist/index.d.ts +329 -25
- package/dist/index.js +800 -105
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +794 -103
- package/dist/index.mjs.map +1 -1
- package/package.json +14 -9
- package/LICENSE +0 -97
package/dist/index.js
CHANGED
|
@@ -30,21 +30,24 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
AutoConnectionProvider: () => AutoConnectionProvider,
|
|
33
34
|
BackpressureError: () => BackpressureError,
|
|
34
35
|
ChangeTracker: () => ChangeTracker,
|
|
35
36
|
ClusterClient: () => ClusterClient,
|
|
36
37
|
ConflictResolverClient: () => ConflictResolverClient,
|
|
37
38
|
ConnectionPool: () => ConnectionPool,
|
|
39
|
+
ConnectionReadyState: () => ConnectionReadyState,
|
|
38
40
|
DEFAULT_BACKPRESSURE_CONFIG: () => DEFAULT_BACKPRESSURE_CONFIG,
|
|
39
41
|
DEFAULT_CLUSTER_CONFIG: () => DEFAULT_CLUSTER_CONFIG,
|
|
40
42
|
EncryptedStorageAdapter: () => EncryptedStorageAdapter,
|
|
41
43
|
EventJournalReader: () => EventJournalReader,
|
|
44
|
+
HttpSyncProvider: () => HttpSyncProvider,
|
|
42
45
|
HybridQueryHandle: () => HybridQueryHandle,
|
|
43
46
|
IDBAdapter: () => IDBAdapter,
|
|
44
|
-
LWWMap: () =>
|
|
47
|
+
LWWMap: () => import_core14.LWWMap,
|
|
45
48
|
PNCounterHandle: () => PNCounterHandle,
|
|
46
49
|
PartitionRouter: () => PartitionRouter,
|
|
47
|
-
Predicates: () =>
|
|
50
|
+
Predicates: () => import_core14.Predicates,
|
|
48
51
|
QueryHandle: () => QueryHandle,
|
|
49
52
|
SearchHandle: () => SearchHandle,
|
|
50
53
|
SingleServerProvider: () => SingleServerProvider,
|
|
@@ -55,6 +58,7 @@ __export(index_exports, {
|
|
|
55
58
|
TopGunClient: () => TopGunClient,
|
|
56
59
|
TopicHandle: () => TopicHandle,
|
|
57
60
|
VALID_TRANSITIONS: () => VALID_TRANSITIONS,
|
|
61
|
+
WebSocketConnection: () => WebSocketConnection,
|
|
58
62
|
isValidTransition: () => isValidTransition,
|
|
59
63
|
logger: () => logger
|
|
60
64
|
});
|
|
@@ -718,9 +722,7 @@ var WebSocketManager = class {
|
|
|
718
722
|
timeoutMs: this.config.heartbeatConfig.timeoutMs
|
|
719
723
|
}, "Heartbeat timeout - triggering reconnection");
|
|
720
724
|
this.stopHeartbeat();
|
|
721
|
-
this.connectionProvider.
|
|
722
|
-
logger.error({ err }, "Error closing ConnectionProvider on heartbeat timeout");
|
|
723
|
-
});
|
|
725
|
+
this.connectionProvider.forceReconnect();
|
|
724
726
|
}
|
|
725
727
|
}
|
|
726
728
|
/**
|
|
@@ -1407,7 +1409,7 @@ var LockManager = class {
|
|
|
1407
1409
|
/**
|
|
1408
1410
|
* Handle lock granted message from server.
|
|
1409
1411
|
*/
|
|
1410
|
-
handleLockGranted(requestId, fencingToken) {
|
|
1412
|
+
handleLockGranted(requestId, _name, fencingToken) {
|
|
1411
1413
|
const req = this.pendingLockRequests.get(requestId);
|
|
1412
1414
|
if (req) {
|
|
1413
1415
|
clearTimeout(req.timer);
|
|
@@ -1418,7 +1420,7 @@ var LockManager = class {
|
|
|
1418
1420
|
/**
|
|
1419
1421
|
* Handle lock released message from server.
|
|
1420
1422
|
*/
|
|
1421
|
-
handleLockReleased(requestId, success) {
|
|
1423
|
+
handleLockReleased(requestId, _name, success) {
|
|
1422
1424
|
const req = this.pendingLockRequests.get(requestId);
|
|
1423
1425
|
if (req) {
|
|
1424
1426
|
clearTimeout(req.timer);
|
|
@@ -1835,6 +1837,8 @@ var import_core3 = require("@topgunbuild/core");
|
|
|
1835
1837
|
var MerkleSyncHandler = class {
|
|
1836
1838
|
constructor(config) {
|
|
1837
1839
|
this.lastSyncTimestamp = 0;
|
|
1840
|
+
/** Accumulated sync stats per map, flushed after a quiet period */
|
|
1841
|
+
this.syncStats = /* @__PURE__ */ new Map();
|
|
1838
1842
|
this.config = config;
|
|
1839
1843
|
}
|
|
1840
1844
|
/**
|
|
@@ -1856,7 +1860,8 @@ var MerkleSyncHandler = class {
|
|
|
1856
1860
|
* Compares root hashes and requests buckets if mismatch detected.
|
|
1857
1861
|
*/
|
|
1858
1862
|
async handleSyncRespRoot(payload) {
|
|
1859
|
-
const { mapName,
|
|
1863
|
+
const { mapName, timestamp } = payload;
|
|
1864
|
+
const rootHash = Number(payload.rootHash);
|
|
1860
1865
|
const map = this.config.getMap(mapName);
|
|
1861
1866
|
if (map instanceof import_core3.LWWMap) {
|
|
1862
1867
|
const localRootHash = map.getMerkleTree().getRootHash();
|
|
@@ -1884,9 +1889,11 @@ var MerkleSyncHandler = class {
|
|
|
1884
1889
|
if (map instanceof import_core3.LWWMap) {
|
|
1885
1890
|
const tree = map.getMerkleTree();
|
|
1886
1891
|
const localBuckets = tree.getBuckets(path);
|
|
1892
|
+
let mismatchCount = 0;
|
|
1887
1893
|
for (const [bucketKey, remoteHash] of Object.entries(buckets)) {
|
|
1888
1894
|
const localHash = localBuckets[bucketKey] || 0;
|
|
1889
1895
|
if (localHash !== remoteHash) {
|
|
1896
|
+
mismatchCount++;
|
|
1890
1897
|
const newPath = path + bucketKey;
|
|
1891
1898
|
this.config.sendMessage({
|
|
1892
1899
|
type: "MERKLE_REQ_BUCKET",
|
|
@@ -1913,7 +1920,17 @@ var MerkleSyncHandler = class {
|
|
|
1913
1920
|
}
|
|
1914
1921
|
}
|
|
1915
1922
|
if (updateCount > 0) {
|
|
1916
|
-
|
|
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);
|
|
1917
1934
|
}
|
|
1918
1935
|
}
|
|
1919
1936
|
}
|
|
@@ -2196,6 +2213,8 @@ function registerClientMessageHandlers(router, delegates, managers) {
|
|
|
2196
2213
|
},
|
|
2197
2214
|
// SYNC handlers
|
|
2198
2215
|
"OP_ACK": (msg) => delegates.handleOpAck(msg),
|
|
2216
|
+
"OP_REJECTED": (msg) => delegates.handleOpRejected(msg),
|
|
2217
|
+
"ERROR": (msg) => delegates.handleError(msg),
|
|
2199
2218
|
"SYNC_RESP_ROOT": (msg) => managers.merkleSyncHandler.handleSyncRespRoot(msg.payload),
|
|
2200
2219
|
"SYNC_RESP_BUCKETS": (msg) => managers.merkleSyncHandler.handleSyncRespBuckets(msg.payload),
|
|
2201
2220
|
"SYNC_RESP_LEAF": (msg) => managers.merkleSyncHandler.handleSyncRespLeaf(msg.payload),
|
|
@@ -2218,12 +2237,12 @@ function registerClientMessageHandlers(router, delegates, managers) {
|
|
|
2218
2237
|
},
|
|
2219
2238
|
// LOCK handlers
|
|
2220
2239
|
"LOCK_GRANTED": (msg) => {
|
|
2221
|
-
const { requestId, fencingToken } = msg.payload;
|
|
2222
|
-
managers.lockManager.handleLockGranted(requestId, fencingToken);
|
|
2240
|
+
const { requestId, name, fencingToken } = msg.payload;
|
|
2241
|
+
managers.lockManager.handleLockGranted(requestId, name, fencingToken);
|
|
2223
2242
|
},
|
|
2224
2243
|
"LOCK_RELEASED": (msg) => {
|
|
2225
|
-
const { requestId, success } = msg.payload;
|
|
2226
|
-
managers.lockManager.handleLockReleased(requestId, success);
|
|
2244
|
+
const { requestId, name, success } = msg.payload;
|
|
2245
|
+
managers.lockManager.handleLockReleased(requestId, name, success);
|
|
2227
2246
|
},
|
|
2228
2247
|
// GC handler
|
|
2229
2248
|
"GC_PRUNE": (msg) => delegates.handleGcPrune(msg),
|
|
@@ -2261,10 +2280,7 @@ function registerClientMessageHandlers(router, delegates, managers) {
|
|
|
2261
2280
|
managers.searchClient.handleSearchResponse(msg.payload);
|
|
2262
2281
|
},
|
|
2263
2282
|
"SEARCH_UPDATE": () => {
|
|
2264
|
-
}
|
|
2265
|
-
// HYBRID handlers
|
|
2266
|
-
"HYBRID_QUERY_RESP": (msg) => delegates.handleHybridQueryResponse(msg.payload),
|
|
2267
|
-
"HYBRID_QUERY_DELTA": (msg) => delegates.handleHybridQueryDelta(msg.payload)
|
|
2283
|
+
}
|
|
2268
2284
|
});
|
|
2269
2285
|
}
|
|
2270
2286
|
|
|
@@ -2399,8 +2415,8 @@ var SyncEngine = class {
|
|
|
2399
2415
|
handleServerEvent: (msg) => this.handleServerEvent(msg),
|
|
2400
2416
|
handleServerBatchEvent: (msg) => this.handleServerBatchEvent(msg),
|
|
2401
2417
|
handleGcPrune: (msg) => this.handleGcPrune(msg),
|
|
2402
|
-
|
|
2403
|
-
|
|
2418
|
+
handleOpRejected: (msg) => this.handleOpRejected(msg),
|
|
2419
|
+
handleError: (msg) => this.handleError(msg)
|
|
2404
2420
|
},
|
|
2405
2421
|
{
|
|
2406
2422
|
topicManager: this.topicManager,
|
|
@@ -2553,6 +2569,15 @@ var SyncEngine = class {
|
|
|
2553
2569
|
const pending = this.opLog.filter((op) => !op.synced);
|
|
2554
2570
|
if (pending.length === 0) return;
|
|
2555
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
|
+
}
|
|
2556
2581
|
this.sendMessage({
|
|
2557
2582
|
type: "OP_BATCH",
|
|
2558
2583
|
payload: {
|
|
@@ -2573,7 +2598,7 @@ var SyncEngine = class {
|
|
|
2573
2598
|
this.authToken = token;
|
|
2574
2599
|
this.tokenProvider = null;
|
|
2575
2600
|
const state = this.stateMachine.getState();
|
|
2576
|
-
if (state === "AUTHENTICATING" /* AUTHENTICATING */
|
|
2601
|
+
if (state === "AUTHENTICATING" /* AUTHENTICATING */) {
|
|
2577
2602
|
this.sendAuth();
|
|
2578
2603
|
} else if (state === "BACKOFF" /* BACKOFF */ || state === "DISCONNECTED" /* DISCONNECTED */) {
|
|
2579
2604
|
logger.info("Auth token set during backoff/disconnect. Reconnecting immediately.");
|
|
@@ -2678,9 +2703,10 @@ var SyncEngine = class {
|
|
|
2678
2703
|
return;
|
|
2679
2704
|
}
|
|
2680
2705
|
await this.messageRouter.route(message);
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
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);
|
|
2684
2710
|
await this.saveOpLog();
|
|
2685
2711
|
}
|
|
2686
2712
|
}
|
|
@@ -2734,20 +2760,37 @@ var SyncEngine = class {
|
|
|
2734
2760
|
this.writeConcernManager.resolveWriteConcernPromise(result.opId, result);
|
|
2735
2761
|
}
|
|
2736
2762
|
}
|
|
2763
|
+
const lastIdNum = parseInt(lastId, 10);
|
|
2737
2764
|
let maxSyncedId = -1;
|
|
2738
2765
|
let ackedCount = 0;
|
|
2739
|
-
|
|
2740
|
-
|
|
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) => {
|
|
2741
2784
|
if (!op.synced) {
|
|
2742
2785
|
ackedCount++;
|
|
2786
|
+
op.synced = true;
|
|
2787
|
+
const opIdNum = parseInt(op.id, 10);
|
|
2788
|
+
if (!isNaN(opIdNum) && opIdNum > maxSyncedId) {
|
|
2789
|
+
maxSyncedId = opIdNum;
|
|
2790
|
+
}
|
|
2743
2791
|
}
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
if (!isNaN(idNum) && idNum > maxSyncedId) {
|
|
2747
|
-
maxSyncedId = idNum;
|
|
2748
|
-
}
|
|
2749
|
-
}
|
|
2750
|
-
});
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2751
2794
|
if (maxSyncedId !== -1) {
|
|
2752
2795
|
this.storageAdapter.markOpsSynced(maxSyncedId).catch((err) => logger.error({ err }, "Failed to mark ops synced"));
|
|
2753
2796
|
}
|
|
@@ -2764,10 +2807,10 @@ var SyncEngine = class {
|
|
|
2764
2807
|
}
|
|
2765
2808
|
}
|
|
2766
2809
|
handleQueryUpdate(message) {
|
|
2767
|
-
const { queryId, key, value,
|
|
2810
|
+
const { queryId, key, value, changeType } = message.payload;
|
|
2768
2811
|
const query = this.queryManager.getQueries().get(queryId);
|
|
2769
2812
|
if (query) {
|
|
2770
|
-
query.onUpdate(key,
|
|
2813
|
+
query.onUpdate(key, changeType === "LEAVE" ? null : value);
|
|
2771
2814
|
}
|
|
2772
2815
|
}
|
|
2773
2816
|
async handleServerEvent(message) {
|
|
@@ -3172,31 +3215,24 @@ var SyncEngine = class {
|
|
|
3172
3215
|
return this.queryManager.runLocalHybridQuery(mapName, filter);
|
|
3173
3216
|
}
|
|
3174
3217
|
/**
|
|
3175
|
-
* Handle
|
|
3218
|
+
* Handle operation rejected by server (permission denied, validation failure, etc.).
|
|
3176
3219
|
*/
|
|
3177
|
-
|
|
3178
|
-
const
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
}
|
|
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
|
+
});
|
|
3187
3229
|
}
|
|
3188
3230
|
/**
|
|
3189
|
-
* Handle
|
|
3231
|
+
* Handle generic error message from server.
|
|
3190
3232
|
*/
|
|
3191
|
-
|
|
3192
|
-
const
|
|
3193
|
-
|
|
3194
|
-
if (payload.type === "LEAVE") {
|
|
3195
|
-
query.onUpdate(payload.key, null);
|
|
3196
|
-
} else {
|
|
3197
|
-
query.onUpdate(payload.key, payload.value, payload.score, payload.matchedTerms);
|
|
3198
|
-
}
|
|
3199
|
-
}
|
|
3233
|
+
handleError(message) {
|
|
3234
|
+
const { code, message: errorMessage, details } = message.payload;
|
|
3235
|
+
logger.error({ code, message: errorMessage, details }, "Server error received");
|
|
3200
3236
|
}
|
|
3201
3237
|
};
|
|
3202
3238
|
|
|
@@ -4080,8 +4116,8 @@ var SearchHandle = class {
|
|
|
4080
4116
|
handleSearchUpdate(message) {
|
|
4081
4117
|
if (message.type !== "SEARCH_UPDATE") return;
|
|
4082
4118
|
if (message.payload?.subscriptionId !== this.subscriptionId) return;
|
|
4083
|
-
const { key, value, score, matchedTerms,
|
|
4084
|
-
switch (
|
|
4119
|
+
const { key, value, score, matchedTerms, changeType } = message.payload;
|
|
4120
|
+
switch (changeType) {
|
|
4085
4121
|
case "ENTER":
|
|
4086
4122
|
this.results.set(key, {
|
|
4087
4123
|
key,
|
|
@@ -4398,6 +4434,30 @@ var import_core10 = require("@topgunbuild/core");
|
|
|
4398
4434
|
// src/cluster/ConnectionPool.ts
|
|
4399
4435
|
var import_core7 = require("@topgunbuild/core");
|
|
4400
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
|
|
4401
4461
|
var ConnectionPool = class {
|
|
4402
4462
|
constructor(config = {}) {
|
|
4403
4463
|
this.listeners = /* @__PURE__ */ new Map();
|
|
@@ -4469,10 +4529,17 @@ var ConnectionPool = class {
|
|
|
4469
4529
|
return;
|
|
4470
4530
|
}
|
|
4471
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
|
+
}
|
|
4472
4538
|
const connection = {
|
|
4473
4539
|
nodeId,
|
|
4474
4540
|
endpoint,
|
|
4475
4541
|
socket: null,
|
|
4542
|
+
cachedConnection: null,
|
|
4476
4543
|
state: "DISCONNECTED",
|
|
4477
4544
|
lastSeen: 0,
|
|
4478
4545
|
latencyMs: 0,
|
|
@@ -4507,15 +4574,34 @@ var ConnectionPool = class {
|
|
|
4507
4574
|
}
|
|
4508
4575
|
logger.info({ nodeId }, "Node removed from connection pool");
|
|
4509
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
|
+
}
|
|
4510
4593
|
/**
|
|
4511
4594
|
* Get connection for a specific node
|
|
4512
4595
|
*/
|
|
4513
4596
|
getConnection(nodeId) {
|
|
4514
4597
|
const connection = this.connections.get(nodeId);
|
|
4515
|
-
if (!connection || connection.state !== "AUTHENTICATED") {
|
|
4598
|
+
if (!connection || connection.state !== "CONNECTED" && connection.state !== "AUTHENTICATED" || !connection.socket) {
|
|
4516
4599
|
return null;
|
|
4517
4600
|
}
|
|
4518
|
-
|
|
4601
|
+
if (!connection.cachedConnection) {
|
|
4602
|
+
connection.cachedConnection = new WebSocketConnection(connection.socket);
|
|
4603
|
+
}
|
|
4604
|
+
return connection.cachedConnection;
|
|
4519
4605
|
}
|
|
4520
4606
|
/**
|
|
4521
4607
|
* Get primary connection (first/seed node)
|
|
@@ -4529,8 +4615,11 @@ var ConnectionPool = class {
|
|
|
4529
4615
|
*/
|
|
4530
4616
|
getAnyHealthyConnection() {
|
|
4531
4617
|
for (const [nodeId, conn] of this.connections) {
|
|
4532
|
-
if (conn.state === "AUTHENTICATED" && conn.socket) {
|
|
4533
|
-
|
|
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 };
|
|
4534
4623
|
}
|
|
4535
4624
|
}
|
|
4536
4625
|
return null;
|
|
@@ -4586,7 +4675,7 @@ var ConnectionPool = class {
|
|
|
4586
4675
|
* Get list of connected node IDs
|
|
4587
4676
|
*/
|
|
4588
4677
|
getConnectedNodes() {
|
|
4589
|
-
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);
|
|
4590
4679
|
}
|
|
4591
4680
|
/**
|
|
4592
4681
|
* Get all node IDs
|
|
@@ -4595,11 +4684,11 @@ var ConnectionPool = class {
|
|
|
4595
4684
|
return Array.from(this.connections.keys());
|
|
4596
4685
|
}
|
|
4597
4686
|
/**
|
|
4598
|
-
* Check if node
|
|
4687
|
+
* Check if node has an open WebSocket connection
|
|
4599
4688
|
*/
|
|
4600
4689
|
isNodeConnected(nodeId) {
|
|
4601
4690
|
const conn = this.connections.get(nodeId);
|
|
4602
|
-
return conn?.state === "AUTHENTICATED";
|
|
4691
|
+
return conn?.state === "CONNECTED" || conn?.state === "AUTHENTICATED";
|
|
4603
4692
|
}
|
|
4604
4693
|
/**
|
|
4605
4694
|
* Check if connected to a specific node.
|
|
@@ -4664,25 +4753,26 @@ var ConnectionPool = class {
|
|
|
4664
4753
|
};
|
|
4665
4754
|
socket.onmessage = (event) => {
|
|
4666
4755
|
connection.lastSeen = Date.now();
|
|
4667
|
-
this.handleMessage(nodeId, event);
|
|
4756
|
+
this.handleMessage(connection.nodeId, event);
|
|
4668
4757
|
};
|
|
4669
4758
|
socket.onerror = (error) => {
|
|
4670
|
-
logger.error({ nodeId, error }, "WebSocket error");
|
|
4671
|
-
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"));
|
|
4672
4761
|
};
|
|
4673
4762
|
socket.onclose = () => {
|
|
4674
4763
|
const wasConnected = connection.state === "AUTHENTICATED";
|
|
4675
4764
|
connection.state = "DISCONNECTED";
|
|
4676
4765
|
connection.socket = null;
|
|
4766
|
+
connection.cachedConnection = null;
|
|
4677
4767
|
if (wasConnected) {
|
|
4678
|
-
this.emit("node:disconnected", nodeId, "Connection closed");
|
|
4768
|
+
this.emit("node:disconnected", connection.nodeId, "Connection closed");
|
|
4679
4769
|
}
|
|
4680
|
-
this.scheduleReconnect(nodeId);
|
|
4770
|
+
this.scheduleReconnect(connection.nodeId);
|
|
4681
4771
|
};
|
|
4682
4772
|
} catch (error) {
|
|
4683
4773
|
connection.state = "FAILED";
|
|
4684
|
-
logger.error({ nodeId, error }, "Failed to connect");
|
|
4685
|
-
this.scheduleReconnect(nodeId);
|
|
4774
|
+
logger.error({ nodeId: connection.nodeId, error }, "Failed to connect");
|
|
4775
|
+
this.scheduleReconnect(connection.nodeId);
|
|
4686
4776
|
}
|
|
4687
4777
|
}
|
|
4688
4778
|
sendAuth(connection) {
|
|
@@ -4711,18 +4801,15 @@ var ConnectionPool = class {
|
|
|
4711
4801
|
logger.info({ nodeId }, "Authenticated with node");
|
|
4712
4802
|
this.emit("node:healthy", nodeId);
|
|
4713
4803
|
this.flushPendingMessages(connection);
|
|
4714
|
-
return;
|
|
4715
4804
|
}
|
|
4716
4805
|
if (message.type === "AUTH_REQUIRED") {
|
|
4717
4806
|
if (this.authToken) {
|
|
4718
4807
|
this.sendAuth(connection);
|
|
4719
4808
|
}
|
|
4720
|
-
return;
|
|
4721
4809
|
}
|
|
4722
4810
|
if (message.type === "AUTH_FAIL") {
|
|
4723
4811
|
logger.error({ nodeId, error: message.error }, "Authentication failed");
|
|
4724
4812
|
connection.state = "FAILED";
|
|
4725
|
-
return;
|
|
4726
4813
|
}
|
|
4727
4814
|
if (message.type === "PONG") {
|
|
4728
4815
|
if (message.timestamp) {
|
|
@@ -4730,10 +4817,6 @@ var ConnectionPool = class {
|
|
|
4730
4817
|
}
|
|
4731
4818
|
return;
|
|
4732
4819
|
}
|
|
4733
|
-
if (message.type === "PARTITION_MAP" || message.type === "PARTITION_MAP_DELTA") {
|
|
4734
|
-
this.emit("message", nodeId, message);
|
|
4735
|
-
return;
|
|
4736
|
-
}
|
|
4737
4820
|
this.emit("message", nodeId, message);
|
|
4738
4821
|
}
|
|
4739
4822
|
flushPendingMessages(connection) {
|
|
@@ -4900,17 +4983,17 @@ var PartitionRouter = class {
|
|
|
4900
4983
|
}
|
|
4901
4984
|
return null;
|
|
4902
4985
|
}
|
|
4903
|
-
const
|
|
4904
|
-
if (
|
|
4905
|
-
return { nodeId: routing.nodeId,
|
|
4986
|
+
const connection = this.connectionPool.getConnection(routing.nodeId);
|
|
4987
|
+
if (connection) {
|
|
4988
|
+
return { nodeId: routing.nodeId, connection };
|
|
4906
4989
|
}
|
|
4907
4990
|
const partition = this.partitionMap.partitions.find((p) => p.partitionId === routing.partitionId);
|
|
4908
4991
|
if (partition) {
|
|
4909
4992
|
for (const backupId of partition.backupNodeIds) {
|
|
4910
|
-
const
|
|
4911
|
-
if (
|
|
4993
|
+
const backupConnection = this.connectionPool.getConnection(backupId);
|
|
4994
|
+
if (backupConnection) {
|
|
4912
4995
|
logger.debug({ key, owner: routing.nodeId, backup: backupId }, "Using backup node");
|
|
4913
|
-
return { nodeId: backupId,
|
|
4996
|
+
return { nodeId: backupId, connection: backupConnection };
|
|
4914
4997
|
}
|
|
4915
4998
|
}
|
|
4916
4999
|
}
|
|
@@ -5186,7 +5269,7 @@ var PartitionRouter = class {
|
|
|
5186
5269
|
};
|
|
5187
5270
|
|
|
5188
5271
|
// src/cluster/ClusterClient.ts
|
|
5189
|
-
var
|
|
5272
|
+
var _ClusterClient = class _ClusterClient {
|
|
5190
5273
|
constructor(config) {
|
|
5191
5274
|
this.listeners = /* @__PURE__ */ new Map();
|
|
5192
5275
|
this.initialized = false;
|
|
@@ -5199,6 +5282,8 @@ var ClusterClient = class {
|
|
|
5199
5282
|
};
|
|
5200
5283
|
// Circuit breaker state per node
|
|
5201
5284
|
this.circuits = /* @__PURE__ */ new Map();
|
|
5285
|
+
// Debounce timer for partition map requests on reconnect
|
|
5286
|
+
this.partitionMapRequestTimer = null;
|
|
5202
5287
|
this.config = config;
|
|
5203
5288
|
this.circuitBreakerConfig = {
|
|
5204
5289
|
...import_core10.DEFAULT_CIRCUIT_BREAKER_CONFIG,
|
|
@@ -5290,14 +5375,14 @@ var ClusterClient = class {
|
|
|
5290
5375
|
this.requestPartitionMapRefresh();
|
|
5291
5376
|
return this.getFallbackConnection();
|
|
5292
5377
|
}
|
|
5293
|
-
const
|
|
5294
|
-
if (!
|
|
5378
|
+
const connection = this.connectionPool.getConnection(owner);
|
|
5379
|
+
if (!connection) {
|
|
5295
5380
|
this.routingMetrics.fallbackRoutes++;
|
|
5296
5381
|
logger.debug({ key, owner }, "Could not get connection to owner, using fallback");
|
|
5297
5382
|
return this.getFallbackConnection();
|
|
5298
5383
|
}
|
|
5299
5384
|
this.routingMetrics.directRoutes++;
|
|
5300
|
-
return
|
|
5385
|
+
return connection;
|
|
5301
5386
|
}
|
|
5302
5387
|
/**
|
|
5303
5388
|
* Get fallback connection when owner is unavailable.
|
|
@@ -5305,10 +5390,10 @@ var ClusterClient = class {
|
|
|
5305
5390
|
*/
|
|
5306
5391
|
getFallbackConnection() {
|
|
5307
5392
|
const conn = this.connectionPool.getAnyHealthyConnection();
|
|
5308
|
-
if (!conn?.
|
|
5393
|
+
if (!conn?.connection) {
|
|
5309
5394
|
throw new Error("No healthy connection available");
|
|
5310
5395
|
}
|
|
5311
|
-
return conn.
|
|
5396
|
+
return conn.connection;
|
|
5312
5397
|
}
|
|
5313
5398
|
/**
|
|
5314
5399
|
* Request a partition map refresh in the background.
|
|
@@ -5319,9 +5404,23 @@ var ClusterClient = class {
|
|
|
5319
5404
|
logger.error({ err }, "Failed to refresh partition map");
|
|
5320
5405
|
});
|
|
5321
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
|
+
}
|
|
5322
5421
|
/**
|
|
5323
5422
|
* Request partition map from a specific node.
|
|
5324
|
-
* Called on
|
|
5423
|
+
* Called on node connection via debounced handler.
|
|
5325
5424
|
*/
|
|
5326
5425
|
requestPartitionMapFromNode(nodeId) {
|
|
5327
5426
|
const socket = this.connectionPool.getConnection(nodeId);
|
|
@@ -5490,8 +5589,8 @@ var ClusterClient = class {
|
|
|
5490
5589
|
* Send directly to partition owner
|
|
5491
5590
|
*/
|
|
5492
5591
|
sendDirect(key, message) {
|
|
5493
|
-
const
|
|
5494
|
-
if (!
|
|
5592
|
+
const route = this.partitionRouter.routeToConnection(key);
|
|
5593
|
+
if (!route) {
|
|
5495
5594
|
logger.warn({ key }, "No route available for key");
|
|
5496
5595
|
return false;
|
|
5497
5596
|
}
|
|
@@ -5502,7 +5601,7 @@ var ClusterClient = class {
|
|
|
5502
5601
|
mapVersion: this.partitionRouter.getMapVersion()
|
|
5503
5602
|
}
|
|
5504
5603
|
};
|
|
5505
|
-
connection.
|
|
5604
|
+
route.connection.send((0, import_core10.serialize)(routedMessage));
|
|
5506
5605
|
return true;
|
|
5507
5606
|
}
|
|
5508
5607
|
/**
|
|
@@ -5606,10 +5705,23 @@ var ClusterClient = class {
|
|
|
5606
5705
|
async refreshPartitionMap() {
|
|
5607
5706
|
await this.partitionRouter.refreshPartitionMap();
|
|
5608
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
|
+
}
|
|
5609
5717
|
/**
|
|
5610
5718
|
* Shutdown cluster client (IConnectionProvider interface).
|
|
5611
5719
|
*/
|
|
5612
5720
|
async close() {
|
|
5721
|
+
if (this.partitionMapRequestTimer) {
|
|
5722
|
+
clearTimeout(this.partitionMapRequestTimer);
|
|
5723
|
+
this.partitionMapRequestTimer = null;
|
|
5724
|
+
}
|
|
5613
5725
|
this.partitionRouter.close();
|
|
5614
5726
|
this.connectionPool.close();
|
|
5615
5727
|
this.initialized = false;
|
|
@@ -5632,23 +5744,23 @@ var ClusterClient = class {
|
|
|
5632
5744
|
return this.partitionRouter;
|
|
5633
5745
|
}
|
|
5634
5746
|
/**
|
|
5635
|
-
* Get any healthy
|
|
5747
|
+
* Get any healthy connection (IConnectionProvider interface).
|
|
5636
5748
|
* @throws Error if not connected
|
|
5637
5749
|
*/
|
|
5638
5750
|
getAnyConnection() {
|
|
5639
5751
|
const conn = this.connectionPool.getAnyHealthyConnection();
|
|
5640
|
-
if (!conn?.
|
|
5752
|
+
if (!conn?.connection) {
|
|
5641
5753
|
throw new Error("No healthy connection available");
|
|
5642
5754
|
}
|
|
5643
|
-
return conn.
|
|
5755
|
+
return conn.connection;
|
|
5644
5756
|
}
|
|
5645
5757
|
/**
|
|
5646
|
-
* Get any healthy
|
|
5758
|
+
* Get any healthy connection, or null if none available.
|
|
5647
5759
|
* Use this for optional connection checks.
|
|
5648
5760
|
*/
|
|
5649
5761
|
getAnyConnectionOrNull() {
|
|
5650
5762
|
const conn = this.connectionPool.getAnyHealthyConnection();
|
|
5651
|
-
return conn?.
|
|
5763
|
+
return conn?.connection ?? null;
|
|
5652
5764
|
}
|
|
5653
5765
|
// ============================================
|
|
5654
5766
|
// Circuit Breaker Methods
|
|
@@ -5739,9 +5851,7 @@ var ClusterClient = class {
|
|
|
5739
5851
|
setupEventHandlers() {
|
|
5740
5852
|
this.connectionPool.on("node:connected", (nodeId) => {
|
|
5741
5853
|
logger.debug({ nodeId }, "Node connected");
|
|
5742
|
-
|
|
5743
|
-
this.requestPartitionMapFromNode(nodeId);
|
|
5744
|
-
}
|
|
5854
|
+
this.debouncedPartitionMapRequest(nodeId);
|
|
5745
5855
|
if (this.connectionPool.getConnectedNodes().length === 1) {
|
|
5746
5856
|
this.emit("connected");
|
|
5747
5857
|
}
|
|
@@ -5796,6 +5906,8 @@ var ClusterClient = class {
|
|
|
5796
5906
|
});
|
|
5797
5907
|
}
|
|
5798
5908
|
};
|
|
5909
|
+
_ClusterClient.PARTITION_MAP_REQUEST_DEBOUNCE_MS = 500;
|
|
5910
|
+
var ClusterClient = _ClusterClient;
|
|
5799
5911
|
|
|
5800
5912
|
// src/connection/SingleServerProvider.ts
|
|
5801
5913
|
var DEFAULT_CONFIG = {
|
|
@@ -5811,14 +5923,18 @@ var SingleServerProvider = class {
|
|
|
5811
5923
|
this.reconnectTimer = null;
|
|
5812
5924
|
this.isClosing = false;
|
|
5813
5925
|
this.listeners = /* @__PURE__ */ new Map();
|
|
5926
|
+
this.onlineHandler = null;
|
|
5927
|
+
this.offlineHandler = null;
|
|
5814
5928
|
this.url = config.url;
|
|
5815
5929
|
this.config = {
|
|
5816
5930
|
url: config.url,
|
|
5817
5931
|
maxReconnectAttempts: config.maxReconnectAttempts ?? DEFAULT_CONFIG.maxReconnectAttempts,
|
|
5818
5932
|
reconnectDelayMs: config.reconnectDelayMs ?? DEFAULT_CONFIG.reconnectDelayMs,
|
|
5819
5933
|
backoffMultiplier: config.backoffMultiplier ?? DEFAULT_CONFIG.backoffMultiplier,
|
|
5820
|
-
maxReconnectDelayMs: config.maxReconnectDelayMs ?? DEFAULT_CONFIG.maxReconnectDelayMs
|
|
5934
|
+
maxReconnectDelayMs: config.maxReconnectDelayMs ?? DEFAULT_CONFIG.maxReconnectDelayMs,
|
|
5935
|
+
listenNetworkEvents: config.listenNetworkEvents ?? true
|
|
5821
5936
|
};
|
|
5937
|
+
this.setupNetworkListeners();
|
|
5822
5938
|
}
|
|
5823
5939
|
/**
|
|
5824
5940
|
* Connect to the WebSocket server.
|
|
@@ -5827,6 +5943,9 @@ var SingleServerProvider = class {
|
|
|
5827
5943
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
5828
5944
|
return;
|
|
5829
5945
|
}
|
|
5946
|
+
if (typeof globalThis.navigator !== "undefined" && globalThis.navigator.onLine === false) {
|
|
5947
|
+
throw new Error("Browser is offline \u2014 skipping connection attempt");
|
|
5948
|
+
}
|
|
5830
5949
|
this.isClosing = false;
|
|
5831
5950
|
return new Promise((resolve, reject) => {
|
|
5832
5951
|
try {
|
|
@@ -5879,7 +5998,7 @@ var SingleServerProvider = class {
|
|
|
5879
5998
|
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
5880
5999
|
throw new Error("Not connected");
|
|
5881
6000
|
}
|
|
5882
|
-
return this.ws;
|
|
6001
|
+
return new WebSocketConnection(this.ws);
|
|
5883
6002
|
}
|
|
5884
6003
|
/**
|
|
5885
6004
|
* Get any available connection.
|
|
@@ -5930,6 +6049,7 @@ var SingleServerProvider = class {
|
|
|
5930
6049
|
*/
|
|
5931
6050
|
async close() {
|
|
5932
6051
|
this.isClosing = true;
|
|
6052
|
+
this.teardownNetworkListeners();
|
|
5933
6053
|
if (this.reconnectTimer) {
|
|
5934
6054
|
clearTimeout(this.reconnectTimer);
|
|
5935
6055
|
this.reconnectTimer = null;
|
|
@@ -5968,6 +6088,10 @@ var SingleServerProvider = class {
|
|
|
5968
6088
|
clearTimeout(this.reconnectTimer);
|
|
5969
6089
|
this.reconnectTimer = null;
|
|
5970
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
|
+
}
|
|
5971
6095
|
if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
|
|
5972
6096
|
logger.error(
|
|
5973
6097
|
{ attempts: this.reconnectAttempts, url: this.url },
|
|
@@ -6003,6 +6127,37 @@ var SingleServerProvider = class {
|
|
|
6003
6127
|
delay = delay * (0.5 + Math.random());
|
|
6004
6128
|
return Math.floor(delay);
|
|
6005
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
|
+
}
|
|
6006
6161
|
/**
|
|
6007
6162
|
* Get the WebSocket URL this provider connects to.
|
|
6008
6163
|
*/
|
|
@@ -6022,6 +6177,43 @@ var SingleServerProvider = class {
|
|
|
6022
6177
|
resetReconnectAttempts() {
|
|
6023
6178
|
this.reconnectAttempts = 0;
|
|
6024
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
|
+
}
|
|
6025
6217
|
};
|
|
6026
6218
|
|
|
6027
6219
|
// src/TopGunClient.ts
|
|
@@ -6079,7 +6271,13 @@ var TopGunClient = class {
|
|
|
6079
6271
|
});
|
|
6080
6272
|
logger.info({ seeds: this.clusterConfig.seeds }, "TopGunClient initialized in cluster mode");
|
|
6081
6273
|
} else {
|
|
6082
|
-
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
|
+
});
|
|
6083
6281
|
this.syncEngine = new SyncEngine({
|
|
6084
6282
|
nodeId: this.nodeId,
|
|
6085
6283
|
connectionProvider: singleServerProvider,
|
|
@@ -7125,18 +7323,514 @@ var EncryptedStorageAdapter = class {
|
|
|
7125
7323
|
};
|
|
7126
7324
|
|
|
7127
7325
|
// src/index.ts
|
|
7326
|
+
var import_core14 = require("@topgunbuild/core");
|
|
7327
|
+
|
|
7328
|
+
// src/connection/HttpSyncProvider.ts
|
|
7128
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
|
+
};
|
|
7348
|
+
var HttpSyncProvider = class {
|
|
7349
|
+
constructor(config) {
|
|
7350
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
7351
|
+
this.pollTimer = null;
|
|
7352
|
+
/** Queued operations to send on next poll */
|
|
7353
|
+
this.pendingOperations = [];
|
|
7354
|
+
/** Queued one-shot queries to send on next poll */
|
|
7355
|
+
this.pendingQueries = [];
|
|
7356
|
+
/** Per-map last sync timestamps for delta tracking */
|
|
7357
|
+
this.lastSyncTimestamps = /* @__PURE__ */ new Map();
|
|
7358
|
+
/** Whether the last HTTP request succeeded */
|
|
7359
|
+
this.connected = false;
|
|
7360
|
+
/** Whether we were previously connected (for reconnected event) */
|
|
7361
|
+
this.wasConnected = false;
|
|
7362
|
+
this.url = config.url.replace(/\/$/, "");
|
|
7363
|
+
this.clientId = config.clientId;
|
|
7364
|
+
this.hlc = config.hlc;
|
|
7365
|
+
this.authToken = config.authToken || "";
|
|
7366
|
+
this.pollIntervalMs = config.pollIntervalMs ?? 5e3;
|
|
7367
|
+
this.requestTimeoutMs = config.requestTimeoutMs ?? 3e4;
|
|
7368
|
+
this.syncMaps = config.syncMaps || [];
|
|
7369
|
+
this.fetchImpl = config.fetchImpl || globalThis.fetch.bind(globalThis);
|
|
7370
|
+
}
|
|
7371
|
+
/**
|
|
7372
|
+
* Connect by sending an initial sync request to verify auth and get state.
|
|
7373
|
+
*/
|
|
7374
|
+
async connect() {
|
|
7375
|
+
try {
|
|
7376
|
+
await this.doSyncRequest();
|
|
7377
|
+
this.connected = true;
|
|
7378
|
+
this.wasConnected = true;
|
|
7379
|
+
this.emit("connected", "http");
|
|
7380
|
+
this.startPolling();
|
|
7381
|
+
} catch (err) {
|
|
7382
|
+
this.connected = false;
|
|
7383
|
+
this.emit("error", err);
|
|
7384
|
+
throw err;
|
|
7385
|
+
}
|
|
7386
|
+
}
|
|
7387
|
+
/**
|
|
7388
|
+
* Get connection for a specific key.
|
|
7389
|
+
* Returns an HttpConnection that queues operations for the next poll cycle.
|
|
7390
|
+
*/
|
|
7391
|
+
getConnection(_key) {
|
|
7392
|
+
return new HttpConnection(this);
|
|
7393
|
+
}
|
|
7394
|
+
/**
|
|
7395
|
+
* Get any available connection.
|
|
7396
|
+
* Returns an HttpConnection that queues operations for the next poll cycle.
|
|
7397
|
+
*/
|
|
7398
|
+
getAnyConnection() {
|
|
7399
|
+
return new HttpConnection(this);
|
|
7400
|
+
}
|
|
7401
|
+
/**
|
|
7402
|
+
* Check if connected (last HTTP request succeeded).
|
|
7403
|
+
*/
|
|
7404
|
+
isConnected() {
|
|
7405
|
+
return this.connected;
|
|
7406
|
+
}
|
|
7407
|
+
/**
|
|
7408
|
+
* Get connected node IDs.
|
|
7409
|
+
* Returns ['http'] when connected, [] when not.
|
|
7410
|
+
*/
|
|
7411
|
+
getConnectedNodes() {
|
|
7412
|
+
return this.connected ? ["http"] : [];
|
|
7413
|
+
}
|
|
7414
|
+
/**
|
|
7415
|
+
* Subscribe to connection events.
|
|
7416
|
+
*/
|
|
7417
|
+
on(event, handler2) {
|
|
7418
|
+
if (!this.listeners.has(event)) {
|
|
7419
|
+
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
7420
|
+
}
|
|
7421
|
+
this.listeners.get(event).add(handler2);
|
|
7422
|
+
}
|
|
7423
|
+
/**
|
|
7424
|
+
* Unsubscribe from connection events.
|
|
7425
|
+
*/
|
|
7426
|
+
off(event, handler2) {
|
|
7427
|
+
this.listeners.get(event)?.delete(handler2);
|
|
7428
|
+
}
|
|
7429
|
+
/**
|
|
7430
|
+
* Send data via the HTTP sync provider.
|
|
7431
|
+
*
|
|
7432
|
+
* Deserializes the msgpackr binary to extract the message type and routes:
|
|
7433
|
+
* - OP_BATCH / CLIENT_OP: queued as operations for next poll
|
|
7434
|
+
* - AUTH: silently ignored (auth via HTTP header)
|
|
7435
|
+
* - SYNC_INIT: silently ignored (HTTP uses timestamp-based deltas)
|
|
7436
|
+
* - QUERY_SUB: queued as one-shot query for next poll
|
|
7437
|
+
* - All other types: silently dropped with debug log
|
|
7438
|
+
*/
|
|
7439
|
+
send(data, _key) {
|
|
7440
|
+
try {
|
|
7441
|
+
const message = (0, import_core13.deserialize)(
|
|
7442
|
+
data instanceof ArrayBuffer ? new Uint8Array(data) : data
|
|
7443
|
+
);
|
|
7444
|
+
switch (message.type) {
|
|
7445
|
+
case "OP_BATCH":
|
|
7446
|
+
if (message.payload?.ops) {
|
|
7447
|
+
this.pendingOperations.push(...message.payload.ops);
|
|
7448
|
+
}
|
|
7449
|
+
break;
|
|
7450
|
+
case "CLIENT_OP":
|
|
7451
|
+
if (message.payload) {
|
|
7452
|
+
this.pendingOperations.push(message.payload);
|
|
7453
|
+
}
|
|
7454
|
+
break;
|
|
7455
|
+
case "AUTH":
|
|
7456
|
+
break;
|
|
7457
|
+
case "SYNC_INIT":
|
|
7458
|
+
break;
|
|
7459
|
+
case "QUERY_SUB":
|
|
7460
|
+
if (message.payload) {
|
|
7461
|
+
this.pendingQueries.push({
|
|
7462
|
+
queryId: message.payload.requestId || `q-${Date.now()}`,
|
|
7463
|
+
mapName: message.payload.mapName || message.mapName,
|
|
7464
|
+
filter: message.payload.query?.where || message.payload.where,
|
|
7465
|
+
limit: message.payload.query?.limit || message.payload.limit
|
|
7466
|
+
});
|
|
7467
|
+
}
|
|
7468
|
+
break;
|
|
7469
|
+
default:
|
|
7470
|
+
logger.debug(
|
|
7471
|
+
{ type: message.type },
|
|
7472
|
+
"HTTP sync provider: unsupported message type dropped"
|
|
7473
|
+
);
|
|
7474
|
+
break;
|
|
7475
|
+
}
|
|
7476
|
+
} catch (err) {
|
|
7477
|
+
logger.warn({ err }, "HTTP sync provider: failed to deserialize message");
|
|
7478
|
+
}
|
|
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
|
+
}
|
|
7491
|
+
/**
|
|
7492
|
+
* Close the HTTP sync provider.
|
|
7493
|
+
* Stops the polling loop, clears queued operations, and sets disconnected state.
|
|
7494
|
+
*/
|
|
7495
|
+
async close() {
|
|
7496
|
+
this.stopPolling();
|
|
7497
|
+
this.pendingOperations = [];
|
|
7498
|
+
this.pendingQueries = [];
|
|
7499
|
+
this.connected = false;
|
|
7500
|
+
logger.info({ url: this.url }, "HttpSyncProvider closed");
|
|
7501
|
+
}
|
|
7502
|
+
/**
|
|
7503
|
+
* Update the auth token (e.g., after token refresh).
|
|
7504
|
+
*/
|
|
7505
|
+
setAuthToken(token) {
|
|
7506
|
+
this.authToken = token;
|
|
7507
|
+
}
|
|
7508
|
+
/**
|
|
7509
|
+
* Send an HTTP sync request with queued operations and receive deltas.
|
|
7510
|
+
*/
|
|
7511
|
+
async doSyncRequest() {
|
|
7512
|
+
const syncMaps = this.syncMaps.map((mapName) => ({
|
|
7513
|
+
mapName,
|
|
7514
|
+
lastSyncTimestamp: this.lastSyncTimestamps.get(mapName) || {
|
|
7515
|
+
millis: 0,
|
|
7516
|
+
counter: 0,
|
|
7517
|
+
nodeId: ""
|
|
7518
|
+
}
|
|
7519
|
+
}));
|
|
7520
|
+
const operations = this.pendingOperations.splice(0);
|
|
7521
|
+
const queries = this.pendingQueries.splice(0);
|
|
7522
|
+
const requestBody = {
|
|
7523
|
+
clientId: this.clientId,
|
|
7524
|
+
clientHlc: this.hlc.now()
|
|
7525
|
+
};
|
|
7526
|
+
if (operations.length > 0) {
|
|
7527
|
+
requestBody.operations = operations;
|
|
7528
|
+
}
|
|
7529
|
+
if (syncMaps.length > 0) {
|
|
7530
|
+
requestBody.syncMaps = syncMaps;
|
|
7531
|
+
}
|
|
7532
|
+
if (queries.length > 0) {
|
|
7533
|
+
requestBody.queries = queries;
|
|
7534
|
+
}
|
|
7535
|
+
const bodyBytes = (0, import_core13.serialize)(requestBody);
|
|
7536
|
+
const bodyBuffer = new ArrayBuffer(bodyBytes.byteLength);
|
|
7537
|
+
new Uint8Array(bodyBuffer).set(bodyBytes);
|
|
7538
|
+
const controller = new AbortController();
|
|
7539
|
+
const timeoutId = setTimeout(() => controller.abort(), this.requestTimeoutMs);
|
|
7540
|
+
try {
|
|
7541
|
+
const response = await this.fetchImpl(`${this.url}/sync`, {
|
|
7542
|
+
method: "POST",
|
|
7543
|
+
headers: {
|
|
7544
|
+
"Content-Type": "application/x-msgpack",
|
|
7545
|
+
"Authorization": `Bearer ${this.authToken}`
|
|
7546
|
+
},
|
|
7547
|
+
body: bodyBuffer,
|
|
7548
|
+
signal: controller.signal
|
|
7549
|
+
});
|
|
7550
|
+
clearTimeout(timeoutId);
|
|
7551
|
+
if (!response.ok) {
|
|
7552
|
+
throw new Error(`HTTP sync request failed: ${response.status} ${response.statusText}`);
|
|
7553
|
+
}
|
|
7554
|
+
const responseBuffer = await response.arrayBuffer();
|
|
7555
|
+
const syncResponse = (0, import_core13.deserialize)(new Uint8Array(responseBuffer));
|
|
7556
|
+
if (syncResponse.serverHlc) {
|
|
7557
|
+
this.hlc.update(syncResponse.serverHlc);
|
|
7558
|
+
}
|
|
7559
|
+
if (syncResponse.ack) {
|
|
7560
|
+
this.emit("message", "http", (0, import_core13.serialize)({
|
|
7561
|
+
type: "OP_ACK",
|
|
7562
|
+
payload: syncResponse.ack
|
|
7563
|
+
}));
|
|
7564
|
+
}
|
|
7565
|
+
if (syncResponse.deltas) {
|
|
7566
|
+
for (const delta of syncResponse.deltas) {
|
|
7567
|
+
this.lastSyncTimestamps.set(delta.mapName, delta.serverSyncTimestamp);
|
|
7568
|
+
for (const record of delta.records) {
|
|
7569
|
+
this.emit("message", "http", (0, import_core13.serialize)({
|
|
7570
|
+
type: "SERVER_EVENT",
|
|
7571
|
+
payload: {
|
|
7572
|
+
mapName: delta.mapName,
|
|
7573
|
+
key: record.key,
|
|
7574
|
+
record: record.record,
|
|
7575
|
+
eventType: record.eventType
|
|
7576
|
+
}
|
|
7577
|
+
}));
|
|
7578
|
+
}
|
|
7579
|
+
}
|
|
7580
|
+
}
|
|
7581
|
+
if (syncResponse.queryResults) {
|
|
7582
|
+
for (const result of syncResponse.queryResults) {
|
|
7583
|
+
this.emit("message", "http", (0, import_core13.serialize)({
|
|
7584
|
+
type: "QUERY_RESP",
|
|
7585
|
+
payload: {
|
|
7586
|
+
requestId: result.queryId,
|
|
7587
|
+
results: result.results,
|
|
7588
|
+
hasMore: result.hasMore,
|
|
7589
|
+
nextCursor: result.nextCursor
|
|
7590
|
+
}
|
|
7591
|
+
}));
|
|
7592
|
+
}
|
|
7593
|
+
}
|
|
7594
|
+
if (!this.connected) {
|
|
7595
|
+
this.connected = true;
|
|
7596
|
+
if (this.wasConnected) {
|
|
7597
|
+
this.emit("reconnected", "http");
|
|
7598
|
+
} else {
|
|
7599
|
+
this.wasConnected = true;
|
|
7600
|
+
this.emit("connected", "http");
|
|
7601
|
+
}
|
|
7602
|
+
}
|
|
7603
|
+
} catch (err) {
|
|
7604
|
+
clearTimeout(timeoutId);
|
|
7605
|
+
if (this.connected) {
|
|
7606
|
+
this.connected = false;
|
|
7607
|
+
this.emit("disconnected", "http");
|
|
7608
|
+
}
|
|
7609
|
+
if (operations.length > 0) {
|
|
7610
|
+
this.pendingOperations.unshift(...operations);
|
|
7611
|
+
}
|
|
7612
|
+
if (queries.length > 0) {
|
|
7613
|
+
this.pendingQueries.unshift(...queries);
|
|
7614
|
+
}
|
|
7615
|
+
throw err;
|
|
7616
|
+
}
|
|
7617
|
+
}
|
|
7618
|
+
/**
|
|
7619
|
+
* Start the polling loop.
|
|
7620
|
+
*/
|
|
7621
|
+
startPolling() {
|
|
7622
|
+
if (this.pollTimer) return;
|
|
7623
|
+
this.pollTimer = setInterval(async () => {
|
|
7624
|
+
try {
|
|
7625
|
+
await this.doSyncRequest();
|
|
7626
|
+
} catch (err) {
|
|
7627
|
+
logger.debug({ err }, "HTTP sync poll failed");
|
|
7628
|
+
}
|
|
7629
|
+
}, this.pollIntervalMs);
|
|
7630
|
+
}
|
|
7631
|
+
/**
|
|
7632
|
+
* Stop the polling loop.
|
|
7633
|
+
*/
|
|
7634
|
+
stopPolling() {
|
|
7635
|
+
if (this.pollTimer) {
|
|
7636
|
+
clearInterval(this.pollTimer);
|
|
7637
|
+
this.pollTimer = null;
|
|
7638
|
+
}
|
|
7639
|
+
}
|
|
7640
|
+
/**
|
|
7641
|
+
* Emit an event to all listeners.
|
|
7642
|
+
*/
|
|
7643
|
+
emit(event, ...args) {
|
|
7644
|
+
const handlers = this.listeners.get(event);
|
|
7645
|
+
if (handlers) {
|
|
7646
|
+
for (const handler2 of handlers) {
|
|
7647
|
+
try {
|
|
7648
|
+
handler2(...args);
|
|
7649
|
+
} catch (err) {
|
|
7650
|
+
logger.error({ err, event }, "Error in HttpSyncProvider event handler");
|
|
7651
|
+
}
|
|
7652
|
+
}
|
|
7653
|
+
}
|
|
7654
|
+
}
|
|
7655
|
+
};
|
|
7656
|
+
|
|
7657
|
+
// src/connection/AutoConnectionProvider.ts
|
|
7658
|
+
var AutoConnectionProvider = class {
|
|
7659
|
+
constructor(config) {
|
|
7660
|
+
/** The active underlying provider */
|
|
7661
|
+
this.activeProvider = null;
|
|
7662
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
7663
|
+
this.config = config;
|
|
7664
|
+
this.maxWsAttempts = config.maxWsAttempts ?? 3;
|
|
7665
|
+
this.isHttpMode = config.httpOnly ?? false;
|
|
7666
|
+
}
|
|
7667
|
+
/**
|
|
7668
|
+
* Connect using WebSocket first, falling back to HTTP after maxWsAttempts failures.
|
|
7669
|
+
* If httpOnly is true, skips WebSocket entirely.
|
|
7670
|
+
*/
|
|
7671
|
+
async connect() {
|
|
7672
|
+
if (this.isHttpMode) {
|
|
7673
|
+
await this.connectHttp();
|
|
7674
|
+
return;
|
|
7675
|
+
}
|
|
7676
|
+
const wsUrl = this.toWsUrl(this.config.url);
|
|
7677
|
+
let lastError = null;
|
|
7678
|
+
for (let attempt = 0; attempt < this.maxWsAttempts; attempt++) {
|
|
7679
|
+
try {
|
|
7680
|
+
const wsProvider = new SingleServerProvider({
|
|
7681
|
+
url: wsUrl,
|
|
7682
|
+
maxReconnectAttempts: 1,
|
|
7683
|
+
reconnectDelayMs: 1e3
|
|
7684
|
+
});
|
|
7685
|
+
await wsProvider.connect();
|
|
7686
|
+
this.activeProvider = wsProvider;
|
|
7687
|
+
this.proxyEvents(wsProvider);
|
|
7688
|
+
logger.info({ url: wsUrl }, "AutoConnectionProvider: WebSocket connected");
|
|
7689
|
+
return;
|
|
7690
|
+
} catch (err) {
|
|
7691
|
+
lastError = err;
|
|
7692
|
+
logger.debug(
|
|
7693
|
+
{ attempt: attempt + 1, maxAttempts: this.maxWsAttempts, err: err.message },
|
|
7694
|
+
"AutoConnectionProvider: WebSocket attempt failed"
|
|
7695
|
+
);
|
|
7696
|
+
}
|
|
7697
|
+
}
|
|
7698
|
+
logger.info(
|
|
7699
|
+
{ wsAttempts: this.maxWsAttempts, url: this.config.url },
|
|
7700
|
+
"AutoConnectionProvider: WebSocket failed, falling back to HTTP"
|
|
7701
|
+
);
|
|
7702
|
+
this.isHttpMode = true;
|
|
7703
|
+
await this.connectHttp();
|
|
7704
|
+
}
|
|
7705
|
+
/**
|
|
7706
|
+
* Connect using HTTP sync provider.
|
|
7707
|
+
*/
|
|
7708
|
+
async connectHttp() {
|
|
7709
|
+
const httpUrl = this.toHttpUrl(this.config.url);
|
|
7710
|
+
const httpProvider = new HttpSyncProvider({
|
|
7711
|
+
url: httpUrl,
|
|
7712
|
+
clientId: this.config.clientId,
|
|
7713
|
+
hlc: this.config.hlc,
|
|
7714
|
+
authToken: this.config.authToken,
|
|
7715
|
+
pollIntervalMs: this.config.httpPollIntervalMs,
|
|
7716
|
+
syncMaps: this.config.syncMaps,
|
|
7717
|
+
fetchImpl: this.config.fetchImpl
|
|
7718
|
+
});
|
|
7719
|
+
this.activeProvider = httpProvider;
|
|
7720
|
+
this.proxyEvents(httpProvider);
|
|
7721
|
+
await httpProvider.connect();
|
|
7722
|
+
logger.info({ url: httpUrl }, "AutoConnectionProvider: HTTP connected");
|
|
7723
|
+
}
|
|
7724
|
+
getConnection(key) {
|
|
7725
|
+
if (!this.activeProvider) {
|
|
7726
|
+
throw new Error("Not connected");
|
|
7727
|
+
}
|
|
7728
|
+
return this.activeProvider.getConnection(key);
|
|
7729
|
+
}
|
|
7730
|
+
getAnyConnection() {
|
|
7731
|
+
if (!this.activeProvider) {
|
|
7732
|
+
throw new Error("Not connected");
|
|
7733
|
+
}
|
|
7734
|
+
return this.activeProvider.getAnyConnection();
|
|
7735
|
+
}
|
|
7736
|
+
isConnected() {
|
|
7737
|
+
return this.activeProvider?.isConnected() ?? false;
|
|
7738
|
+
}
|
|
7739
|
+
getConnectedNodes() {
|
|
7740
|
+
return this.activeProvider?.getConnectedNodes() ?? [];
|
|
7741
|
+
}
|
|
7742
|
+
on(event, handler2) {
|
|
7743
|
+
if (!this.listeners.has(event)) {
|
|
7744
|
+
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
7745
|
+
}
|
|
7746
|
+
this.listeners.get(event).add(handler2);
|
|
7747
|
+
if (this.activeProvider) {
|
|
7748
|
+
this.activeProvider.on(event, handler2);
|
|
7749
|
+
}
|
|
7750
|
+
}
|
|
7751
|
+
off(event, handler2) {
|
|
7752
|
+
this.listeners.get(event)?.delete(handler2);
|
|
7753
|
+
if (this.activeProvider) {
|
|
7754
|
+
this.activeProvider.off(event, handler2);
|
|
7755
|
+
}
|
|
7756
|
+
}
|
|
7757
|
+
send(data, key) {
|
|
7758
|
+
if (!this.activeProvider) {
|
|
7759
|
+
throw new Error("Not connected");
|
|
7760
|
+
}
|
|
7761
|
+
this.activeProvider.send(data, key);
|
|
7762
|
+
}
|
|
7763
|
+
/**
|
|
7764
|
+
* Force reconnect by delegating to the active provider.
|
|
7765
|
+
*/
|
|
7766
|
+
forceReconnect() {
|
|
7767
|
+
if (this.activeProvider) {
|
|
7768
|
+
this.activeProvider.forceReconnect();
|
|
7769
|
+
}
|
|
7770
|
+
}
|
|
7771
|
+
/**
|
|
7772
|
+
* Close the active underlying provider.
|
|
7773
|
+
*/
|
|
7774
|
+
async close() {
|
|
7775
|
+
if (this.activeProvider) {
|
|
7776
|
+
await this.activeProvider.close();
|
|
7777
|
+
this.activeProvider = null;
|
|
7778
|
+
}
|
|
7779
|
+
}
|
|
7780
|
+
/**
|
|
7781
|
+
* Whether currently using HTTP mode.
|
|
7782
|
+
*/
|
|
7783
|
+
isUsingHttp() {
|
|
7784
|
+
return this.isHttpMode;
|
|
7785
|
+
}
|
|
7786
|
+
/**
|
|
7787
|
+
* Proxy events from the underlying provider to our listeners.
|
|
7788
|
+
*/
|
|
7789
|
+
proxyEvents(provider) {
|
|
7790
|
+
const events = [
|
|
7791
|
+
"connected",
|
|
7792
|
+
"disconnected",
|
|
7793
|
+
"reconnected",
|
|
7794
|
+
"message",
|
|
7795
|
+
"partitionMapUpdated",
|
|
7796
|
+
"error"
|
|
7797
|
+
];
|
|
7798
|
+
for (const event of events) {
|
|
7799
|
+
const handlers = this.listeners.get(event);
|
|
7800
|
+
if (handlers) {
|
|
7801
|
+
for (const handler2 of handlers) {
|
|
7802
|
+
provider.on(event, handler2);
|
|
7803
|
+
}
|
|
7804
|
+
}
|
|
7805
|
+
}
|
|
7806
|
+
}
|
|
7807
|
+
/**
|
|
7808
|
+
* Convert a URL to WebSocket URL format.
|
|
7809
|
+
*/
|
|
7810
|
+
toWsUrl(url) {
|
|
7811
|
+
return url.replace(/^http:\/\//, "ws://").replace(/^https:\/\//, "wss://");
|
|
7812
|
+
}
|
|
7813
|
+
/**
|
|
7814
|
+
* Convert a URL to HTTP URL format.
|
|
7815
|
+
*/
|
|
7816
|
+
toHttpUrl(url) {
|
|
7817
|
+
return url.replace(/^ws:\/\//, "http://").replace(/^wss:\/\//, "https://");
|
|
7818
|
+
}
|
|
7819
|
+
};
|
|
7129
7820
|
// Annotate the CommonJS export names for ESM import in node:
|
|
7130
7821
|
0 && (module.exports = {
|
|
7822
|
+
AutoConnectionProvider,
|
|
7131
7823
|
BackpressureError,
|
|
7132
7824
|
ChangeTracker,
|
|
7133
7825
|
ClusterClient,
|
|
7134
7826
|
ConflictResolverClient,
|
|
7135
7827
|
ConnectionPool,
|
|
7828
|
+
ConnectionReadyState,
|
|
7136
7829
|
DEFAULT_BACKPRESSURE_CONFIG,
|
|
7137
7830
|
DEFAULT_CLUSTER_CONFIG,
|
|
7138
7831
|
EncryptedStorageAdapter,
|
|
7139
7832
|
EventJournalReader,
|
|
7833
|
+
HttpSyncProvider,
|
|
7140
7834
|
HybridQueryHandle,
|
|
7141
7835
|
IDBAdapter,
|
|
7142
7836
|
LWWMap,
|
|
@@ -7153,6 +7847,7 @@ var import_core13 = require("@topgunbuild/core");
|
|
|
7153
7847
|
TopGunClient,
|
|
7154
7848
|
TopicHandle,
|
|
7155
7849
|
VALID_TRANSITIONS,
|
|
7850
|
+
WebSocketConnection,
|
|
7156
7851
|
isValidTransition,
|
|
7157
7852
|
logger
|
|
7158
7853
|
});
|