@forgezero/providers 0.1.4 → 0.1.6
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/binance.js +1 -1
- package/dist/chain.js +1 -1
- package/dist/database.d.ts +84 -12
- package/dist/database.js +273 -35
- package/dist/email.js +1 -1
- package/dist/http.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/pool.js +1 -1
- package/dist/storage.js +1 -1
- package/dist/translation.js +1 -1
- package/package.json +1 -1
package/dist/binance.js
CHANGED
package/dist/chain.js
CHANGED
package/dist/database.d.ts
CHANGED
|
@@ -13,36 +13,108 @@
|
|
|
13
13
|
* HEALTH three failed connections mark it degraded, and the same admin
|
|
14
14
|
* screen that shows email relays shows the database
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* answering.
|
|
16
|
+
* Coordinator routing below remains inside one authenticated cluster. It never
|
|
17
|
+
* treats a different database or replica as an automatic write destination.
|
|
19
18
|
*/
|
|
20
19
|
/** Just enough of an ArangoDB client that this package imports nothing. */
|
|
21
20
|
export interface ArangoLike {
|
|
22
|
-
query<T = unknown>(query: unknown, bindVars?: Record<string, unknown
|
|
21
|
+
query<T = unknown>(query: unknown, bindVars?: Record<string, unknown>, options?: {
|
|
22
|
+
allowDirtyRead?: boolean;
|
|
23
|
+
}): Promise<{
|
|
23
24
|
all(): Promise<T[]>;
|
|
24
25
|
}>;
|
|
25
26
|
version?(): Promise<{
|
|
26
27
|
version: string;
|
|
27
28
|
}>;
|
|
29
|
+
close?(): void | Promise<void>;
|
|
28
30
|
}
|
|
29
|
-
export interface
|
|
31
|
+
export interface ArangoConnectOptions {
|
|
32
|
+
/** One coordinator from the verified list; the adapter owns selection. */
|
|
30
33
|
url: string;
|
|
31
34
|
database: string;
|
|
32
35
|
username: string;
|
|
36
|
+
password: string;
|
|
37
|
+
loadBalancingStrategy: 'NONE';
|
|
38
|
+
/** Always zero: only this adapter may decide whether replay is safe. */
|
|
39
|
+
maxRetries: number;
|
|
40
|
+
}
|
|
41
|
+
export interface ArangoConfig {
|
|
42
|
+
/** Backwards-compatible single-coordinator coordinate. */
|
|
43
|
+
url?: string;
|
|
44
|
+
/** Explicit authenticated private coordinators. Bounded to prevent config abuse. */
|
|
45
|
+
urls?: readonly string[];
|
|
46
|
+
/**
|
|
47
|
+
* Dynamic membership is accepted only as a signed/attested envelope verified
|
|
48
|
+
* by the host. A Worker/KV node list by itself is discovery data, not routing
|
|
49
|
+
* authority.
|
|
50
|
+
*/
|
|
51
|
+
membership?: ArangoMembershipSnapshot;
|
|
52
|
+
verifyMembership?: ArangoMembershipVerifier;
|
|
53
|
+
/** Required with dynamic membership, preventing a valid list crossing clusters. */
|
|
54
|
+
clusterId?: string;
|
|
55
|
+
database: string;
|
|
56
|
+
username: string;
|
|
33
57
|
/** Built by the host, because a connection pool must outlive one call. */
|
|
34
|
-
connect?: (options:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
failover?: boolean;
|
|
58
|
+
connect?: (options: ArangoConnectOptions) => ArangoLike;
|
|
59
|
+
/** Maximum coordinator attempts for an explicitly safe request. */
|
|
60
|
+
maxCoordinatorAttempts?: number;
|
|
61
|
+
/** Local transport-failure quarantine. Primarily injectable for tests. */
|
|
62
|
+
unhealthyForMs?: number;
|
|
63
|
+
now?: () => number;
|
|
41
64
|
}
|
|
42
65
|
export interface Query {
|
|
43
66
|
query: unknown;
|
|
44
67
|
bindVars?: Record<string, unknown>;
|
|
68
|
+
/**
|
|
69
|
+
* No retry is the secure default. `read` and `idempotent` are explicit
|
|
70
|
+
* caller assertions; this adapter never guesses AQL semantics from text.
|
|
71
|
+
*/
|
|
72
|
+
retrySafety?: 'read' | 'idempotent' | 'never';
|
|
73
|
+
/** Required when `retrySafety` is `idempotent`; retained for audit correlation. */
|
|
74
|
+
idempotencyKey?: string;
|
|
45
75
|
}
|
|
76
|
+
export interface ArangoCoordinatorMember {
|
|
77
|
+
url: string;
|
|
78
|
+
/** Clients address Coordinators; Community 3.11.14 has no read-only joiner role. */
|
|
79
|
+
role: 'coordinator';
|
|
80
|
+
/** Actual `/_admin/server/mode` state for this Coordinator. */
|
|
81
|
+
serverMode: 'default';
|
|
82
|
+
capabilities: {
|
|
83
|
+
/** Always false for the exact Community 3.11.14 contract. */
|
|
84
|
+
dirtyFollowerReads: false;
|
|
85
|
+
};
|
|
86
|
+
status: 'healthy' | 'degraded' | 'unhealthy';
|
|
87
|
+
/** Milliseconds since epoch, covered by the membership signature/attestation. */
|
|
88
|
+
observedAt: number;
|
|
89
|
+
/** Enrolled hybrid identity. The node key is its Ed25519 public half. */
|
|
90
|
+
nodeKey: string;
|
|
91
|
+
publicKeys: {
|
|
92
|
+
ed25519: string;
|
|
93
|
+
mlDsa: string;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export interface ArangoMembershipSnapshot {
|
|
97
|
+
authority: 'forgezero-platform-signed' | 'node-attested';
|
|
98
|
+
edition: 'community';
|
|
99
|
+
version: '3.11.14';
|
|
100
|
+
clusterId: string;
|
|
101
|
+
revision: string;
|
|
102
|
+
issuedAt: number;
|
|
103
|
+
expiresAt: number;
|
|
104
|
+
coordinators: readonly ArangoCoordinatorMember[];
|
|
105
|
+
}
|
|
106
|
+
export type ArangoMembershipVerifier = (snapshot: ArangoMembershipSnapshot, signal?: AbortSignal) => Promise<boolean>;
|
|
107
|
+
export declare const MAX_ARANGO_COORDINATORS = 16;
|
|
108
|
+
export declare const ARANGO_COMMUNITY_VERSION: "3.11.14";
|
|
109
|
+
/** Protected API used to attest cluster-wide `default`/maintenance mode. */
|
|
110
|
+
export declare const ARANGO_SERVER_MODE_PATH: "/_admin/server/mode";
|
|
111
|
+
/**
|
|
112
|
+
* Accept only an explicit, bounded set of private authenticated endpoints.
|
|
113
|
+
* Service discovery may propose a new list, but it must pass this authority
|
|
114
|
+
* boundary before a fresh pool is created; public Worker/KV state is never a
|
|
115
|
+
* database routing authority.
|
|
116
|
+
*/
|
|
117
|
+
export declare function arangoCoordinatorUrls(config: Pick<ArangoConfig, 'url' | 'urls'>): readonly string[];
|
|
46
118
|
export declare const arangodb: import("./index").ProviderSpec<Query, unknown[]>;
|
|
47
119
|
/** Drop memoised connections. Tests, and after a credential rotation. */
|
|
48
120
|
export declare function resetPools(): void;
|
package/dist/database.js
CHANGED
|
@@ -144,15 +144,148 @@ function createRegistry(options) {
|
|
|
144
144
|
}
|
|
145
145
|
return { call };
|
|
146
146
|
}
|
|
147
|
-
var VERSION = "0.1.
|
|
147
|
+
var VERSION = "0.1.6";
|
|
148
148
|
|
|
149
149
|
// src/database.ts
|
|
150
150
|
var pools = new Map;
|
|
151
|
+
var MAX_ARANGO_COORDINATORS = 16;
|
|
152
|
+
var ARANGO_COMMUNITY_VERSION = "3.11.14";
|
|
153
|
+
var ARANGO_SERVER_MODE_PATH = "/_admin/server/mode";
|
|
154
|
+
function arangoCoordinatorUrls(config) {
|
|
155
|
+
const input = config.urls ?? (config.url ? [config.url] : []);
|
|
156
|
+
if (input.length < 1 || input.length > MAX_ARANGO_COORDINATORS) {
|
|
157
|
+
throw new ProviderError("ARANGO_COORDINATORS_INVALID", `ArangoDB needs from 1 to ${MAX_ARANGO_COORDINATORS} private coordinator URLs.`);
|
|
158
|
+
}
|
|
159
|
+
const normalized = input.map((raw) => {
|
|
160
|
+
let value;
|
|
161
|
+
try {
|
|
162
|
+
value = new URL(raw);
|
|
163
|
+
} catch {
|
|
164
|
+
throw new ProviderError("ARANGO_COORDINATORS_INVALID", "ArangoDB coordinator URL is malformed.");
|
|
165
|
+
}
|
|
166
|
+
if (value.protocol !== "http:" && value.protocol !== "https:") {
|
|
167
|
+
throw new ProviderError("ARANGO_COORDINATORS_INVALID", "ArangoDB coordinators must use HTTP or HTTPS.");
|
|
168
|
+
}
|
|
169
|
+
if (value.username || value.password || value.search || value.hash || value.pathname && value.pathname !== "/") {
|
|
170
|
+
throw new ProviderError("ARANGO_COORDINATORS_INVALID", "ArangoDB coordinator URLs contain origins only; credentials and paths are separate.");
|
|
171
|
+
}
|
|
172
|
+
const hostname = value.hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
173
|
+
const ipv4 = hostname.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)?.slice(1).map(Number);
|
|
174
|
+
const privateV4 = ipv4 && ipv4.every((part) => part >= 0 && part <= 255) && (ipv4[0] === 10 || ipv4[0] === 127 || ipv4[0] === 172 && ipv4[1] >= 16 && ipv4[1] <= 31 || ipv4[0] === 192 && ipv4[1] === 168);
|
|
175
|
+
const privateV6 = hostname === "::1" || /^f[cd][0-9a-f]:/i.test(hostname);
|
|
176
|
+
if (!privateV4 && !privateV6 && hostname !== "localhost") {
|
|
177
|
+
throw new ProviderError("ARANGO_COORDINATORS_INVALID", "ArangoDB coordinators must be literal private or loopback endpoints.");
|
|
178
|
+
}
|
|
179
|
+
value.hostname = value.hostname.toLowerCase();
|
|
180
|
+
return value.toString().replace(/\/$/, "");
|
|
181
|
+
});
|
|
182
|
+
if (new Set(normalized).size !== normalized.length) {
|
|
183
|
+
throw new ProviderError("ARANGO_COORDINATORS_INVALID", "ArangoDB coordinator URLs must be unique.");
|
|
184
|
+
}
|
|
185
|
+
return normalized;
|
|
186
|
+
}
|
|
187
|
+
async function resolveMembership(config, signal) {
|
|
188
|
+
if (!config.membership) {
|
|
189
|
+
const urls = arangoCoordinatorUrls(config);
|
|
190
|
+
return {
|
|
191
|
+
members: urls.map((url) => ({
|
|
192
|
+
url,
|
|
193
|
+
role: "coordinator",
|
|
194
|
+
serverMode: "default",
|
|
195
|
+
capabilities: { dirtyFollowerReads: false },
|
|
196
|
+
status: "healthy"
|
|
197
|
+
})),
|
|
198
|
+
identity: `static:${urls.join(",")}`
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (config.url || config.urls) {
|
|
202
|
+
throw new ProviderError("ARANGO_MEMBERSHIP_AMBIGUOUS", "Configure static coordinator URLs or verified membership, not both.");
|
|
203
|
+
}
|
|
204
|
+
if (!config.clusterId || config.membership.clusterId !== config.clusterId) {
|
|
205
|
+
throw new ProviderError("ARANGO_MEMBERSHIP_CLUSTER", "Verified ArangoDB membership does not match the configured cluster.");
|
|
206
|
+
}
|
|
207
|
+
if (!config.verifyMembership || !await config.verifyMembership(config.membership, signal)) {
|
|
208
|
+
throw new ProviderError("ARANGO_MEMBERSHIP_UNVERIFIED", "Dynamic ArangoDB membership must pass the host signature or attestation verifier.");
|
|
209
|
+
}
|
|
210
|
+
const now = (config.now ?? Date.now)();
|
|
211
|
+
const snapshot = config.membership;
|
|
212
|
+
if (snapshot.edition !== "community" || snapshot.version !== ARANGO_COMMUNITY_VERSION) {
|
|
213
|
+
throw new ProviderError("ARANGO_MEMBERSHIP_CONTRACT", `Dynamic ArangoDB membership must describe Community ${ARANGO_COMMUNITY_VERSION}.`);
|
|
214
|
+
}
|
|
215
|
+
if (!Number.isFinite(snapshot.issuedAt) || !Number.isFinite(snapshot.expiresAt) || snapshot.issuedAt > now || snapshot.expiresAt <= now) {
|
|
216
|
+
throw new ProviderError("ARANGO_MEMBERSHIP_EXPIRED", "Dynamic ArangoDB membership is not currently valid.");
|
|
217
|
+
}
|
|
218
|
+
const validAuthority = snapshot.authority === "forgezero-platform-signed" || snapshot.authority === "node-attested";
|
|
219
|
+
const validNodes = snapshot.coordinators.every((node) => Number.isFinite(node.observedAt) && node.nodeKey.length > 0 && node.nodeKey === node.publicKeys?.ed25519 && Boolean(node.publicKeys.mlDsa) && (node.status === "healthy" || node.status === "degraded" || node.status === "unhealthy") && node.role === "coordinator" && node.serverMode === "default" && node.capabilities?.dirtyFollowerReads === false);
|
|
220
|
+
if (!validAuthority || !snapshot.revision || !validNodes) {
|
|
221
|
+
throw new ProviderError("ARANGO_MEMBERSHIP_INVALID", "Dynamic ArangoDB membership needs a revision and observed timestamps.");
|
|
222
|
+
}
|
|
223
|
+
const allUrls = arangoCoordinatorUrls({ urls: snapshot.coordinators.map((node) => node.url) });
|
|
224
|
+
const members = allUrls.map((url, index) => ({ ...snapshot.coordinators[index], url })).filter((member) => member.status !== "unhealthy");
|
|
225
|
+
if (members.length === 0) {
|
|
226
|
+
throw new ProviderError("ARANGO_COORDINATORS_UNAVAILABLE", "Verified membership has no healthy or degraded ArangoDB coordinator.");
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
members,
|
|
230
|
+
identity: `membership:${snapshot.clusterId}`,
|
|
231
|
+
issuedAt: snapshot.issuedAt,
|
|
232
|
+
revision: snapshot.revision
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function retryableCoordinatorFailure(error) {
|
|
236
|
+
const failure = error;
|
|
237
|
+
const status = typeof failure.code === "number" ? failure.code : failure.status;
|
|
238
|
+
if (status === 408 || status === 429 || status === 502 || status === 503 || status === 504)
|
|
239
|
+
return true;
|
|
240
|
+
return typeof failure.code === "string" && [
|
|
241
|
+
"ECONNREFUSED",
|
|
242
|
+
"ECONNRESET",
|
|
243
|
+
"EHOSTUNREACH",
|
|
244
|
+
"ENETUNREACH",
|
|
245
|
+
"ETIMEDOUT"
|
|
246
|
+
].includes(failure.code);
|
|
247
|
+
}
|
|
248
|
+
function boundedAttempts(config, available, safe) {
|
|
249
|
+
if (!safe)
|
|
250
|
+
return 1;
|
|
251
|
+
const requested = config.maxCoordinatorAttempts ?? available;
|
|
252
|
+
if (!Number.isInteger(requested) || requested < 1 || requested > MAX_ARANGO_COORDINATORS) {
|
|
253
|
+
throw new ProviderError("ARANGO_ATTEMPTS_INVALID", `ArangoDB coordinator attempts must be from 1 to ${MAX_ARANGO_COORDINATORS}.`);
|
|
254
|
+
}
|
|
255
|
+
return Math.min(requested, available);
|
|
256
|
+
}
|
|
257
|
+
function requestIsSafe(request) {
|
|
258
|
+
if (request.retrySafety === "read")
|
|
259
|
+
return true;
|
|
260
|
+
if (request.retrySafety !== "idempotent")
|
|
261
|
+
return false;
|
|
262
|
+
if (!request.idempotencyKey || request.idempotencyKey.length > 128) {
|
|
263
|
+
throw new ProviderError("ARANGO_IDEMPOTENCY_KEY_INVALID", "An idempotent ArangoDB retry needs a non-empty idempotency key of at most 128 characters.");
|
|
264
|
+
}
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
function selectNode(pool, excluded, now, eligibleMember) {
|
|
268
|
+
const eligible = pool.nodes.filter((node) => !excluded.has(node.url) && node.unhealthyUntil <= now && eligibleMember(node));
|
|
269
|
+
if (eligible.length === 0)
|
|
270
|
+
return;
|
|
271
|
+
const least = Math.min(...eligible.map((node) => node.inFlight));
|
|
272
|
+
for (let offset = 0;offset < pool.nodes.length; offset += 1) {
|
|
273
|
+
const index = (pool.cursor + offset) % pool.nodes.length;
|
|
274
|
+
const node = pool.nodes[index];
|
|
275
|
+
if (!excluded.has(node.url) && node.unhealthyUntil <= now && eligibleMember(node) && node.inFlight === least) {
|
|
276
|
+
pool.cursor = (index + 1) % pool.nodes.length;
|
|
277
|
+
return node;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
function nodeForRequest(pool, excluded, now) {
|
|
283
|
+
return selectNode(pool, excluded, now, () => true);
|
|
284
|
+
}
|
|
151
285
|
var arangodb = defineProvider({
|
|
152
286
|
id: "arangodb",
|
|
153
287
|
service: "database",
|
|
154
288
|
label: "ArangoDB",
|
|
155
|
-
multiInstance: true,
|
|
156
289
|
credentials: {
|
|
157
290
|
type: "object",
|
|
158
291
|
additionalProperties: false,
|
|
@@ -164,20 +297,45 @@ var arangodb = defineProvider({
|
|
|
164
297
|
config: {
|
|
165
298
|
type: "object",
|
|
166
299
|
additionalProperties: false,
|
|
167
|
-
required: ["
|
|
300
|
+
required: ["database", "username"],
|
|
301
|
+
anyOf: [
|
|
302
|
+
{ required: ["url"] },
|
|
303
|
+
{ required: ["urls"] },
|
|
304
|
+
{ required: ["clusterId"] }
|
|
305
|
+
],
|
|
168
306
|
properties: {
|
|
169
307
|
url: {
|
|
170
308
|
type: "string",
|
|
171
309
|
title: "URL",
|
|
172
310
|
description: "Bind to loopback. A database reachable from the internet is a database that will be."
|
|
173
311
|
},
|
|
312
|
+
urls: {
|
|
313
|
+
type: "array",
|
|
314
|
+
minItems: 1,
|
|
315
|
+
maxItems: MAX_ARANGO_COORDINATORS,
|
|
316
|
+
uniqueItems: true,
|
|
317
|
+
items: { type: "string" },
|
|
318
|
+
title: "Coordinator URLs",
|
|
319
|
+
description: "Authenticated private coordinators in this one logical cluster."
|
|
320
|
+
},
|
|
321
|
+
clusterId: {
|
|
322
|
+
type: "string",
|
|
323
|
+
title: "Cluster ID",
|
|
324
|
+
description: "Binds host-verified dynamic membership to one logical cluster."
|
|
325
|
+
},
|
|
174
326
|
database: { type: "string", title: "Database" },
|
|
175
327
|
username: { type: "string", title: "Username" },
|
|
176
|
-
|
|
177
|
-
type: "
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
328
|
+
maxCoordinatorAttempts: {
|
|
329
|
+
type: "integer",
|
|
330
|
+
minimum: 1,
|
|
331
|
+
maximum: MAX_ARANGO_COORDINATORS,
|
|
332
|
+
title: "Safe request attempts"
|
|
333
|
+
},
|
|
334
|
+
unhealthyForMs: {
|
|
335
|
+
type: "integer",
|
|
336
|
+
minimum: 1000,
|
|
337
|
+
maximum: 300000,
|
|
338
|
+
title: "Local coordinator quarantine (ms)"
|
|
181
339
|
}
|
|
182
340
|
}
|
|
183
341
|
},
|
|
@@ -186,35 +344,111 @@ var arangodb = defineProvider({
|
|
|
186
344
|
if (!config.connect) {
|
|
187
345
|
throw new ProviderError("ARANGO_NO_CONNECT", "Provide a connect function in this provider's config — arangojs on a server; a database is not reachable from an edge runtime.");
|
|
188
346
|
}
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
347
|
+
const membership = await resolveMembership(config, context.signal);
|
|
348
|
+
const members = membership.members;
|
|
349
|
+
const urls = members.map(({ url }) => url);
|
|
350
|
+
const safe = requestIsSafe(request);
|
|
351
|
+
const available = members.length;
|
|
352
|
+
if (available === 0) {
|
|
353
|
+
throw new ProviderError("ARANGO_COORDINATORS_UNAVAILABLE", "No verified authoritative ArangoDB coordinator is available.");
|
|
354
|
+
}
|
|
355
|
+
const attempts = boundedAttempts(config, available, safe);
|
|
356
|
+
const key = `${membership.identity}/${config.database}/${config.username}`;
|
|
357
|
+
let pool = pools.get(key);
|
|
358
|
+
if (!pool) {
|
|
359
|
+
const password = await context.secret("password");
|
|
360
|
+
pool = {
|
|
361
|
+
nodes: members.map((member) => ({
|
|
362
|
+
url: member.url,
|
|
363
|
+
member,
|
|
364
|
+
client: config.connect({
|
|
365
|
+
url: member.url,
|
|
366
|
+
database: config.database,
|
|
367
|
+
username: config.username,
|
|
368
|
+
password,
|
|
369
|
+
loadBalancingStrategy: "NONE",
|
|
370
|
+
maxRetries: 0
|
|
371
|
+
}),
|
|
372
|
+
inFlight: 0,
|
|
373
|
+
unhealthyUntil: 0
|
|
374
|
+
})),
|
|
375
|
+
cursor: 0,
|
|
376
|
+
membershipIssuedAt: membership.issuedAt,
|
|
377
|
+
membershipRevision: membership.revision
|
|
378
|
+
};
|
|
379
|
+
pools.set(key, pool);
|
|
380
|
+
} else if (membership.issuedAt !== undefined) {
|
|
381
|
+
if (pool.membershipIssuedAt !== undefined && membership.issuedAt < pool.membershipIssuedAt) {
|
|
382
|
+
throw new ProviderError("ARANGO_MEMBERSHIP_ROLLBACK", "ArangoDB membership is older than the last accepted signed revision.");
|
|
383
|
+
}
|
|
384
|
+
const existing = new Map(pool.nodes.map((node) => [node.url, node]));
|
|
385
|
+
const missing = urls.filter((url) => !existing.has(url));
|
|
386
|
+
const password = missing.length > 0 ? await context.secret("password") : undefined;
|
|
387
|
+
const nextNodes = members.map((member) => {
|
|
388
|
+
const retained2 = existing.get(member.url);
|
|
389
|
+
if (retained2) {
|
|
390
|
+
retained2.member = member;
|
|
391
|
+
return retained2;
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
url: member.url,
|
|
395
|
+
member,
|
|
396
|
+
client: config.connect({
|
|
397
|
+
url: member.url,
|
|
398
|
+
database: config.database,
|
|
399
|
+
username: config.username,
|
|
400
|
+
password,
|
|
401
|
+
loadBalancingStrategy: "NONE",
|
|
402
|
+
maxRetries: 0
|
|
403
|
+
}),
|
|
404
|
+
inFlight: 0,
|
|
405
|
+
unhealthyUntil: 0
|
|
406
|
+
};
|
|
197
407
|
});
|
|
198
|
-
|
|
408
|
+
const retained = new Set(urls);
|
|
409
|
+
for (const removed of pool.nodes.filter((node) => !retained.has(node.url) && node.inFlight === 0)) {
|
|
410
|
+
try {
|
|
411
|
+
await removed.client.close?.();
|
|
412
|
+
} catch {}
|
|
413
|
+
}
|
|
414
|
+
pool.nodes = nextNodes;
|
|
415
|
+
pool.cursor %= nextNodes.length;
|
|
416
|
+
pool.membershipIssuedAt = membership.issuedAt;
|
|
417
|
+
pool.membershipRevision = membership.revision;
|
|
418
|
+
}
|
|
419
|
+
const tried = new Set;
|
|
420
|
+
let lastError;
|
|
421
|
+
for (let attempt = 0;attempt < attempts; attempt += 1) {
|
|
422
|
+
if (context.signal?.aborted) {
|
|
423
|
+
throw context.signal.reason ?? new ProviderError("ARANGO_ABORTED", "ArangoDB request was cancelled.");
|
|
424
|
+
}
|
|
425
|
+
const now = (config.now ?? Date.now)();
|
|
426
|
+
const node = nodeForRequest(pool, tried, now);
|
|
427
|
+
if (!node)
|
|
428
|
+
break;
|
|
429
|
+
tried.add(node.url);
|
|
430
|
+
node.inFlight += 1;
|
|
431
|
+
try {
|
|
432
|
+
const cursor = await node.client.query(request.query, request.bindVars);
|
|
433
|
+
const result = await cursor.all();
|
|
434
|
+
node.unhealthyUntil = 0;
|
|
435
|
+
return result;
|
|
436
|
+
} catch (error) {
|
|
437
|
+
lastError = error;
|
|
438
|
+
if (!retryableCoordinatorFailure(error))
|
|
439
|
+
throw error;
|
|
440
|
+
const quarantine = Math.min(Math.max(config.unhealthyForMs ?? 15000, 1000), 300000);
|
|
441
|
+
node.unhealthyUntil = now + quarantine;
|
|
442
|
+
if (!safe)
|
|
443
|
+
throw error;
|
|
444
|
+
} finally {
|
|
445
|
+
node.inFlight -= 1;
|
|
446
|
+
}
|
|
199
447
|
}
|
|
200
|
-
|
|
201
|
-
return cursor.all();
|
|
448
|
+
throw lastError ?? new ProviderError("ARANGO_COORDINATORS_UNAVAILABLE", "No healthy ArangoDB coordinator is available.");
|
|
202
449
|
},
|
|
203
|
-
classify(
|
|
204
|
-
|
|
205
|
-
if (arango.errorNum === 1210)
|
|
206
|
-
return "terminal";
|
|
207
|
-
if (arango.errorNum === 1202)
|
|
208
|
-
return "terminal";
|
|
209
|
-
if (arango.errorNum === 1501)
|
|
210
|
-
return "terminal";
|
|
211
|
-
if (arango.code === 400 || arango.code === 404 || arango.code === 409)
|
|
212
|
-
return "terminal";
|
|
213
|
-
if (arango.code === 401 || arango.code === 403)
|
|
214
|
-
return "retryable";
|
|
215
|
-
if (arango.code === 503)
|
|
216
|
-
return "backoff";
|
|
217
|
-
return "retryable";
|
|
450
|
+
classify() {
|
|
451
|
+
return "terminal";
|
|
218
452
|
}
|
|
219
453
|
});
|
|
220
454
|
function resetPools() {
|
|
@@ -224,5 +458,9 @@ var databaseProviders = [arangodb];
|
|
|
224
458
|
export {
|
|
225
459
|
resetPools,
|
|
226
460
|
databaseProviders,
|
|
227
|
-
arangodb
|
|
461
|
+
arangodb,
|
|
462
|
+
arangoCoordinatorUrls,
|
|
463
|
+
MAX_ARANGO_COORDINATORS,
|
|
464
|
+
ARANGO_SERVER_MODE_PATH,
|
|
465
|
+
ARANGO_COMMUNITY_VERSION
|
|
228
466
|
};
|
package/dist/email.js
CHANGED
package/dist/http.js
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/pool.js
CHANGED
package/dist/storage.js
CHANGED
package/dist/translation.js
CHANGED