@delali/sirannon-db 0.1.4 → 0.1.5

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.
@@ -32,8 +32,8 @@ var RemoteDatabase = class {
32
32
  * )
33
33
  * ```
34
34
  */
35
- async query(sql, params) {
36
- const response = await this.transport.query(sql, params);
35
+ async query(sql, params, options) {
36
+ const response = await this.transport.query(sql, params, options?.readConcern);
37
37
  return response.rows;
38
38
  }
39
39
  /**
@@ -104,8 +104,8 @@ var HttpTransport = class {
104
104
  ...headers
105
105
  };
106
106
  }
107
- async query(sql, params) {
108
- return this.post("/query", { sql, params });
107
+ async query(sql, params, readConcern) {
108
+ return this.post("/query", { sql, params, readConcern });
109
109
  }
110
110
  async execute(sql, params) {
111
111
  return this.post("/execute", { sql, params });
@@ -165,6 +165,7 @@ var WebSocketTransport = class {
165
165
  autoReconnect;
166
166
  reconnectInterval;
167
167
  requestTimeout;
168
+ protocols;
168
169
  pendingRequests = /* @__PURE__ */ new Map();
169
170
  activeSubscriptions = /* @__PURE__ */ new Map();
170
171
  idCounter = 0;
@@ -176,6 +177,7 @@ var WebSocketTransport = class {
176
177
  this.autoReconnect = options?.autoReconnect ?? true;
177
178
  this.reconnectInterval = options?.reconnectInterval ?? 1e3;
178
179
  this.requestTimeout = options?.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT;
180
+ this.protocols = options?.protocols;
179
181
  }
180
182
  async query(sql, params) {
181
183
  await this.ensureConnected();
@@ -249,7 +251,7 @@ var WebSocketTransport = class {
249
251
  connect() {
250
252
  return new Promise((resolve, reject) => {
251
253
  let settled = false;
252
- const ws = new WebSocket(this.url);
254
+ const ws = this.protocols === void 0 ? new WebSocket(this.url) : new WebSocket(this.url, this.protocols);
253
255
  const onOpen = () => {
254
256
  settled = true;
255
257
  this.ws = ws;
@@ -413,30 +415,192 @@ var WebSocketTransport = class {
413
415
  };
414
416
 
415
417
  // src/client/client.ts
418
+ var CLUSTER_DISCOVERY_FETCH_TIMEOUT_MS = 2e3;
419
+ function clusterRoutingFingerprint(state) {
420
+ const readEndpoints = state.readEndpoints.map((endpoint) => ({
421
+ url: endpoint.url,
422
+ readConcerns: [...endpoint.readConcerns].sort()
423
+ })).sort((left, right) => left.url.localeCompare(right.url));
424
+ return JSON.stringify({
425
+ currentPrimary: state.currentPrimary,
426
+ primaryTerm: state.primaryTerm,
427
+ readEndpoints
428
+ });
429
+ }
430
+ function clusterRoutingChanged(previous, next) {
431
+ return previous === void 0 || clusterRoutingFingerprint(previous) !== clusterRoutingFingerprint(next);
432
+ }
433
+ function isTopologyConfig(urlOrOpts) {
434
+ if (typeof urlOrOpts !== "object") {
435
+ return false;
436
+ }
437
+ return "primary" in urlOrOpts || "replicas" in urlOrOpts && Array.isArray(urlOrOpts.replicas) && urlOrOpts.replicas.length > 0 || "endpoints" in urlOrOpts && Array.isArray(urlOrOpts.endpoints) && urlOrOpts.endpoints.length > 0 || urlOrOpts.discovery === "coordinator";
438
+ }
439
+ function toBaseUrl(url) {
440
+ return normaliseEndpointUrl(url);
441
+ }
442
+ function toServerBaseUrl(url, databaseId) {
443
+ const base = toBaseUrl(url);
444
+ if (!databaseId) return base.replace(/\/db\/[^/]+$/i, "");
445
+ return base.replace(new RegExp(`/db/${escapeRegExp(encodeURIComponent(databaseId))}$`, "i"), "");
446
+ }
447
+ function toWsUrl(baseUrl) {
448
+ return baseUrl.replace(/^http:\/\//i, "ws://").replace(/^https:\/\//i, "wss://");
449
+ }
450
+ function escapeRegExp(value) {
451
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
452
+ }
453
+ function normaliseEndpointUrl(rawUrl) {
454
+ let parsed;
455
+ try {
456
+ parsed = new URL(rawUrl);
457
+ } catch {
458
+ throw new TypeError(`Endpoint URL '${rawUrl}' is invalid`);
459
+ }
460
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
461
+ throw new TypeError(`Endpoint URL '${rawUrl}' must use http or https`);
462
+ }
463
+ if (parsed.username || parsed.password) {
464
+ throw new TypeError(`Endpoint URL '${rawUrl}' must not contain credentials`);
465
+ }
466
+ if (parsed.hash) {
467
+ throw new TypeError(`Endpoint URL '${rawUrl}' must not contain a fragment`);
468
+ }
469
+ if (parsed.search) {
470
+ throw new TypeError(`Endpoint URL '${rawUrl}' must not contain a query string`);
471
+ }
472
+ parsed.pathname = parsed.pathname.replace(/\/+$/, "");
473
+ return parsed.toString().replace(/\/$/, "");
474
+ }
475
+ function isReadConcernLevel(value) {
476
+ return value === "local" || value === "majority" || value === "linearizable";
477
+ }
478
+ function parseDiscoveredReadConcerns(value) {
479
+ if (!Array.isArray(value)) {
480
+ throw new RemoteError("INVALID_RESPONSE", "Cluster metadata readConcerns must be an array");
481
+ }
482
+ const concerns = [];
483
+ for (const concern of value) {
484
+ if (!isReadConcernLevel(concern)) {
485
+ throw new RemoteError("INVALID_RESPONSE", "Cluster metadata contains an invalid read concern");
486
+ }
487
+ if (!concerns.includes(concern)) {
488
+ concerns.push(concern);
489
+ }
490
+ }
491
+ return concerns;
492
+ }
493
+ function toDiscoveredServerBaseUrl(endpoint, databaseId) {
494
+ try {
495
+ return toServerBaseUrl(endpoint, databaseId);
496
+ } catch (err) {
497
+ const message = err instanceof Error ? err.message : String(err);
498
+ throw new RemoteError("INVALID_RESPONSE", `Cluster metadata contains an unsafe endpoint: ${message}`);
499
+ }
500
+ }
501
+ function parseClusterRouting(data, databaseId) {
502
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
503
+ throw new RemoteError("INVALID_RESPONSE", "Cluster metadata must be an object");
504
+ }
505
+ const record = data;
506
+ if (record.databaseId !== databaseId) {
507
+ throw new RemoteError("INVALID_RESPONSE", "Cluster metadata database id does not match the request");
508
+ }
509
+ if (record.primaryTerm !== void 0 && typeof record.primaryTerm !== "string") {
510
+ throw new RemoteError("INVALID_RESPONSE", "Cluster metadata primaryTerm must be a string");
511
+ }
512
+ let currentPrimary = null;
513
+ if (record.currentPrimary !== void 0 && record.currentPrimary !== null) {
514
+ if (typeof record.currentPrimary !== "object" || Array.isArray(record.currentPrimary)) {
515
+ throw new RemoteError("INVALID_RESPONSE", "Cluster metadata currentPrimary must be an object or null");
516
+ }
517
+ const primary = record.currentPrimary;
518
+ if (typeof primary.endpoint !== "string") {
519
+ throw new RemoteError("INVALID_RESPONSE", "Cluster metadata currentPrimary.endpoint must be a string");
520
+ }
521
+ currentPrimary = toDiscoveredServerBaseUrl(primary.endpoint, databaseId);
522
+ }
523
+ const readEndpointsRaw = record.readEndpoints;
524
+ if (readEndpointsRaw !== void 0 && !Array.isArray(readEndpointsRaw)) {
525
+ throw new RemoteError("INVALID_RESPONSE", "Cluster metadata readEndpoints must be an array");
526
+ }
527
+ const readEndpoints = (readEndpointsRaw ?? []).map((endpointInfo) => {
528
+ if (typeof endpointInfo !== "object" || endpointInfo === null || Array.isArray(endpointInfo)) {
529
+ throw new RemoteError("INVALID_RESPONSE", "Cluster metadata read endpoint must be an object");
530
+ }
531
+ const endpoint = endpointInfo;
532
+ if (typeof endpoint.endpoint !== "string") {
533
+ throw new RemoteError("INVALID_RESPONSE", "Cluster metadata read endpoint URL must be a string");
534
+ }
535
+ return {
536
+ url: toDiscoveredServerBaseUrl(endpoint.endpoint, databaseId),
537
+ readConcerns: parseDiscoveredReadConcerns(endpoint.readConcerns)
538
+ };
539
+ });
540
+ return {
541
+ currentPrimary,
542
+ primaryTerm: record.primaryTerm ?? null,
543
+ readEndpoints
544
+ };
545
+ }
416
546
  var SirannonClient = class {
417
547
  baseUrl;
418
548
  wsBaseUrl;
419
549
  transport;
420
550
  headers;
551
+ webSocketProtocols;
421
552
  autoReconnect;
422
553
  reconnectInterval;
423
554
  databases = /* @__PURE__ */ new Map();
424
555
  closed = false;
425
- constructor(url, options) {
426
- this.baseUrl = url.replace(/\/$/, "");
427
- this.wsBaseUrl = this.baseUrl.replace(/^http:\/\//i, "ws://").replace(/^https:\/\//i, "wss://");
428
- this.transport = options?.transport ?? "websocket";
429
- this.headers = options?.headers;
430
- this.autoReconnect = options?.autoReconnect ?? true;
431
- this.reconnectInterval = options?.reconnectInterval ?? 1e3;
556
+ topologyEnabled;
557
+ primaryUrl;
558
+ replicaUrls;
559
+ readPreference;
560
+ discovery;
561
+ readConcern;
562
+ starterEndpoints;
563
+ clusterRouting = /* @__PURE__ */ new Map();
564
+ topologyTransports = /* @__PURE__ */ new Map();
565
+ latencies = [];
566
+ latencyMeasuredAt = 0;
567
+ latencyMeasuring = null;
568
+ LATENCY_TTL_MS = 6e4;
569
+ removedReplicas = /* @__PURE__ */ new Set();
570
+ constructor(urlOrOpts, options) {
571
+ if (isTopologyConfig(urlOrOpts)) {
572
+ const topoOpts = urlOrOpts;
573
+ this.topologyEnabled = true;
574
+ this.primaryUrl = topoOpts.primary ? toBaseUrl(topoOpts.primary) : void 0;
575
+ this.replicaUrls = (topoOpts.replicas ?? []).map(toBaseUrl);
576
+ this.readPreference = topoOpts.readPreference ?? "primary";
577
+ this.discovery = topoOpts.discovery ?? "static";
578
+ this.readConcern = topoOpts.readConcern;
579
+ this.starterEndpoints = (topoOpts.endpoints ?? []).map(toBaseUrl);
580
+ this.baseUrl = this.primaryUrl ?? this.replicaUrls[0] ?? this.starterEndpoints[0] ?? "";
581
+ this.wsBaseUrl = toWsUrl(this.baseUrl);
582
+ this.transport = topoOpts.transport ?? "websocket";
583
+ this.headers = topoOpts.headers;
584
+ this.webSocketProtocols = topoOpts.webSocketProtocols;
585
+ this.autoReconnect = topoOpts.autoReconnect ?? true;
586
+ this.reconnectInterval = topoOpts.reconnectInterval ?? 1e3;
587
+ } else {
588
+ this.topologyEnabled = false;
589
+ this.primaryUrl = void 0;
590
+ this.replicaUrls = [];
591
+ this.readPreference = "primary";
592
+ this.discovery = "static";
593
+ this.readConcern = void 0;
594
+ this.starterEndpoints = [];
595
+ this.baseUrl = toBaseUrl(urlOrOpts);
596
+ this.wsBaseUrl = toWsUrl(this.baseUrl);
597
+ this.transport = options?.transport ?? "websocket";
598
+ this.headers = options?.headers;
599
+ this.webSocketProtocols = options?.webSocketProtocols;
600
+ this.autoReconnect = options?.autoReconnect ?? true;
601
+ this.reconnectInterval = options?.reconnectInterval ?? 1e3;
602
+ }
432
603
  }
433
- /**
434
- * Get a {@link RemoteDatabase} proxy for the given database ID.
435
- * Returns a cached instance if one already exists for this ID.
436
- *
437
- * The underlying transport connection is established lazily on
438
- * the first operation (query, execute, or subscribe).
439
- */
440
604
  database(id) {
441
605
  if (this.closed) {
442
606
  throw new Error("Client is closed");
@@ -452,10 +616,6 @@ var SirannonClient = class {
452
616
  this.databases.set(id, db);
453
617
  return db;
454
618
  }
455
- /**
456
- * Close all database connections and release resources.
457
- * After calling `close()`, new calls to `database()` will throw.
458
- */
459
619
  close() {
460
620
  this.closed = true;
461
621
  const openDatabases = [...this.databases.values()];
@@ -465,15 +625,555 @@ var SirannonClient = class {
465
625
  }
466
626
  }
467
627
  createTransport(databaseId) {
628
+ if (!this.topologyEnabled) {
629
+ return this.createTransportForUrl(this.baseUrl, this.wsBaseUrl, databaseId);
630
+ }
631
+ const transport = new TopologyAwareTransport(databaseId, this);
632
+ this.topologyTransports.set(databaseId, transport);
633
+ return transport;
634
+ }
635
+ createTransportForUrl(baseUrl, wsBaseUrl, databaseId) {
468
636
  const encodedId = encodeURIComponent(databaseId);
469
637
  if (this.transport === "http") {
470
- return new HttpTransport(`${this.baseUrl}/db/${encodedId}`, this.headers);
638
+ return new HttpTransport(`${baseUrl}/db/${encodedId}`, this.headers);
471
639
  }
472
- return new WebSocketTransport(`${this.wsBaseUrl}/db/${encodedId}`, {
640
+ return new WebSocketTransport(`${wsBaseUrl}/db/${encodedId}`, {
473
641
  autoReconnect: this.autoReconnect,
474
- reconnectInterval: this.reconnectInterval
642
+ reconnectInterval: this.reconnectInterval,
643
+ protocols: this.webSocketProtocols
644
+ });
645
+ }
646
+ _createTransportForEndpoint(url, databaseId) {
647
+ const base = toBaseUrl(url);
648
+ const ws = toWsUrl(base);
649
+ return this.createTransportForUrl(base, ws, databaseId);
650
+ }
651
+ async _getReadEndpoint(databaseId, readConcern) {
652
+ if (this.discovery === "coordinator" && databaseId) {
653
+ const routing = await this.ensureClusterRouting(databaseId);
654
+ const concern = readConcern ?? this.readConcern ?? "majority";
655
+ if (concern === "linearizable") {
656
+ if (routing.currentPrimary) return routing.currentPrimary;
657
+ throw new RemoteError("NO_SAFE_PRIMARY", "No current primary is available for linearizable reads");
658
+ }
659
+ const readable = routing.readEndpoints.filter((endpoint) => endpoint.readConcerns.includes(concern));
660
+ const preferredReadable = this.readPreference === "replica" && routing.currentPrimary ? readable.filter((endpoint) => endpoint.url !== routing.currentPrimary) : readable;
661
+ if (this.readPreference !== "primary" && preferredReadable.length > 0) {
662
+ if (this.readPreference === "nearest") {
663
+ return preferredReadable[0].url;
664
+ }
665
+ const idx = Math.floor(Math.random() * preferredReadable.length);
666
+ return preferredReadable[idx].url;
667
+ }
668
+ if (routing.currentPrimary) return routing.currentPrimary;
669
+ const localReadable = routing.readEndpoints.find((endpoint) => endpoint.readConcerns.includes("local"));
670
+ if (localReadable) return localReadable.url;
671
+ throw new RemoteError("ROUTING_ERROR", "No usable read endpoint is available");
672
+ }
673
+ if (this.readPreference === "primary") {
674
+ return this.primaryUrl ?? this.baseUrl;
675
+ }
676
+ const availableReplicas = this.replicaUrls.filter((r) => !this.removedReplicas.has(r));
677
+ if (this.readPreference === "replica") {
678
+ if (availableReplicas.length === 0) {
679
+ return this.primaryUrl ?? this.baseUrl;
680
+ }
681
+ const idx = Math.floor(Math.random() * availableReplicas.length);
682
+ return availableReplicas[idx];
683
+ }
684
+ if (this.readPreference === "nearest") {
685
+ await this.ensureLatencyMeasured();
686
+ const reachable = this.latencies.filter((l) => l.reachable && !this.removedReplicas.has(l.url));
687
+ if (reachable.length === 0) {
688
+ return this.primaryUrl ?? this.baseUrl;
689
+ }
690
+ reachable.sort((a, b) => a.latencyMs - b.latencyMs);
691
+ return reachable[0].url;
692
+ }
693
+ return this.primaryUrl ?? this.baseUrl;
694
+ }
695
+ async _getWriteEndpoint(databaseId) {
696
+ if (this.discovery === "coordinator" && databaseId) {
697
+ const routing = await this.ensureClusterRouting(databaseId);
698
+ if (!routing.currentPrimary) {
699
+ throw new RemoteError("NO_SAFE_PRIMARY", "No current primary is available");
700
+ }
701
+ return routing.currentPrimary;
702
+ }
703
+ return this.primaryUrl ?? this.baseUrl;
704
+ }
705
+ _getReadConcern() {
706
+ return this.readConcern;
707
+ }
708
+ _usesCoordinatorDiscovery() {
709
+ return this.discovery === "coordinator";
710
+ }
711
+ _removeReplica(url) {
712
+ this.removedReplicas.add(url);
713
+ }
714
+ async ensureLatencyMeasured() {
715
+ const now = Date.now();
716
+ if (this.latencies.length > 0 && now - this.latencyMeasuredAt < this.LATENCY_TTL_MS) {
717
+ return;
718
+ }
719
+ if (this.latencyMeasuring) {
720
+ await this.latencyMeasuring;
721
+ return;
722
+ }
723
+ this.latencyMeasuring = this.measureLatencies();
724
+ try {
725
+ await this.latencyMeasuring;
726
+ } finally {
727
+ this.latencyMeasuring = null;
728
+ }
729
+ }
730
+ async measureLatencies() {
731
+ const allEndpoints = [];
732
+ if (this.primaryUrl) {
733
+ allEndpoints.push(this.primaryUrl);
734
+ }
735
+ for (const r of this.replicaUrls) {
736
+ allEndpoints.push(r);
737
+ }
738
+ const results = await Promise.all(
739
+ allEndpoints.map(async (url) => {
740
+ const start = performance.now();
741
+ const controller = new AbortController();
742
+ const timeout = setTimeout(() => controller.abort(), 5e3);
743
+ const unrefable = timeout;
744
+ if (typeof unrefable.unref === "function") {
745
+ unrefable.unref();
746
+ }
747
+ try {
748
+ const init = { signal: controller.signal };
749
+ const response = await fetch(`${url}/health`, init);
750
+ if (!response.ok) {
751
+ return { url, latencyMs: Number.MAX_SAFE_INTEGER, reachable: false };
752
+ }
753
+ return { url, latencyMs: performance.now() - start, reachable: true };
754
+ } catch {
755
+ return { url, latencyMs: Number.MAX_SAFE_INTEGER, reachable: false };
756
+ } finally {
757
+ clearTimeout(timeout);
758
+ }
759
+ })
760
+ );
761
+ this.latencies = results;
762
+ this.latencyMeasuredAt = Date.now();
763
+ }
764
+ async _refreshClusterRouting(databaseId) {
765
+ const candidates = this.clusterDiscoveryCandidates(databaseId);
766
+ const encodedId = encodeURIComponent(databaseId);
767
+ for (const endpoint of candidates) {
768
+ const base = toServerBaseUrl(endpoint, databaseId);
769
+ const controller = new AbortController();
770
+ const timeout = setTimeout(() => controller.abort(), CLUSTER_DISCOVERY_FETCH_TIMEOUT_MS);
771
+ const unrefable = timeout;
772
+ unrefable.unref?.();
773
+ let next = null;
774
+ try {
775
+ const response = await fetch(`${base}/db/${encodedId}/cluster`, {
776
+ headers: this.headers,
777
+ signal: controller.signal
778
+ });
779
+ if (!response.ok) {
780
+ continue;
781
+ }
782
+ const data = await response.json();
783
+ next = parseClusterRouting(data, databaseId);
784
+ } catch (err) {
785
+ if (err instanceof RemoteError && err.code === "INVALID_RESPONSE") {
786
+ throw err;
787
+ }
788
+ } finally {
789
+ clearTimeout(timeout);
790
+ }
791
+ if (!next) {
792
+ continue;
793
+ }
794
+ const previous = this.clusterRouting.get(databaseId);
795
+ this.clusterRouting.set(databaseId, next);
796
+ if (clusterRoutingChanged(previous, next)) {
797
+ try {
798
+ await this.notifyClusterRoutingChanged(databaseId);
799
+ } catch (err) {
800
+ if (previous) {
801
+ this.clusterRouting.set(databaseId, previous);
802
+ } else {
803
+ this.clusterRouting.delete(databaseId);
804
+ }
805
+ throw err;
806
+ }
807
+ }
808
+ return;
809
+ }
810
+ throw new RemoteError("ROUTING_ERROR", `Could not discover cluster routing for database '${databaseId}'`);
811
+ }
812
+ async ensureClusterRouting(databaseId) {
813
+ const existing = this.clusterRouting.get(databaseId);
814
+ if (existing) return existing;
815
+ await this._refreshClusterRouting(databaseId);
816
+ const refreshed = this.clusterRouting.get(databaseId);
817
+ if (!refreshed) {
818
+ throw new RemoteError("ROUTING_ERROR", `Could not discover cluster routing for database '${databaseId}'`);
819
+ }
820
+ return refreshed;
821
+ }
822
+ clusterDiscoveryCandidates(databaseId) {
823
+ const candidates = /* @__PURE__ */ new Set();
824
+ for (const endpoint of this.starterEndpoints) candidates.add(endpoint);
825
+ if (this.primaryUrl) candidates.add(this.primaryUrl);
826
+ for (const endpoint of this.replicaUrls) candidates.add(endpoint);
827
+ const existing = this.clusterRouting.get(databaseId);
828
+ if (existing?.currentPrimary) candidates.add(existing.currentPrimary);
829
+ for (const endpoint of existing?.readEndpoints ?? []) candidates.add(endpoint.url);
830
+ if (this.baseUrl) candidates.add(this.baseUrl);
831
+ return [...candidates];
832
+ }
833
+ _unregisterTopologyTransport(databaseId, transport) {
834
+ if (this.topologyTransports.get(databaseId) === transport) {
835
+ this.topologyTransports.delete(databaseId);
836
+ }
837
+ }
838
+ async notifyClusterRoutingChanged(databaseId) {
839
+ const transport = this.topologyTransports.get(databaseId);
840
+ if (!transport) return;
841
+ await transport._handleClusterRoutingChanged();
842
+ }
843
+ };
844
+ var TopologyAwareTransport = class {
845
+ databaseId;
846
+ client;
847
+ closed = false;
848
+ readTransport = null;
849
+ writeTransport = null;
850
+ subscriptionTransport = null;
851
+ readTransportRequest = null;
852
+ writeTransportRequest = null;
853
+ subscriptionTransportRequest = null;
854
+ subscriptionOperation = Promise.resolve();
855
+ activeSubscriptions = /* @__PURE__ */ new Map();
856
+ nextSubscriptionId = 0;
857
+ currentReadUrl = "";
858
+ currentWriteUrl = "";
859
+ currentSubscriptionUrl = "";
860
+ constructor(databaseId, client) {
861
+ this.databaseId = databaseId;
862
+ this.client = client;
863
+ }
864
+ async query(sql, params) {
865
+ const readConcern = this.client._getReadConcern();
866
+ const transport = await this.getReadTransport(readConcern);
867
+ const endpointUsed = this.currentReadUrl;
868
+ try {
869
+ return await transport.query(sql, params, readConcern ? { level: readConcern } : void 0);
870
+ } catch (err) {
871
+ if (this.client._usesCoordinatorDiscovery() && shouldRefreshRouting(err)) {
872
+ await this.client._refreshClusterRouting(this.databaseId);
873
+ this.readTransport = null;
874
+ this.currentReadUrl = "";
875
+ const refreshed = await this.getReadTransport(readConcern);
876
+ return refreshed.query(sql, params, readConcern ? { level: readConcern } : void 0);
877
+ }
878
+ const isTransportError = err instanceof Error && (err.name !== "RemoteError" || err.code === "CONNECTION_ERROR");
879
+ const writeEndpoint = await this.client._getWriteEndpoint(this.databaseId);
880
+ if (isTransportError && endpointUsed && endpointUsed !== writeEndpoint) {
881
+ this.client._removeReplica(endpointUsed);
882
+ if (this.currentReadUrl === endpointUsed) {
883
+ this.readTransport = null;
884
+ this.currentReadUrl = "";
885
+ }
886
+ const fallback = await this.getReadTransport();
887
+ return fallback.query(sql, params);
888
+ }
889
+ throw err;
890
+ }
891
+ }
892
+ async execute(sql, params) {
893
+ const transport = await this.getWriteTransport();
894
+ try {
895
+ return await transport.execute(sql, params);
896
+ } catch (err) {
897
+ if (this.client._usesCoordinatorDiscovery() && shouldRefreshRouting(err)) {
898
+ await this.client._refreshClusterRouting(this.databaseId);
899
+ this.writeTransport = null;
900
+ this.currentWriteUrl = "";
901
+ }
902
+ throw err;
903
+ }
904
+ }
905
+ async transaction(statements) {
906
+ const transport = await this.getWriteTransport();
907
+ try {
908
+ return await transport.transaction(statements);
909
+ } catch (err) {
910
+ if (this.client._usesCoordinatorDiscovery() && shouldRefreshRouting(err)) {
911
+ await this.client._refreshClusterRouting(this.databaseId);
912
+ this.writeTransport = null;
913
+ this.currentWriteUrl = "";
914
+ }
915
+ throw err;
916
+ }
917
+ }
918
+ async subscribe(table, filter, callback) {
919
+ try {
920
+ return await this.subscribeOnCurrentEndpoint(table, filter, callback);
921
+ } catch (err) {
922
+ if (this.client._usesCoordinatorDiscovery() && shouldRefreshRouting(err)) {
923
+ const hadActiveSubscriptions = this.activeSubscriptions.size > 0;
924
+ if (!hadActiveSubscriptions) {
925
+ this.closeSubscriptionTransport();
926
+ }
927
+ await this.client._refreshClusterRouting(this.databaseId);
928
+ if (!hadActiveSubscriptions) {
929
+ this.closeSubscriptionTransport();
930
+ }
931
+ return this.subscribeOnCurrentEndpoint(table, filter, callback);
932
+ }
933
+ throw err;
934
+ }
935
+ }
936
+ async _handleClusterRoutingChanged() {
937
+ if (!this.client._usesCoordinatorDiscovery()) return;
938
+ if (this.activeSubscriptions.size === 0) return;
939
+ await this.withSubscriptionOperation(() => this.migrateSubscriptionsToCurrentEndpoint());
940
+ }
941
+ close() {
942
+ this.closed = true;
943
+ const sameTransport = this.readTransport !== null && this.readTransport === this.writeTransport;
944
+ const subscriptionIsRead = this.subscriptionTransport !== null && this.subscriptionTransport === this.readTransport;
945
+ const subscriptionIsWrite = this.subscriptionTransport !== null && this.subscriptionTransport === this.writeTransport;
946
+ for (const subscription of this.activeSubscriptions.values()) {
947
+ subscription.active = false;
948
+ subscription.remote = null;
949
+ }
950
+ this.activeSubscriptions.clear();
951
+ if (this.readTransport) {
952
+ this.readTransport.close();
953
+ this.readTransport = null;
954
+ }
955
+ if (this.writeTransport && !sameTransport) {
956
+ this.writeTransport.close();
957
+ }
958
+ this.writeTransport = null;
959
+ if (this.subscriptionTransport && !subscriptionIsRead && !subscriptionIsWrite) {
960
+ this.subscriptionTransport.close();
961
+ }
962
+ this.subscriptionTransport = null;
963
+ this.currentSubscriptionUrl = "";
964
+ this.client._unregisterTopologyTransport(this.databaseId, this);
965
+ }
966
+ async subscribeOnCurrentEndpoint(table, filter, callback) {
967
+ return this.withSubscriptionOperation(async () => {
968
+ const transport = await this.getSubscriptionTransport(this.client._getReadConcern());
969
+ const remote = await transport.subscribe(table, filter, callback);
970
+ const subscription = {
971
+ id: ++this.nextSubscriptionId,
972
+ table,
973
+ filter,
974
+ callback,
975
+ remote,
976
+ active: true
977
+ };
978
+ this.activeSubscriptions.set(subscription.id, subscription);
979
+ return this.createSubscriptionHandle(subscription);
980
+ });
981
+ }
982
+ createSubscriptionHandle(subscription) {
983
+ return {
984
+ unsubscribe: () => {
985
+ if (!subscription.active) return;
986
+ subscription.active = false;
987
+ this.activeSubscriptions.delete(subscription.id);
988
+ const remote = subscription.remote;
989
+ subscription.remote = null;
990
+ remote?.unsubscribe();
991
+ }
992
+ };
993
+ }
994
+ async migrateSubscriptionsToCurrentEndpoint() {
995
+ this.assertOpen();
996
+ const subscriptions = [...this.activeSubscriptions.values()].filter((subscription) => subscription.active);
997
+ if (subscriptions.length === 0) return;
998
+ const readConcern = this.client._getReadConcern();
999
+ const endpoint = await this.client._getReadEndpoint(this.databaseId, readConcern);
1000
+ this.assertOpen();
1001
+ if (this.subscriptionTransport && this.currentSubscriptionUrl === endpoint) {
1002
+ return;
1003
+ }
1004
+ const nextTransport = this.client._createTransportForEndpoint(endpoint, this.databaseId);
1005
+ const nextSubscriptions = /* @__PURE__ */ new Map();
1006
+ try {
1007
+ for (const subscription of subscriptions) {
1008
+ if (!subscription.active) continue;
1009
+ const remote = await nextTransport.subscribe(subscription.table, subscription.filter, subscription.callback);
1010
+ if (!subscription.active) {
1011
+ remote.unsubscribe();
1012
+ continue;
1013
+ }
1014
+ nextSubscriptions.set(subscription.id, remote);
1015
+ }
1016
+ } catch (err) {
1017
+ for (const remote of nextSubscriptions.values()) {
1018
+ remote.unsubscribe();
1019
+ }
1020
+ nextTransport.close();
1021
+ throw toSubscriptionRoutingError(err, this.databaseId);
1022
+ }
1023
+ const oldTransport = this.subscriptionTransport;
1024
+ this.subscriptionTransport = nextTransport;
1025
+ this.currentSubscriptionUrl = endpoint;
1026
+ for (const subscription of subscriptions) {
1027
+ const nextRemote = nextSubscriptions.get(subscription.id);
1028
+ if (!nextRemote) continue;
1029
+ const previousRemote = subscription.remote;
1030
+ if (!subscription.active) {
1031
+ nextRemote.unsubscribe();
1032
+ continue;
1033
+ }
1034
+ subscription.remote = nextRemote;
1035
+ previousRemote?.unsubscribe();
1036
+ }
1037
+ if (oldTransport) {
1038
+ oldTransport.close();
1039
+ }
1040
+ }
1041
+ async getReadTransport(readConcern) {
1042
+ this.assertOpen();
1043
+ while (this.readTransportRequest) {
1044
+ await this.readTransportRequest.catch(() => void 0);
1045
+ this.assertOpen();
1046
+ }
1047
+ const request = this.resolveReadTransport(readConcern);
1048
+ this.readTransportRequest = request;
1049
+ try {
1050
+ return await request;
1051
+ } finally {
1052
+ if (this.readTransportRequest === request) {
1053
+ this.readTransportRequest = null;
1054
+ }
1055
+ }
1056
+ }
1057
+ async resolveReadTransport(readConcern) {
1058
+ const endpoint = await this.client._getReadEndpoint(this.databaseId, readConcern);
1059
+ this.assertOpen();
1060
+ if (this.readTransport && this.currentReadUrl === endpoint) {
1061
+ return this.readTransport;
1062
+ }
1063
+ const nextTransport = this.client._createTransportForEndpoint(endpoint, this.databaseId);
1064
+ const oldTransport = this.readTransport;
1065
+ this.readTransport = nextTransport;
1066
+ this.currentReadUrl = endpoint;
1067
+ if (oldTransport) {
1068
+ oldTransport.close();
1069
+ }
1070
+ return this.readTransport;
1071
+ }
1072
+ async getWriteTransport() {
1073
+ this.assertOpen();
1074
+ while (this.writeTransportRequest) {
1075
+ await this.writeTransportRequest.catch(() => void 0);
1076
+ this.assertOpen();
1077
+ }
1078
+ const request = this.resolveWriteTransport();
1079
+ this.writeTransportRequest = request;
1080
+ try {
1081
+ return await request;
1082
+ } finally {
1083
+ if (this.writeTransportRequest === request) {
1084
+ this.writeTransportRequest = null;
1085
+ }
1086
+ }
1087
+ }
1088
+ async resolveWriteTransport() {
1089
+ const endpoint = await this.client._getWriteEndpoint(this.databaseId);
1090
+ this.assertOpen();
1091
+ if (this.writeTransport && this.currentWriteUrl === endpoint) {
1092
+ return this.writeTransport;
1093
+ }
1094
+ const nextTransport = this.client._createTransportForEndpoint(endpoint, this.databaseId);
1095
+ const oldTransport = this.writeTransport;
1096
+ this.writeTransport = nextTransport;
1097
+ this.currentWriteUrl = endpoint;
1098
+ if (oldTransport) {
1099
+ oldTransport.close();
1100
+ }
1101
+ return this.writeTransport;
1102
+ }
1103
+ async getSubscriptionTransport(readConcern) {
1104
+ this.assertOpen();
1105
+ if (this.subscriptionTransport) {
1106
+ return this.subscriptionTransport;
1107
+ }
1108
+ while (this.subscriptionTransportRequest) {
1109
+ await this.subscriptionTransportRequest.catch(() => void 0);
1110
+ this.assertOpen();
1111
+ if (this.subscriptionTransport) {
1112
+ return this.subscriptionTransport;
1113
+ }
1114
+ }
1115
+ const request = this.resolveSubscriptionTransport(readConcern);
1116
+ this.subscriptionTransportRequest = request;
1117
+ try {
1118
+ return await request;
1119
+ } finally {
1120
+ if (this.subscriptionTransportRequest === request) {
1121
+ this.subscriptionTransportRequest = null;
1122
+ }
1123
+ }
1124
+ }
1125
+ async resolveSubscriptionTransport(readConcern) {
1126
+ if (this.subscriptionTransport) {
1127
+ return this.subscriptionTransport;
1128
+ }
1129
+ const endpoint = await this.client._getReadEndpoint(this.databaseId, readConcern);
1130
+ this.assertOpen();
1131
+ if (this.subscriptionTransport) {
1132
+ return this.subscriptionTransport;
1133
+ }
1134
+ this.subscriptionTransport = this.client._createTransportForEndpoint(endpoint, this.databaseId);
1135
+ this.currentSubscriptionUrl = endpoint;
1136
+ return this.subscriptionTransport;
1137
+ }
1138
+ closeSubscriptionTransport() {
1139
+ if (this.subscriptionTransport) {
1140
+ this.subscriptionTransport.close();
1141
+ this.subscriptionTransport = null;
1142
+ }
1143
+ this.currentSubscriptionUrl = "";
1144
+ }
1145
+ async withSubscriptionOperation(operation) {
1146
+ const previous = this.subscriptionOperation;
1147
+ let release = () => {
1148
+ };
1149
+ this.subscriptionOperation = new Promise((resolve) => {
1150
+ release = resolve;
475
1151
  });
1152
+ await previous.catch(() => void 0);
1153
+ try {
1154
+ return await operation();
1155
+ } finally {
1156
+ release();
1157
+ }
1158
+ }
1159
+ assertOpen() {
1160
+ if (this.closed) {
1161
+ throw new RemoteError("TRANSPORT_ERROR", "Transport is closed");
1162
+ }
476
1163
  }
477
1164
  };
1165
+ function toSubscriptionRoutingError(err, databaseId) {
1166
+ const detail = err instanceof Error ? err.message : String(err);
1167
+ return new RemoteError(
1168
+ "ROUTING_ERROR",
1169
+ `Could not re-establish active subscriptions on refreshed routing for database '${databaseId}': ${detail}`
1170
+ );
1171
+ }
1172
+ function shouldRefreshRouting(err) {
1173
+ if (!(err instanceof RemoteError)) {
1174
+ return false;
1175
+ }
1176
+ return err.code === "STALE_PRIMARY" || err.code === "AUTHORITY_LOST" || err.code === "COORDINATOR_UNAVAILABLE" || err.code === "NO_SAFE_PRIMARY" || err.code === "CONNECTION_ERROR";
1177
+ }
478
1178
 
479
1179
  export { HttpTransport, RemoteDatabase, RemoteError, RemoteSubscriptionBuilderImpl, SirannonClient, WebSocketTransport };