@delali/sirannon-db 0.1.7 → 0.2.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/README.md +117 -969
- package/dist/backup-scheduler/index.d.ts +1 -1
- package/dist/backup-scheduler/index.mjs +2 -2
- package/dist/baseline-Br77Fnhb.d.ts +6 -0
- package/dist/{change-tracker-CbmaMO-N.d.ts → change-tracker-DKRVUC3l.d.ts} +5 -8
- package/dist/{chunk-DJLX6CAE.mjs → chunk-4ISB7XMA.mjs} +2 -1
- package/dist/chunk-5NOIGN5Y.mjs +18 -0
- package/dist/chunk-67M7KAH6.mjs +90 -0
- package/dist/{chunk-GEZUUIKV.mjs → chunk-D7LAYTKN.mjs} +5 -2
- package/dist/{chunk-4IGMIJQK.mjs → chunk-EBJXPQQO.mjs} +59 -20
- package/dist/{chunk-BNUTBHHH.mjs → chunk-FHWTZFI4.mjs} +1 -1
- package/dist/chunk-GVCNMPOS.mjs +130 -0
- package/dist/chunk-H237TXZW.mjs +1407 -0
- package/dist/{chunk-HHRMRFFR.mjs → chunk-H6PIVVDN.mjs} +353 -142
- package/dist/chunk-HR5CWTLC.mjs +661 -0
- package/dist/chunk-JU64Y7HM.mjs +299 -0
- package/dist/{chunk-CW6S3WL5.mjs → chunk-JZGINXTN.mjs} +7 -5
- package/dist/{chunk-CJLYFDP5.mjs → chunk-LFZ37BSX.mjs} +1 -1
- package/dist/chunk-LNY2VVHE.mjs +18 -0
- package/dist/chunk-NVQS53NT.mjs +665 -0
- package/dist/{chunk-H5AB6NIR.mjs → chunk-O7SLN3GI.mjs} +2 -15
- package/dist/chunk-OQVZBEBY.mjs +283 -0
- package/dist/{chunk-VLTICJOD.mjs → chunk-PIKHN33N.mjs} +1 -1
- package/dist/chunk-TJF5GZSV.mjs +187 -0
- package/dist/chunk-TUD5CJ76.mjs +126 -0
- package/dist/{chunk-YPYVQJ4C.mjs → chunk-UC3SCMIN.mjs} +9 -1
- package/dist/client/index.d.ts +216 -321
- package/dist/client/index.mjs +1192 -1342
- package/dist/client/topology.d.ts +61 -0
- package/dist/client/topology.mjs +682 -0
- package/dist/client-base-CLWmH5Ln.d.ts +234 -0
- package/dist/codegen/cli.d.ts +2 -0
- package/dist/codegen/cli.mjs +11 -0
- package/dist/codegen/index.d.ts +30 -0
- package/dist/codegen/index.mjs +5 -0
- package/dist/core/index.d.ts +37 -16
- package/dist/core/index.mjs +2322 -298
- package/dist/core/writer-worker.mjs +28 -6
- package/dist/{database-DuGp0Rtr.d.ts → database-BY0L5Q2n.d.ts} +59 -14
- package/dist/driver/better-sqlite3.d.ts +1 -1
- package/dist/driver/better-sqlite3.mjs +6 -6
- package/dist/driver/node.d.ts +1 -1
- package/dist/driver/node.mjs +6 -6
- package/dist/driver/wa-sqlite.d.ts +1 -136
- package/dist/{errors-5Nf5ZAEC.d.ts → errors-Bw5MdNCu.d.ts} +5 -1
- package/dist/file-migrations/index.d.ts +7 -5
- package/dist/file-migrations/index.mjs +42 -37
- package/dist/operation-registry-9DcvxcE5.d.ts +45 -0
- package/dist/primary-wins-DPAm2AKG.d.ts +61 -0
- package/dist/protocol-6KrSq2Hy.d.ts +66 -0
- package/dist/react/index.d.ts +20 -0
- package/dist/react/index.mjs +137 -0
- package/dist/replication/coordinator/etcd.d.ts +5 -3
- package/dist/replication/coordinator/etcd.mjs +554 -462
- package/dist/replication/index.d.ts +51 -105
- package/dist/replication/index.mjs +1415 -2061
- package/dist/server/index.d.ts +111 -87
- package/dist/server/index.mjs +1917 -489
- package/dist/server-options-1JHu8pid.d.ts +191 -0
- package/dist/{sirannon-4SspRvP5.d.ts → sirannon-DaQSyhbJ.d.ts} +7 -2
- package/dist/transport/grpc.d.ts +7 -4
- package/dist/transport/grpc.mjs +191 -97
- package/dist/transport/memory.d.ts +5 -134
- package/dist/transport/memory.mjs +3 -1
- package/dist/{types-BsjobKbl.d.ts → types-B7gmEsZW.d.ts} +8 -61
- package/dist/{types-BEu1I_9_.d.ts → types-BsVabqSI.d.ts} +1 -1
- package/dist/types-CL6piSnD.d.ts +43 -0
- package/dist/types-C_D8IhpO.d.ts +60 -0
- package/dist/{types-D_hQW1hr.d.ts → types-zhnRXrsb.d.ts} +55 -165
- package/package.json +44 -6
- package/dist/chunk-TKGHYWQ6.mjs +0 -35
- package/dist/types-D4p4UyDK.d.ts +0 -26
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
import { DatabaseClient, toBaseUrl, createEndpointTransport, RemoteError, toServerBaseUrl, unrefTimer } from '../chunk-H237TXZW.mjs';
|
|
2
|
+
|
|
3
|
+
// src/client/cluster-routing.ts
|
|
4
|
+
function clusterRoutingFingerprint(state) {
|
|
5
|
+
const readEndpoints = state.readEndpoints.map((endpoint) => ({
|
|
6
|
+
url: endpoint.url,
|
|
7
|
+
readConcerns: [...endpoint.readConcerns].sort()
|
|
8
|
+
})).sort((left, right) => left.url.localeCompare(right.url));
|
|
9
|
+
return JSON.stringify({
|
|
10
|
+
currentPrimary: state.currentPrimary,
|
|
11
|
+
primaryTerm: state.primaryTerm,
|
|
12
|
+
readEndpoints
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function clusterRoutingChanged(previous, next) {
|
|
16
|
+
return previous === void 0 || clusterRoutingFingerprint(previous) !== clusterRoutingFingerprint(next);
|
|
17
|
+
}
|
|
18
|
+
function isReadConcernLevel(value) {
|
|
19
|
+
return value === "local" || value === "majority" || value === "linearizable";
|
|
20
|
+
}
|
|
21
|
+
function parseDiscoveredReadConcerns(value) {
|
|
22
|
+
if (!Array.isArray(value)) {
|
|
23
|
+
throw new RemoteError("INVALID_RESPONSE", "Cluster metadata readConcerns must be an array");
|
|
24
|
+
}
|
|
25
|
+
const concerns = [];
|
|
26
|
+
for (const concern of value) {
|
|
27
|
+
if (!isReadConcernLevel(concern)) {
|
|
28
|
+
throw new RemoteError("INVALID_RESPONSE", "Cluster metadata contains an invalid read concern");
|
|
29
|
+
}
|
|
30
|
+
if (!concerns.includes(concern)) {
|
|
31
|
+
concerns.push(concern);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return concerns;
|
|
35
|
+
}
|
|
36
|
+
function toDiscoveredServerBaseUrl(endpoint, databaseId) {
|
|
37
|
+
try {
|
|
38
|
+
return toServerBaseUrl(endpoint, databaseId);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
41
|
+
throw new RemoteError("INVALID_RESPONSE", `Cluster metadata contains an unsafe endpoint: ${message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function parseClusterRouting(data, databaseId) {
|
|
45
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
46
|
+
throw new RemoteError("INVALID_RESPONSE", "Cluster metadata must be an object");
|
|
47
|
+
}
|
|
48
|
+
const record = data;
|
|
49
|
+
if (record.databaseId !== databaseId) {
|
|
50
|
+
throw new RemoteError("INVALID_RESPONSE", "Cluster metadata database id does not match the request");
|
|
51
|
+
}
|
|
52
|
+
if (record.primaryTerm !== void 0 && typeof record.primaryTerm !== "string") {
|
|
53
|
+
throw new RemoteError("INVALID_RESPONSE", "Cluster metadata primaryTerm must be a string");
|
|
54
|
+
}
|
|
55
|
+
let currentPrimary = null;
|
|
56
|
+
if (record.currentPrimary !== void 0 && record.currentPrimary !== null) {
|
|
57
|
+
if (typeof record.currentPrimary !== "object" || Array.isArray(record.currentPrimary)) {
|
|
58
|
+
throw new RemoteError("INVALID_RESPONSE", "Cluster metadata currentPrimary must be an object or null");
|
|
59
|
+
}
|
|
60
|
+
const primary = record.currentPrimary;
|
|
61
|
+
if (typeof primary.endpoint !== "string") {
|
|
62
|
+
throw new RemoteError("INVALID_RESPONSE", "Cluster metadata currentPrimary.endpoint must be a string");
|
|
63
|
+
}
|
|
64
|
+
currentPrimary = toDiscoveredServerBaseUrl(primary.endpoint, databaseId);
|
|
65
|
+
}
|
|
66
|
+
const readEndpointsRaw = record.readEndpoints;
|
|
67
|
+
if (readEndpointsRaw !== void 0 && !Array.isArray(readEndpointsRaw)) {
|
|
68
|
+
throw new RemoteError("INVALID_RESPONSE", "Cluster metadata readEndpoints must be an array");
|
|
69
|
+
}
|
|
70
|
+
const readEndpoints = (readEndpointsRaw ?? []).map((endpointInfo) => {
|
|
71
|
+
if (typeof endpointInfo !== "object" || endpointInfo === null || Array.isArray(endpointInfo)) {
|
|
72
|
+
throw new RemoteError("INVALID_RESPONSE", "Cluster metadata read endpoint must be an object");
|
|
73
|
+
}
|
|
74
|
+
const endpoint = endpointInfo;
|
|
75
|
+
if (typeof endpoint.endpoint !== "string") {
|
|
76
|
+
throw new RemoteError("INVALID_RESPONSE", "Cluster metadata read endpoint URL must be a string");
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
url: toDiscoveredServerBaseUrl(endpoint.endpoint, databaseId),
|
|
80
|
+
readConcerns: parseDiscoveredReadConcerns(endpoint.readConcerns)
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
return {
|
|
84
|
+
currentPrimary,
|
|
85
|
+
primaryTerm: record.primaryTerm ?? null,
|
|
86
|
+
readEndpoints
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function shouldRefreshRouting(err) {
|
|
90
|
+
if (!(err instanceof RemoteError)) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
return err.code === "STALE_PRIMARY" || err.code === "AUTHORITY_LOST" || err.code === "COORDINATOR_UNAVAILABLE" || err.code === "NO_SAFE_PRIMARY" || err.code === "CONNECTION_ERROR";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/client/topology-subscriptions.ts
|
|
97
|
+
var TopologySubscriptionSet = class {
|
|
98
|
+
constructor(databaseId, routing, assertOpen) {
|
|
99
|
+
this.databaseId = databaseId;
|
|
100
|
+
this.routing = routing;
|
|
101
|
+
this.assertOpen = assertOpen;
|
|
102
|
+
}
|
|
103
|
+
transport = null;
|
|
104
|
+
transportRequest = null;
|
|
105
|
+
operation = Promise.resolve();
|
|
106
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
107
|
+
nextSubscriptionId = 0;
|
|
108
|
+
currentUrl = "";
|
|
109
|
+
get size() {
|
|
110
|
+
return this.subscriptions.size;
|
|
111
|
+
}
|
|
112
|
+
async subscribe(table, filter, callback, options) {
|
|
113
|
+
return this.track(
|
|
114
|
+
(transport) => transport.subscribe(table, filter, callback, options),
|
|
115
|
+
(transport) => transport.subscribe(table, filter, callback, { onReset: options?.onReset })
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
async subscribeLive(name, args, handlers, registryDigest) {
|
|
119
|
+
const open = (transport) => transport.liveSubscribe(name, args, handlers, registryDigest);
|
|
120
|
+
return this.track(open, open);
|
|
121
|
+
}
|
|
122
|
+
async track(open, reopen) {
|
|
123
|
+
try {
|
|
124
|
+
return await this.subscribeOnCurrentEndpoint(open, reopen);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
if (this.routing._usesCoordinatorDiscovery() && shouldRefreshRouting(err)) {
|
|
127
|
+
const hadActiveSubscriptions = this.subscriptions.size > 0;
|
|
128
|
+
if (!hadActiveSubscriptions) {
|
|
129
|
+
this.closeTransport();
|
|
130
|
+
}
|
|
131
|
+
await this.routing._refreshClusterRouting(this.databaseId);
|
|
132
|
+
if (!hadActiveSubscriptions) {
|
|
133
|
+
this.closeTransport();
|
|
134
|
+
}
|
|
135
|
+
return this.subscribeOnCurrentEndpoint(open, reopen);
|
|
136
|
+
}
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async handleRoutingChanged() {
|
|
141
|
+
if (!this.routing._usesCoordinatorDiscovery()) return;
|
|
142
|
+
if (this.subscriptions.size === 0) return;
|
|
143
|
+
await this.withOperation(() => this.migrateToCurrentEndpoint());
|
|
144
|
+
}
|
|
145
|
+
closeAll() {
|
|
146
|
+
for (const subscription of this.subscriptions.values()) {
|
|
147
|
+
subscription.active = false;
|
|
148
|
+
subscription.remote = null;
|
|
149
|
+
}
|
|
150
|
+
this.subscriptions.clear();
|
|
151
|
+
this.closeTransport();
|
|
152
|
+
}
|
|
153
|
+
async subscribeOnCurrentEndpoint(open, reopen) {
|
|
154
|
+
return this.withOperation(async () => {
|
|
155
|
+
const transport = await this.getTransport(this.routing._getReadConcern());
|
|
156
|
+
const remote = await open(transport);
|
|
157
|
+
const subscription = {
|
|
158
|
+
id: ++this.nextSubscriptionId,
|
|
159
|
+
reopen,
|
|
160
|
+
remote,
|
|
161
|
+
active: true
|
|
162
|
+
};
|
|
163
|
+
this.subscriptions.set(subscription.id, subscription);
|
|
164
|
+
return this.createSubscriptionHandle(subscription);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
createSubscriptionHandle(subscription) {
|
|
168
|
+
return {
|
|
169
|
+
unsubscribe: () => {
|
|
170
|
+
if (!subscription.active) return;
|
|
171
|
+
subscription.active = false;
|
|
172
|
+
this.subscriptions.delete(subscription.id);
|
|
173
|
+
const remote = subscription.remote;
|
|
174
|
+
subscription.remote = null;
|
|
175
|
+
remote?.unsubscribe();
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
async migrateToCurrentEndpoint() {
|
|
180
|
+
this.assertOpen();
|
|
181
|
+
const subscriptions = [...this.subscriptions.values()].filter((subscription) => subscription.active);
|
|
182
|
+
if (subscriptions.length === 0) return;
|
|
183
|
+
const readConcern = this.routing._getReadConcern();
|
|
184
|
+
const endpoint = await this.routing._getReadEndpoint(this.databaseId, readConcern);
|
|
185
|
+
this.assertOpen();
|
|
186
|
+
if (this.transport && this.currentUrl === endpoint) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const nextTransport = this.routing._createTransportForEndpoint(endpoint, this.databaseId);
|
|
190
|
+
const nextSubscriptions = /* @__PURE__ */ new Map();
|
|
191
|
+
try {
|
|
192
|
+
for (const subscription of subscriptions) {
|
|
193
|
+
if (!subscription.active) continue;
|
|
194
|
+
const remote = await subscription.reopen(nextTransport);
|
|
195
|
+
if (!subscription.active) {
|
|
196
|
+
remote.unsubscribe();
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
nextSubscriptions.set(subscription.id, remote);
|
|
200
|
+
}
|
|
201
|
+
} catch (err) {
|
|
202
|
+
for (const remote of nextSubscriptions.values()) {
|
|
203
|
+
remote.unsubscribe();
|
|
204
|
+
}
|
|
205
|
+
nextTransport.close();
|
|
206
|
+
throw toSubscriptionRoutingError(err, this.databaseId);
|
|
207
|
+
}
|
|
208
|
+
const oldTransport = this.transport;
|
|
209
|
+
this.transport = nextTransport;
|
|
210
|
+
this.currentUrl = endpoint;
|
|
211
|
+
for (const subscription of subscriptions) {
|
|
212
|
+
const nextRemote = nextSubscriptions.get(subscription.id);
|
|
213
|
+
if (!nextRemote) continue;
|
|
214
|
+
const previousRemote = subscription.remote;
|
|
215
|
+
if (!subscription.active) {
|
|
216
|
+
nextRemote.unsubscribe();
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
subscription.remote = nextRemote;
|
|
220
|
+
previousRemote?.unsubscribe();
|
|
221
|
+
}
|
|
222
|
+
if (oldTransport) {
|
|
223
|
+
oldTransport.close();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async getTransport(readConcern) {
|
|
227
|
+
this.assertOpen();
|
|
228
|
+
if (this.transport) {
|
|
229
|
+
return this.transport;
|
|
230
|
+
}
|
|
231
|
+
while (this.transportRequest) {
|
|
232
|
+
await this.transportRequest.catch(() => void 0);
|
|
233
|
+
this.assertOpen();
|
|
234
|
+
if (this.transport) {
|
|
235
|
+
return this.transport;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const request = this.resolveTransport(readConcern);
|
|
239
|
+
this.transportRequest = request;
|
|
240
|
+
try {
|
|
241
|
+
return await request;
|
|
242
|
+
} finally {
|
|
243
|
+
if (this.transportRequest === request) {
|
|
244
|
+
this.transportRequest = null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async resolveTransport(readConcern) {
|
|
249
|
+
if (this.transport) {
|
|
250
|
+
return this.transport;
|
|
251
|
+
}
|
|
252
|
+
const endpoint = await this.routing._getReadEndpoint(this.databaseId, readConcern);
|
|
253
|
+
this.assertOpen();
|
|
254
|
+
if (this.transport) {
|
|
255
|
+
return this.transport;
|
|
256
|
+
}
|
|
257
|
+
this.transport = this.routing._createTransportForEndpoint(endpoint, this.databaseId);
|
|
258
|
+
this.currentUrl = endpoint;
|
|
259
|
+
return this.transport;
|
|
260
|
+
}
|
|
261
|
+
closeTransport() {
|
|
262
|
+
if (this.transport) {
|
|
263
|
+
this.transport.close();
|
|
264
|
+
this.transport = null;
|
|
265
|
+
}
|
|
266
|
+
this.currentUrl = "";
|
|
267
|
+
}
|
|
268
|
+
async withOperation(operation) {
|
|
269
|
+
const previous = this.operation;
|
|
270
|
+
let release = () => {
|
|
271
|
+
};
|
|
272
|
+
this.operation = new Promise((resolve) => {
|
|
273
|
+
release = resolve;
|
|
274
|
+
});
|
|
275
|
+
await previous.catch(() => void 0);
|
|
276
|
+
try {
|
|
277
|
+
return await operation();
|
|
278
|
+
} finally {
|
|
279
|
+
release();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
function toSubscriptionRoutingError(err, databaseId) {
|
|
284
|
+
return new RemoteError(
|
|
285
|
+
"ROUTING_ERROR",
|
|
286
|
+
`Could not re-establish active subscriptions on refreshed routing for database '${databaseId}': ${err instanceof Error ? err.message : String(err)}`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/client/topology-transport.ts
|
|
291
|
+
var TopologyAwareTransport = class {
|
|
292
|
+
constructor(databaseId, routing, onClose) {
|
|
293
|
+
this.databaseId = databaseId;
|
|
294
|
+
this.routing = routing;
|
|
295
|
+
this.onClose = onClose;
|
|
296
|
+
this.subscriptionSet = new TopologySubscriptionSet(databaseId, routing, () => this.assertOpen());
|
|
297
|
+
}
|
|
298
|
+
closed = false;
|
|
299
|
+
readTransport = null;
|
|
300
|
+
writeTransport = null;
|
|
301
|
+
readTransportRequest = null;
|
|
302
|
+
writeTransportRequest = null;
|
|
303
|
+
subscriptionSet;
|
|
304
|
+
currentReadUrl = "";
|
|
305
|
+
currentWriteUrl = "";
|
|
306
|
+
async query(sql, params) {
|
|
307
|
+
const readConcern = this.routing._getReadConcern();
|
|
308
|
+
const transport = await this.getReadTransport(readConcern);
|
|
309
|
+
const endpointUsed = this.currentReadUrl;
|
|
310
|
+
try {
|
|
311
|
+
return await transport.query(sql, params, readConcern ? { level: readConcern } : void 0);
|
|
312
|
+
} catch (err) {
|
|
313
|
+
if (this.routing._usesCoordinatorDiscovery() && shouldRefreshRouting(err)) {
|
|
314
|
+
await this.routing._refreshClusterRouting(this.databaseId);
|
|
315
|
+
this.readTransport = null;
|
|
316
|
+
this.currentReadUrl = "";
|
|
317
|
+
const refreshed = await this.getReadTransport(readConcern);
|
|
318
|
+
return refreshed.query(sql, params, readConcern ? { level: readConcern } : void 0);
|
|
319
|
+
}
|
|
320
|
+
const isTransportError = err instanceof Error && (err.name !== "RemoteError" || err.code === "CONNECTION_ERROR");
|
|
321
|
+
const writeEndpoint = await this.routing._getWriteEndpoint(this.databaseId);
|
|
322
|
+
if (isTransportError && endpointUsed && endpointUsed !== writeEndpoint) {
|
|
323
|
+
this.routing._removeReplica(endpointUsed);
|
|
324
|
+
if (this.currentReadUrl === endpointUsed) {
|
|
325
|
+
this.readTransport = null;
|
|
326
|
+
this.currentReadUrl = "";
|
|
327
|
+
}
|
|
328
|
+
const fallback = await this.getReadTransport();
|
|
329
|
+
return fallback.query(sql, params);
|
|
330
|
+
}
|
|
331
|
+
throw err;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async execute(sql, params) {
|
|
335
|
+
return this.onWriteTransport((transport) => transport.execute(sql, params));
|
|
336
|
+
}
|
|
337
|
+
async transaction(statements) {
|
|
338
|
+
return this.onWriteTransport((transport) => transport.transaction(statements));
|
|
339
|
+
}
|
|
340
|
+
async batch(sql, paramsBatch, writeConcern) {
|
|
341
|
+
return this.onWriteTransport((transport) => transport.batch(sql, paramsBatch, writeConcern));
|
|
342
|
+
}
|
|
343
|
+
async load(sql, paramsBatch, durability, checkpoint) {
|
|
344
|
+
return this.onWriteTransport((transport) => transport.load(sql, paramsBatch, durability, checkpoint));
|
|
345
|
+
}
|
|
346
|
+
async onWriteTransport(operation) {
|
|
347
|
+
const transport = await this.getWriteTransport();
|
|
348
|
+
try {
|
|
349
|
+
return await operation(transport);
|
|
350
|
+
} catch (err) {
|
|
351
|
+
if (this.routing._usesCoordinatorDiscovery() && shouldRefreshRouting(err)) {
|
|
352
|
+
await this.routing._refreshClusterRouting(this.databaseId);
|
|
353
|
+
this.writeTransport = null;
|
|
354
|
+
this.currentWriteUrl = "";
|
|
355
|
+
}
|
|
356
|
+
throw err;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async queryNamed(name, args) {
|
|
360
|
+
const readConcern = this.routing._getReadConcern();
|
|
361
|
+
const transport = await this.getReadTransport(readConcern);
|
|
362
|
+
return transport.queryNamed(name, args, readConcern ? { level: readConcern } : void 0);
|
|
363
|
+
}
|
|
364
|
+
async executeNamed(name, args, writeConcern) {
|
|
365
|
+
return this.onWriteTransport((transport) => transport.executeNamed(name, args, writeConcern));
|
|
366
|
+
}
|
|
367
|
+
async liveSubscribe(name, args, handlers, registryDigest) {
|
|
368
|
+
return this.subscriptionSet.subscribeLive(name, args, handlers, registryDigest);
|
|
369
|
+
}
|
|
370
|
+
async subscribe(table, filter, callback, options) {
|
|
371
|
+
return this.subscriptionSet.subscribe(table, filter, callback, options);
|
|
372
|
+
}
|
|
373
|
+
async _handleClusterRoutingChanged() {
|
|
374
|
+
await this.subscriptionSet.handleRoutingChanged();
|
|
375
|
+
}
|
|
376
|
+
close() {
|
|
377
|
+
this.closed = true;
|
|
378
|
+
const sameTransport = this.readTransport !== null && this.readTransport === this.writeTransport;
|
|
379
|
+
this.subscriptionSet.closeAll();
|
|
380
|
+
if (this.readTransport) {
|
|
381
|
+
this.readTransport.close();
|
|
382
|
+
this.readTransport = null;
|
|
383
|
+
}
|
|
384
|
+
if (this.writeTransport && !sameTransport) {
|
|
385
|
+
this.writeTransport.close();
|
|
386
|
+
}
|
|
387
|
+
this.writeTransport = null;
|
|
388
|
+
this.onClose(this);
|
|
389
|
+
}
|
|
390
|
+
async getReadTransport(readConcern) {
|
|
391
|
+
this.assertOpen();
|
|
392
|
+
while (this.readTransportRequest) {
|
|
393
|
+
await this.readTransportRequest.catch(() => void 0);
|
|
394
|
+
this.assertOpen();
|
|
395
|
+
}
|
|
396
|
+
const request = this.resolveReadTransport(readConcern);
|
|
397
|
+
this.readTransportRequest = request;
|
|
398
|
+
try {
|
|
399
|
+
return await request;
|
|
400
|
+
} finally {
|
|
401
|
+
if (this.readTransportRequest === request) {
|
|
402
|
+
this.readTransportRequest = null;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async resolveReadTransport(readConcern) {
|
|
407
|
+
const endpoint = await this.routing._getReadEndpoint(this.databaseId, readConcern);
|
|
408
|
+
this.assertOpen();
|
|
409
|
+
if (this.readTransport && this.currentReadUrl === endpoint) {
|
|
410
|
+
return this.readTransport;
|
|
411
|
+
}
|
|
412
|
+
const nextTransport = this.routing._createTransportForEndpoint(endpoint, this.databaseId);
|
|
413
|
+
const oldTransport = this.readTransport;
|
|
414
|
+
this.readTransport = nextTransport;
|
|
415
|
+
this.currentReadUrl = endpoint;
|
|
416
|
+
if (oldTransport) {
|
|
417
|
+
oldTransport.close();
|
|
418
|
+
}
|
|
419
|
+
return this.readTransport;
|
|
420
|
+
}
|
|
421
|
+
async getWriteTransport() {
|
|
422
|
+
this.assertOpen();
|
|
423
|
+
while (this.writeTransportRequest) {
|
|
424
|
+
await this.writeTransportRequest.catch(() => void 0);
|
|
425
|
+
this.assertOpen();
|
|
426
|
+
}
|
|
427
|
+
const request = this.resolveWriteTransport();
|
|
428
|
+
this.writeTransportRequest = request;
|
|
429
|
+
try {
|
|
430
|
+
return await request;
|
|
431
|
+
} finally {
|
|
432
|
+
if (this.writeTransportRequest === request) {
|
|
433
|
+
this.writeTransportRequest = null;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
async resolveWriteTransport() {
|
|
438
|
+
const endpoint = await this.routing._getWriteEndpoint(this.databaseId);
|
|
439
|
+
this.assertOpen();
|
|
440
|
+
if (this.writeTransport && this.currentWriteUrl === endpoint) {
|
|
441
|
+
return this.writeTransport;
|
|
442
|
+
}
|
|
443
|
+
const nextTransport = this.routing._createTransportForEndpoint(endpoint, this.databaseId);
|
|
444
|
+
const oldTransport = this.writeTransport;
|
|
445
|
+
this.writeTransport = nextTransport;
|
|
446
|
+
this.currentWriteUrl = endpoint;
|
|
447
|
+
if (oldTransport) {
|
|
448
|
+
oldTransport.close();
|
|
449
|
+
}
|
|
450
|
+
return this.writeTransport;
|
|
451
|
+
}
|
|
452
|
+
assertOpen() {
|
|
453
|
+
if (this.closed) {
|
|
454
|
+
throw new RemoteError("TRANSPORT_ERROR", "Transport is closed");
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
// src/client/topology.ts
|
|
460
|
+
var CLUSTER_DISCOVERY_FETCH_TIMEOUT_MS = 2e3;
|
|
461
|
+
var LATENCY_TTL_MS = 6e4;
|
|
462
|
+
var LATENCY_PROBE_TIMEOUT_MS = 5e3;
|
|
463
|
+
var TopologyAwareClient = class extends DatabaseClient {
|
|
464
|
+
baseUrl;
|
|
465
|
+
primaryUrl;
|
|
466
|
+
replicaUrls;
|
|
467
|
+
readPreference;
|
|
468
|
+
discovery;
|
|
469
|
+
readConcern;
|
|
470
|
+
starterEndpoints;
|
|
471
|
+
clusterRouting = /* @__PURE__ */ new Map();
|
|
472
|
+
topologyTransports = /* @__PURE__ */ new Map();
|
|
473
|
+
latencies = [];
|
|
474
|
+
latencyMeasuredAt = 0;
|
|
475
|
+
latencyMeasuring = null;
|
|
476
|
+
removedReplicas = /* @__PURE__ */ new Set();
|
|
477
|
+
constructor(options) {
|
|
478
|
+
super(options);
|
|
479
|
+
this.primaryUrl = options.primary ? toBaseUrl(options.primary) : void 0;
|
|
480
|
+
this.replicaUrls = (options.replicas ?? []).map(toBaseUrl);
|
|
481
|
+
this.readPreference = options.readPreference ?? "primary";
|
|
482
|
+
this.discovery = options.discovery ?? "static";
|
|
483
|
+
this.readConcern = options.readConcern;
|
|
484
|
+
this.starterEndpoints = (options.endpoints ?? []).map(toBaseUrl);
|
|
485
|
+
this.baseUrl = this.primaryUrl ?? this.replicaUrls[0] ?? this.starterEndpoints[0] ?? "";
|
|
486
|
+
}
|
|
487
|
+
createTransport(databaseId) {
|
|
488
|
+
const transport = new TopologyAwareTransport(
|
|
489
|
+
databaseId,
|
|
490
|
+
this,
|
|
491
|
+
(closing) => this.unregisterTopologyTransport(databaseId, closing)
|
|
492
|
+
);
|
|
493
|
+
this.topologyTransports.set(databaseId, transport);
|
|
494
|
+
return transport;
|
|
495
|
+
}
|
|
496
|
+
_createTransportForEndpoint(url, databaseId) {
|
|
497
|
+
return createEndpointTransport(this.settings, url, databaseId);
|
|
498
|
+
}
|
|
499
|
+
async resolveServerUrl(databaseId) {
|
|
500
|
+
return this._getWriteEndpoint(databaseId);
|
|
501
|
+
}
|
|
502
|
+
async _getReadEndpoint(databaseId, readConcern) {
|
|
503
|
+
if (this.discovery === "coordinator" && databaseId) {
|
|
504
|
+
return this.routedReadEndpoint(databaseId, readConcern);
|
|
505
|
+
}
|
|
506
|
+
if (this.readPreference === "primary") {
|
|
507
|
+
return this.primaryUrl ?? this.baseUrl;
|
|
508
|
+
}
|
|
509
|
+
const availableReplicas = this.replicaUrls.filter((url) => !this.removedReplicas.has(url));
|
|
510
|
+
if (this.readPreference === "replica") {
|
|
511
|
+
if (availableReplicas.length === 0) {
|
|
512
|
+
return this.primaryUrl ?? this.baseUrl;
|
|
513
|
+
}
|
|
514
|
+
return availableReplicas[Math.floor(Math.random() * availableReplicas.length)];
|
|
515
|
+
}
|
|
516
|
+
await this.ensureLatencyMeasured();
|
|
517
|
+
const reachable = this.latencies.filter((entry) => entry.reachable && !this.removedReplicas.has(entry.url));
|
|
518
|
+
if (reachable.length === 0) {
|
|
519
|
+
return this.primaryUrl ?? this.baseUrl;
|
|
520
|
+
}
|
|
521
|
+
reachable.sort((left, right) => left.latencyMs - right.latencyMs);
|
|
522
|
+
return reachable[0].url;
|
|
523
|
+
}
|
|
524
|
+
async routedReadEndpoint(databaseId, readConcern) {
|
|
525
|
+
const routing = await this.ensureClusterRouting(databaseId);
|
|
526
|
+
const concern = readConcern ?? this.readConcern ?? "majority";
|
|
527
|
+
if (concern === "linearizable") {
|
|
528
|
+
if (routing.currentPrimary) return routing.currentPrimary;
|
|
529
|
+
throw new RemoteError("NO_SAFE_PRIMARY", "No current primary is available for linearizable reads");
|
|
530
|
+
}
|
|
531
|
+
const readable = routing.readEndpoints.filter((endpoint) => endpoint.readConcerns.includes(concern));
|
|
532
|
+
const preferredReadable = this.readPreference === "replica" && routing.currentPrimary ? readable.filter((endpoint) => endpoint.url !== routing.currentPrimary) : readable;
|
|
533
|
+
if (this.readPreference !== "primary" && preferredReadable.length > 0) {
|
|
534
|
+
if (this.readPreference === "nearest") {
|
|
535
|
+
return preferredReadable[0].url;
|
|
536
|
+
}
|
|
537
|
+
return preferredReadable[Math.floor(Math.random() * preferredReadable.length)].url;
|
|
538
|
+
}
|
|
539
|
+
if (routing.currentPrimary) return routing.currentPrimary;
|
|
540
|
+
const localReadable = routing.readEndpoints.find((endpoint) => endpoint.readConcerns.includes("local"));
|
|
541
|
+
if (localReadable) return localReadable.url;
|
|
542
|
+
throw new RemoteError("ROUTING_ERROR", "No usable read endpoint is available");
|
|
543
|
+
}
|
|
544
|
+
async _getWriteEndpoint(databaseId) {
|
|
545
|
+
if (this.discovery === "coordinator" && databaseId) {
|
|
546
|
+
const routing = await this.ensureClusterRouting(databaseId);
|
|
547
|
+
if (!routing.currentPrimary) {
|
|
548
|
+
throw new RemoteError("NO_SAFE_PRIMARY", "No current primary is available");
|
|
549
|
+
}
|
|
550
|
+
return routing.currentPrimary;
|
|
551
|
+
}
|
|
552
|
+
return this.primaryUrl ?? this.baseUrl;
|
|
553
|
+
}
|
|
554
|
+
_getReadConcern() {
|
|
555
|
+
return this.readConcern;
|
|
556
|
+
}
|
|
557
|
+
_usesCoordinatorDiscovery() {
|
|
558
|
+
return this.discovery === "coordinator";
|
|
559
|
+
}
|
|
560
|
+
_removeReplica(url) {
|
|
561
|
+
this.removedReplicas.add(url);
|
|
562
|
+
}
|
|
563
|
+
async _refreshClusterRouting(databaseId) {
|
|
564
|
+
const encodedId = encodeURIComponent(databaseId);
|
|
565
|
+
for (const endpoint of this.clusterDiscoveryCandidates(databaseId)) {
|
|
566
|
+
const next = await this.fetchClusterRouting(toServerBaseUrl(endpoint, databaseId), encodedId, databaseId);
|
|
567
|
+
if (!next) continue;
|
|
568
|
+
const previous = this.clusterRouting.get(databaseId);
|
|
569
|
+
this.clusterRouting.set(databaseId, next);
|
|
570
|
+
if (clusterRoutingChanged(previous, next)) {
|
|
571
|
+
try {
|
|
572
|
+
await this.notifyClusterRoutingChanged(databaseId);
|
|
573
|
+
} catch (err) {
|
|
574
|
+
if (previous) {
|
|
575
|
+
this.clusterRouting.set(databaseId, previous);
|
|
576
|
+
} else {
|
|
577
|
+
this.clusterRouting.delete(databaseId);
|
|
578
|
+
}
|
|
579
|
+
throw err;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
throw new RemoteError("ROUTING_ERROR", `Could not discover cluster routing for database '${databaseId}'`);
|
|
585
|
+
}
|
|
586
|
+
async fetchClusterRouting(base, encodedId, databaseId) {
|
|
587
|
+
const controller = new AbortController();
|
|
588
|
+
const timeout = setTimeout(() => controller.abort(), CLUSTER_DISCOVERY_FETCH_TIMEOUT_MS);
|
|
589
|
+
unrefTimer(timeout);
|
|
590
|
+
try {
|
|
591
|
+
const response = await fetch(`${base}/db/${encodedId}/cluster`, {
|
|
592
|
+
headers: this.settings.headers,
|
|
593
|
+
signal: controller.signal
|
|
594
|
+
});
|
|
595
|
+
if (!response.ok) return null;
|
|
596
|
+
return parseClusterRouting(await response.json(), databaseId);
|
|
597
|
+
} catch (err) {
|
|
598
|
+
if (err instanceof RemoteError && err.code === "INVALID_RESPONSE") {
|
|
599
|
+
throw err;
|
|
600
|
+
}
|
|
601
|
+
return null;
|
|
602
|
+
} finally {
|
|
603
|
+
clearTimeout(timeout);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
async ensureClusterRouting(databaseId) {
|
|
607
|
+
const existing = this.clusterRouting.get(databaseId);
|
|
608
|
+
if (existing) return existing;
|
|
609
|
+
await this._refreshClusterRouting(databaseId);
|
|
610
|
+
const refreshed = this.clusterRouting.get(databaseId);
|
|
611
|
+
if (!refreshed) {
|
|
612
|
+
throw new RemoteError("ROUTING_ERROR", `Could not discover cluster routing for database '${databaseId}'`);
|
|
613
|
+
}
|
|
614
|
+
return refreshed;
|
|
615
|
+
}
|
|
616
|
+
clusterDiscoveryCandidates(databaseId) {
|
|
617
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
618
|
+
for (const endpoint of this.starterEndpoints) candidates.add(endpoint);
|
|
619
|
+
if (this.primaryUrl) candidates.add(this.primaryUrl);
|
|
620
|
+
for (const endpoint of this.replicaUrls) candidates.add(endpoint);
|
|
621
|
+
const existing = this.clusterRouting.get(databaseId);
|
|
622
|
+
if (existing?.currentPrimary) candidates.add(existing.currentPrimary);
|
|
623
|
+
for (const endpoint of existing?.readEndpoints ?? []) candidates.add(endpoint.url);
|
|
624
|
+
if (this.baseUrl) candidates.add(this.baseUrl);
|
|
625
|
+
return [...candidates];
|
|
626
|
+
}
|
|
627
|
+
async ensureLatencyMeasured() {
|
|
628
|
+
if (this.latencies.length > 0 && Date.now() - this.latencyMeasuredAt < LATENCY_TTL_MS) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (this.latencyMeasuring) {
|
|
632
|
+
await this.latencyMeasuring;
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
this.latencyMeasuring = this.measureLatencies();
|
|
636
|
+
try {
|
|
637
|
+
await this.latencyMeasuring;
|
|
638
|
+
} finally {
|
|
639
|
+
this.latencyMeasuring = null;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
async measureLatencies() {
|
|
643
|
+
const allEndpoints = [];
|
|
644
|
+
if (this.primaryUrl) {
|
|
645
|
+
allEndpoints.push(this.primaryUrl);
|
|
646
|
+
}
|
|
647
|
+
for (const replica of this.replicaUrls) {
|
|
648
|
+
allEndpoints.push(replica);
|
|
649
|
+
}
|
|
650
|
+
this.latencies = await Promise.all(allEndpoints.map((url) => measureEndpointLatency(url)));
|
|
651
|
+
this.latencyMeasuredAt = Date.now();
|
|
652
|
+
}
|
|
653
|
+
unregisterTopologyTransport(databaseId, transport) {
|
|
654
|
+
if (this.topologyTransports.get(databaseId) === transport) {
|
|
655
|
+
this.topologyTransports.delete(databaseId);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async notifyClusterRoutingChanged(databaseId) {
|
|
659
|
+
const transport = this.topologyTransports.get(databaseId);
|
|
660
|
+
if (!transport) return;
|
|
661
|
+
await transport._handleClusterRoutingChanged();
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
async function measureEndpointLatency(url) {
|
|
665
|
+
const start = performance.now();
|
|
666
|
+
const controller = new AbortController();
|
|
667
|
+
const timeout = setTimeout(() => controller.abort(), LATENCY_PROBE_TIMEOUT_MS);
|
|
668
|
+
unrefTimer(timeout);
|
|
669
|
+
try {
|
|
670
|
+
const response = await fetch(`${url}/health`, { signal: controller.signal });
|
|
671
|
+
if (!response.ok) {
|
|
672
|
+
return { url, latencyMs: Number.MAX_SAFE_INTEGER, reachable: false };
|
|
673
|
+
}
|
|
674
|
+
return { url, latencyMs: performance.now() - start, reachable: true };
|
|
675
|
+
} catch {
|
|
676
|
+
return { url, latencyMs: Number.MAX_SAFE_INTEGER, reachable: false };
|
|
677
|
+
} finally {
|
|
678
|
+
clearTimeout(timeout);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
export { TopologyAwareClient };
|