ydb-qdrant 7.0.1 → 8.1.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.
Files changed (52) hide show
  1. package/README.md +2 -2
  2. package/dist/config/env.d.ts +0 -8
  3. package/dist/config/env.js +2 -29
  4. package/dist/package/api.d.ts +5 -2
  5. package/dist/package/api.js +2 -2
  6. package/dist/qdrant/QdrantRestTypes.d.ts +35 -0
  7. package/dist/repositories/collectionsRepo.d.ts +1 -2
  8. package/dist/repositories/collectionsRepo.js +62 -103
  9. package/dist/repositories/collectionsRepo.one-table.js +103 -47
  10. package/dist/repositories/collectionsRepo.shared.d.ts +2 -0
  11. package/dist/repositories/collectionsRepo.shared.js +32 -0
  12. package/dist/repositories/pointsRepo.d.ts +4 -8
  13. package/dist/repositories/pointsRepo.one-table/Delete.js +122 -67
  14. package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.d.ts +5 -2
  15. package/dist/repositories/pointsRepo.one-table/PathSegmentsFilter.js +7 -6
  16. package/dist/repositories/pointsRepo.one-table/Search.d.ts +4 -0
  17. package/dist/repositories/pointsRepo.one-table/Search.js +208 -0
  18. package/dist/repositories/pointsRepo.one-table/Upsert.d.ts +2 -2
  19. package/dist/repositories/pointsRepo.one-table/Upsert.js +51 -66
  20. package/dist/repositories/pointsRepo.one-table.d.ts +1 -1
  21. package/dist/repositories/pointsRepo.one-table.js +1 -1
  22. package/dist/routes/collections.js +7 -61
  23. package/dist/routes/points.js +11 -66
  24. package/dist/services/PointsService.d.ts +3 -8
  25. package/dist/services/PointsService.js +19 -23
  26. package/dist/types.d.ts +23 -33
  27. package/dist/types.js +18 -20
  28. package/dist/utils/normalization.js +13 -14
  29. package/dist/utils/retry.js +19 -29
  30. package/dist/utils/vectorBinary.js +10 -5
  31. package/dist/ydb/bootstrapMetaTable.d.ts +7 -0
  32. package/dist/ydb/bootstrapMetaTable.js +75 -0
  33. package/dist/ydb/client.d.ts +23 -17
  34. package/dist/ydb/client.js +82 -423
  35. package/dist/ydb/schema.js +88 -148
  36. package/package.json +2 -10
  37. package/dist/qdrant/QdrantTypes.d.ts +0 -19
  38. package/dist/repositories/pointsRepo.one-table/Search/Approximate.d.ts +0 -18
  39. package/dist/repositories/pointsRepo.one-table/Search/Approximate.js +0 -119
  40. package/dist/repositories/pointsRepo.one-table/Search/Exact.d.ts +0 -17
  41. package/dist/repositories/pointsRepo.one-table/Search/Exact.js +0 -101
  42. package/dist/repositories/pointsRepo.one-table/Search/index.d.ts +0 -8
  43. package/dist/repositories/pointsRepo.one-table/Search/index.js +0 -30
  44. package/dist/utils/typeGuards.d.ts +0 -1
  45. package/dist/utils/typeGuards.js +0 -3
  46. package/dist/ydb/QueryDiagnostics.d.ts +0 -6
  47. package/dist/ydb/QueryDiagnostics.js +0 -52
  48. package/dist/ydb/SessionPool.d.ts +0 -36
  49. package/dist/ydb/SessionPool.js +0 -248
  50. package/dist/ydb/bulkUpsert.d.ts +0 -6
  51. package/dist/ydb/bulkUpsert.js +0 -52
  52. /package/dist/qdrant/{QdrantTypes.js → QdrantRestTypes.js} +0 -0
@@ -1,194 +1,50 @@
1
- import { Driver } from "@ydbjs/core";
2
- import { query } from "@ydbjs/query";
3
- import { CredentialsProvider } from "@ydbjs/auth";
4
- import { AnonymousCredentialsProvider } from "@ydbjs/auth/anonymous";
5
- import { AccessTokenCredentialsProvider } from "@ydbjs/auth/access-token";
6
- import { MetadataCredentialsProvider } from "@ydbjs/auth/metadata";
7
- import { readFile } from "node:fs/promises";
8
- import crypto from "node:crypto";
9
- import path from "node:path";
10
- import { createRequire } from "node:module";
11
- import { pathToFileURL } from "node:url";
12
- import { YDB_DATABASE, YDB_ENDPOINT, STARTUP_PROBE_SESSION_TIMEOUT_MS, } from "../config/env.js";
1
+ import { createRequire } from "module";
2
+ import { YDB_DATABASE, YDB_ENDPOINT, SESSION_POOL_MIN_SIZE, SESSION_POOL_MAX_SIZE, SESSION_KEEPALIVE_PERIOD_MS, STARTUP_PROBE_SESSION_TIMEOUT_MS, } from "../config/env.js";
13
3
  import { logger } from "../logging/logger.js";
14
- import { SessionPool } from "./SessionPool.js";
4
+ const require = createRequire(import.meta.url);
5
+ const { Driver, getCredentialsFromEnv, Types, TypedValues, TableDescription, Column, ExecuteQuerySettings, BulkUpsertSettings, OperationParams, Ydb, } = require("ydb-sdk");
6
+ export { Types, TypedValues, TableDescription, Column, ExecuteQuerySettings, BulkUpsertSettings, Ydb, };
7
+ export function createExecuteQuerySettings(options) {
8
+ const { keepInCache = true, idempotent = true } = options ?? {};
9
+ const settings = new ExecuteQuerySettings();
10
+ if (keepInCache) {
11
+ settings.withKeepInCache(true);
12
+ }
13
+ if (idempotent) {
14
+ settings.withIdempotent(true);
15
+ }
16
+ return settings;
17
+ }
18
+ export function createExecuteQuerySettingsWithTimeout(options) {
19
+ const settings = createExecuteQuerySettings(options);
20
+ const op = new OperationParams();
21
+ const seconds = Math.max(1, Math.ceil(options.timeoutMs / 1000));
22
+ // Limit both overall operation processing time and cancellation time on the
23
+ // server side so the probe fails fast instead of hanging for the default.
24
+ op.withOperationTimeoutSeconds(seconds);
25
+ op.withCancelAfterSeconds(seconds);
26
+ settings.withOperationParams(op);
27
+ return settings;
28
+ }
29
+ export function createBulkUpsertSettingsWithTimeout(options) {
30
+ const settings = new BulkUpsertSettings();
31
+ const op = new OperationParams();
32
+ const seconds = Math.max(1, Math.ceil(options.timeoutMs / 1000));
33
+ op.withOperationTimeoutSeconds(seconds);
34
+ op.withCancelAfterSeconds(seconds);
35
+ settings.withOperationParams(op);
36
+ return settings;
37
+ }
15
38
  const DRIVER_READY_TIMEOUT_MS = 15000;
39
+ const TABLE_SESSION_TIMEOUT_MS = 20000;
16
40
  const YDB_HEALTHCHECK_READY_TIMEOUT_MS = 5000;
17
41
  const DRIVER_REFRESH_COOLDOWN_MS = 30000;
18
- // Bound `withSession()` so `d.ready()` / session acquire can't hang forever under YDB stalls.
19
- // Matches historical table client session timeout behavior (~20s).
20
- const WITH_SESSION_TIMEOUT_MS = 20000;
21
42
  let overrideConfig;
22
43
  let driver;
23
- let sqlClient;
24
- let sessionPool;
25
44
  let lastDriverRefreshAt = 0;
26
45
  let driverRefreshInFlight = null;
27
- // Test-only: allows injecting a mock Driver factory.
46
+ // Test-only: allows injecting a mock Driver factory
28
47
  let driverFactoryOverride;
29
- let queryCtxPromise = null;
30
- async function getQueryCtx() {
31
- if (queryCtxPromise) {
32
- return await queryCtxPromise;
33
- }
34
- const require = createRequire(import.meta.url);
35
- const entry = require.resolve("@ydbjs/query");
36
- const ctxPath = path.join(path.dirname(entry), "ctx.js");
37
- queryCtxPromise = import(pathToFileURL(ctxPath).href);
38
- return await queryCtxPromise;
39
- }
40
- const IAM_TOKEN_URL = "https://iam.api.cloud.yandex.net/iam/v1/tokens";
41
- const SA_JWT_MAX_AGE_SECONDS = 3600;
42
- const SA_TOKEN_REFRESH_SKEW_MS = 60_000;
43
- // Yandex Cloud IAM tokens: lifetime does not exceed 12 hours, but the docs recommend
44
- // requesting a token more often (e.g., every hour). We refresh a bit earlier as a
45
- // best-effort fallback when the API response doesn't include expiresAt.
46
- // Source: https://cloud.yandex.com/en/docs/iam/operations/iam-token/create-for-sa
47
- const SA_TOKEN_REFRESH_FALLBACK_MS = 55 * 60_000;
48
- function parseTruthyEnv(value) {
49
- if (value === undefined)
50
- return false;
51
- const normalized = value.trim().toLowerCase();
52
- if (normalized === "" ||
53
- normalized === "0" ||
54
- normalized === "false" ||
55
- normalized === "no" ||
56
- normalized === "off") {
57
- return false;
58
- }
59
- return true;
60
- }
61
- function toBase64Url(input) {
62
- const buf = Buffer.isBuffer(input) ? input : Buffer.from(input);
63
- return buf
64
- .toString("base64")
65
- .replace(/\+/g, "-")
66
- .replace(/\//g, "_")
67
- .replace(/=+$/g, "");
68
- }
69
- function jsonToBase64Url(obj) {
70
- return toBase64Url(Buffer.from(JSON.stringify(obj), "utf8"));
71
- }
72
- function normalizeServiceAccountPrivateKeyPem(raw) {
73
- const trimmed = raw.trim();
74
- if (trimmed.startsWith("PLEASE DO NOT REMOVE THIS LINE!")) {
75
- return trimmed.split("\n").slice(1).join("\n").trim();
76
- }
77
- return trimmed;
78
- }
79
- async function readServiceAccountKeyFile(absPath) {
80
- const raw = await readFile(absPath, "utf8");
81
- const parsed = JSON.parse(raw);
82
- if (typeof parsed.id !== "string" ||
83
- typeof parsed.service_account_id !== "string" ||
84
- typeof parsed.private_key !== "string") {
85
- throw new Error("Invalid service account key JSON: expected fields id, service_account_id, private_key");
86
- }
87
- return {
88
- id: parsed.id,
89
- service_account_id: parsed.service_account_id,
90
- private_key: parsed.private_key,
91
- };
92
- }
93
- function createServiceAccountJwt(args) {
94
- // Per Yandex Cloud IAM docs, the service account JWT must be PS256 with kid
95
- // and audience set to the IAM token endpoint.
96
- const header = { typ: "JWT", alg: "PS256", kid: args.keyId };
97
- const payload = {
98
- iss: args.serviceAccountId,
99
- aud: IAM_TOKEN_URL,
100
- iat: args.nowSec,
101
- exp: args.nowSec + SA_JWT_MAX_AGE_SECONDS,
102
- };
103
- const encodedHeader = jsonToBase64Url(header);
104
- const encodedPayload = jsonToBase64Url(payload);
105
- const signingInput = `${encodedHeader}.${encodedPayload}`;
106
- const signature = crypto.sign("RSA-SHA256", Buffer.from(signingInput), {
107
- key: normalizeServiceAccountPrivateKeyPem(args.privateKeyPem),
108
- padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
109
- saltLength: 32,
110
- });
111
- return `${signingInput}.${toBase64Url(signature)}`;
112
- }
113
- async function exchangeJwtForIamToken(args) {
114
- const res = await fetch(IAM_TOKEN_URL, {
115
- method: "POST",
116
- headers: { "content-type": "application/json" },
117
- body: JSON.stringify({ jwt: args.jwt }),
118
- signal: args.signal,
119
- });
120
- if (!res.ok) {
121
- const text = await res.text().catch(() => "");
122
- throw new Error(`Failed to exchange JWT for IAM token: HTTP ${res.status}${text ? `: ${text}` : ""}`);
123
- }
124
- const body = (await res.json());
125
- const token = body.iamToken;
126
- if (typeof token !== "string" || token.length === 0) {
127
- throw new Error("IAM token exchange response is missing iamToken");
128
- }
129
- const expiresAtRaw = body.expiresAt ?? body.expires_at;
130
- const expiresAtMs = typeof expiresAtRaw === "string" ? Date.parse(expiresAtRaw) : NaN;
131
- return {
132
- iamToken: token,
133
- expiresAtMs: Number.isFinite(expiresAtMs) ? expiresAtMs : null,
134
- };
135
- }
136
- class ServiceAccountKeyFileCredentialsProvider extends CredentialsProvider {
137
- keyFilePath;
138
- cached = null;
139
- constructor(keyFilePath) {
140
- super();
141
- this.keyFilePath = keyFilePath;
142
- }
143
- async getToken(force = false, signal) {
144
- const nowMs = Date.now();
145
- if (!force && this.cached) {
146
- const { token, expiresAtMs, cachedAtMs } = this.cached;
147
- if (expiresAtMs !== null) {
148
- if (nowMs + SA_TOKEN_REFRESH_SKEW_MS < expiresAtMs) {
149
- return token;
150
- }
151
- }
152
- else {
153
- // If the API doesn't provide expiresAt, refresh hourly (best-effort).
154
- if (nowMs - cachedAtMs < SA_TOKEN_REFRESH_FALLBACK_MS) {
155
- return token;
156
- }
157
- }
158
- }
159
- const key = await readServiceAccountKeyFile(this.keyFilePath);
160
- const nowSec = Math.floor(nowMs / 1000);
161
- const jwt = createServiceAccountJwt({
162
- keyId: key.id,
163
- serviceAccountId: key.service_account_id,
164
- privateKeyPem: key.private_key,
165
- nowSec,
166
- });
167
- const { iamToken, expiresAtMs } = await exchangeJwtForIamToken({
168
- jwt,
169
- signal,
170
- });
171
- this.cached = { token: iamToken, expiresAtMs, cachedAtMs: nowMs };
172
- return iamToken;
173
- }
174
- }
175
- function resolveCredentialsProviderFromEnv() {
176
- const saKeyPath = process.env.YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS?.trim();
177
- if (saKeyPath) {
178
- return new ServiceAccountKeyFileCredentialsProvider(saKeyPath);
179
- }
180
- if (parseTruthyEnv(process.env.YDB_METADATA_CREDENTIALS)) {
181
- return new MetadataCredentialsProvider();
182
- }
183
- const accessToken = process.env.YDB_ACCESS_TOKEN_CREDENTIALS?.trim();
184
- if (accessToken) {
185
- return new AccessTokenCredentialsProvider({ token: accessToken });
186
- }
187
- if (parseTruthyEnv(process.env.YDB_ANONYMOUS_CREDENTIALS)) {
188
- return new AnonymousCredentialsProvider();
189
- }
190
- throw new Error("No YDB credentials configured. Set one of: YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS, YDB_METADATA_CREDENTIALS=1, YDB_ACCESS_TOKEN_CREDENTIALS, YDB_ANONYMOUS_CREDENTIALS=1");
191
- }
192
48
  export function isCompilationTimeoutError(error) {
193
49
  if (!(error instanceof Error)) {
194
50
  return false;
@@ -207,54 +63,6 @@ export function isCompilationTimeoutError(error) {
207
63
  }
208
64
  return false;
209
65
  }
210
- function getErrorCause(error) {
211
- return error.cause;
212
- }
213
- function getCauseName(cause) {
214
- if (cause instanceof Error) {
215
- return cause.name;
216
- }
217
- if (typeof cause === "object" && cause !== null) {
218
- const n = cause.name;
219
- if (typeof n === "string") {
220
- return n;
221
- }
222
- }
223
- return undefined;
224
- }
225
- export function isTimeoutAbortError(error) {
226
- // `@ydbjs/query` implements `.timeout(ms)` by adding `AbortSignal.timeout(ms)` to the query
227
- // signal chain. When that fires, @ydbjs/retry wraps it via `@ydbjs/abortable` and we end up
228
- // observing a DOMException `{ name: "AbortError" }`, often with an underlying `cause`
229
- // `{ name: "TimeoutError" }`.
230
- if (!(error instanceof Error)) {
231
- return false;
232
- }
233
- if (error.name === "TimeoutError") {
234
- return true;
235
- }
236
- if (error.name !== "AbortError") {
237
- return false;
238
- }
239
- const cause = getErrorCause(error);
240
- const causeName = getCauseName(cause);
241
- if (causeName === "TimeoutError") {
242
- return true;
243
- }
244
- // Node versions differ in how abort reasons are propagated through `AbortSignal.any(...)`.
245
- // In our codebase we currently only use AbortError signals for query timeouts / cancellations,
246
- // so treat an AbortError without a usable cause as timeout-like.
247
- return true;
248
- }
249
- export function getAbortErrorCause(error) {
250
- if (!(error instanceof Error)) {
251
- return undefined;
252
- }
253
- if (error.name !== "AbortError") {
254
- return undefined;
255
- }
256
- return getErrorCause(error);
257
- }
258
66
  function shouldTriggerDriverRefresh(error) {
259
67
  if (!(error instanceof Error)) {
260
68
  return false;
@@ -269,8 +77,9 @@ function shouldTriggerDriverRefresh(error) {
269
77
  if (/SessionExpired|SESSION_EXPIRED|session.*expired/i.test(msg)) {
270
78
  return true;
271
79
  }
272
- // Compilation timeout is treated as a signal to refresh the driver so that
273
- // subsequent attempts use a fresh connection state.
80
+ // YDB query compilation timeout (TIMEOUT code 400090) treat as a signal
81
+ // to refresh the driver/session pool so that subsequent attempts use a
82
+ // fresh connection state.
274
83
  if (isCompilationTimeoutError(error)) {
275
84
  return true;
276
85
  }
@@ -306,12 +115,6 @@ async function maybeRefreshDriverOnSessionError(error) {
306
115
  }
307
116
  export function __setDriverForTests(fake) {
308
117
  driver = fake;
309
- sqlClient = undefined;
310
- sessionPool = undefined;
311
- queryCtxPromise = null;
312
- }
313
- export function __setSessionPoolForTests(fake) {
314
- sessionPool = fake;
315
118
  }
316
119
  export function __setDriverFactoryForTests(factory) {
317
120
  driverFactoryOverride = factory;
@@ -327,207 +130,81 @@ export function configureDriver(config) {
327
130
  }
328
131
  overrideConfig = config;
329
132
  }
330
- function buildConnectionString(args) {
331
- const endpoint = args.endpoint.trim();
332
- const database = args.database.trim();
333
- if (!endpoint) {
334
- throw new Error("YDB endpoint is empty; set YDB_ENDPOINT");
335
- }
336
- if (!database) {
337
- throw new Error("YDB database is empty; set YDB_DATABASE");
338
- }
339
- const url = new URL(endpoint);
340
- const dbPath = database.startsWith("/") ? database : `/${database}`;
341
- url.pathname = dbPath;
342
- return url.toString();
343
- }
344
133
  function getOrCreateDriver() {
345
134
  if (driver) {
346
135
  return driver;
347
136
  }
348
- const endpoint = overrideConfig?.endpoint ?? YDB_ENDPOINT;
349
- const database = overrideConfig?.database ?? YDB_DATABASE;
350
- let connectionString;
351
- if (overrideConfig?.connectionString) {
352
- connectionString = overrideConfig.connectionString;
353
- }
354
- else if (driverFactoryOverride) {
355
- // Tests may inject a driver factory without setting env vars.
356
- // If endpoint/database are available, still build a real connection string.
357
- const hasEndpoint = endpoint.trim().length > 0;
358
- const hasDatabase = database.trim().length > 0;
359
- connectionString =
360
- hasEndpoint && hasDatabase
361
- ? buildConnectionString({ endpoint, database })
362
- : "";
363
- }
364
- else {
365
- connectionString = buildConnectionString({ endpoint, database });
366
- }
367
- const credentialsProvider = overrideConfig?.credentialsProvider
368
- ? overrideConfig.credentialsProvider
369
- : driverFactoryOverride
370
- ? undefined
371
- : resolveCredentialsProviderFromEnv();
372
- const driverOptions = {
373
- ...(credentialsProvider ? { credentialsProvider } : {}),
374
- "ydb.sdk.ready_timeout_ms": DRIVER_READY_TIMEOUT_MS,
137
+ const base = overrideConfig?.connectionString != null
138
+ ? { connectionString: overrideConfig.connectionString }
139
+ : {
140
+ endpoint: overrideConfig?.endpoint ?? YDB_ENDPOINT,
141
+ database: overrideConfig?.database ?? YDB_DATABASE,
142
+ };
143
+ const driverConfig = {
144
+ ...base,
145
+ authService: overrideConfig?.authService ?? getCredentialsFromEnv(),
146
+ poolSettings: {
147
+ minLimit: SESSION_POOL_MIN_SIZE,
148
+ maxLimit: SESSION_POOL_MAX_SIZE,
149
+ keepAlivePeriod: SESSION_KEEPALIVE_PERIOD_MS,
150
+ },
375
151
  };
376
152
  driver = driverFactoryOverride
377
- ? driverFactoryOverride(connectionString, driverOptions)
378
- : new Driver(connectionString, driverOptions);
379
- logger.info({ hasCredentialsProvider: true, connectionStringMasked: true }, "YDB driver created");
153
+ ? driverFactoryOverride(driverConfig)
154
+ : new Driver(driverConfig);
155
+ logger.info({
156
+ poolMinSize: SESSION_POOL_MIN_SIZE,
157
+ poolMaxSize: SESSION_POOL_MAX_SIZE,
158
+ keepAlivePeriodMs: SESSION_KEEPALIVE_PERIOD_MS,
159
+ }, "YDB driver created with session pool settings");
380
160
  return driver;
381
161
  }
382
- /**
383
- * @internal
384
- * Exposes the singleton YDB `Driver` for internal modules that need non-QueryService RPCs
385
- * (e.g. TableService BulkUpsert).
386
- */
387
- export function __getDriverForInternalUse() {
388
- return getOrCreateDriver();
389
- }
390
- function getOrCreateQueryClient() {
391
- if (sqlClient) {
392
- return sqlClient;
393
- }
162
+ export async function readyOrThrow() {
394
163
  const d = getOrCreateDriver();
395
- sqlClient = query(d);
396
- return sqlClient;
397
- }
398
- function getOrCreateSessionPool() {
399
- if (sessionPool) {
400
- return sessionPool;
164
+ const ok = await d.ready(DRIVER_READY_TIMEOUT_MS);
165
+ if (!ok) {
166
+ throw new Error(`YDB driver is not ready in ${DRIVER_READY_TIMEOUT_MS / 1000}s. Check connectivity and credentials.`);
401
167
  }
402
- const d = getOrCreateDriver();
403
- sessionPool = new SessionPool(d);
404
- // Keepalive is best-effort; don't block startup.
405
- sessionPool.start();
406
- return sessionPool;
407
168
  }
408
- async function withTimeoutSignal(timeoutMs, fn) {
409
- const ac = new AbortController();
410
- const t = setTimeout(() => ac.abort(), timeoutMs);
169
+ export async function withSession(fn) {
170
+ const d = getOrCreateDriver();
411
171
  try {
412
- return await fn(ac.signal);
172
+ return await d.tableClient.withSession(fn, TABLE_SESSION_TIMEOUT_MS);
413
173
  }
414
- finally {
415
- clearTimeout(t);
174
+ catch (err) {
175
+ void maybeRefreshDriverOnSessionError(err);
176
+ throw err;
416
177
  }
417
178
  }
418
- export async function readyOrThrow() {
419
- const d = getOrCreateDriver();
420
- await withTimeoutSignal(DRIVER_READY_TIMEOUT_MS, async (signal) => {
421
- await d.ready(signal);
422
- });
423
- }
424
- export async function withSession(fn) {
425
- const sql = getOrCreateQueryClient();
179
+ export async function withQuerySession(fn, options) {
426
180
  const d = getOrCreateDriver();
427
- const pool = getOrCreateSessionPool();
428
181
  try {
429
- // Bound only `d.ready()` and `pool.acquire()` so stalls in driver readiness or session
430
- // availability can't hang forever. We intentionally do NOT abort the user callback,
431
- // because request operations can legitimately exceed this bound (e.g. multi-batch upserts).
432
- await withTimeoutSignal(WITH_SESSION_TIMEOUT_MS, async (signal) => {
433
- await d.ready(signal);
182
+ return await d.queryClient.do({
183
+ fn,
184
+ timeout: options?.timeoutMs ?? TABLE_SESSION_TIMEOUT_MS,
185
+ idempotent: options?.idempotent,
434
186
  });
435
- // Ensure we have a few sessions ready to reduce cold-start session churn.
436
- // Best-effort: ignore failures/timeouts.
437
- void withTimeoutSignal(WITH_SESSION_TIMEOUT_MS, async (signal) => {
438
- await pool.warmup(signal);
439
- }).catch(() => undefined);
440
- // @ydbjs/query QueryClient.do() is not implemented in the currently published builds.
441
- // Cancellation is cooperative: we provide an AbortSignal to `fn()` and also store it in
442
- // the @ydbjs/query async-local context so query operations can observe it via `.signal(...)`.
443
- // (Note: this signal is not auto-aborted by `withSession()`; query-level `.timeout(...)`
444
- // is the primary guard for long-running YQL statements.)
445
- const operationSignal = new AbortController().signal;
446
- const ctxMod = await getQueryCtx();
447
- let leased = null;
448
- const MAX_ATTEMPTS = 3;
449
- for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt += 1) {
450
- leased = await withTimeoutSignal(WITH_SESSION_TIMEOUT_MS, async (signal) => {
451
- return await pool.acquire(signal);
452
- });
453
- try {
454
- const store = {
455
- nodeId: leased.nodeId,
456
- sessionId: leased.sessionId,
457
- signal: operationSignal,
458
- };
459
- const result = await ctxMod.ctx.run(store, async () => {
460
- return await fn(sql, operationSignal);
461
- });
462
- pool.release(leased);
463
- return result;
464
- }
465
- catch (err) {
466
- const shouldRetryWithFreshSession = err instanceof Error &&
467
- /BAD_SESSION|SESSION_EXPIRED|SessionExpired|No session became available/i.test(err.message ?? "");
468
- if (shouldRetryWithFreshSession) {
469
- await pool.discard(leased);
470
- leased = null;
471
- if (attempt === MAX_ATTEMPTS - 1) {
472
- throw err;
473
- }
474
- continue;
475
- }
476
- pool.release(leased);
477
- throw err;
478
- }
479
- }
480
- throw new Error("withSession: exhausted attempts to acquire a healthy session");
481
187
  }
482
188
  catch (err) {
189
+ // Query sessions can also get stuck/busy; reuse the same driver refresh path.
483
190
  void maybeRefreshDriverOnSessionError(err);
484
191
  throw err;
485
192
  }
486
193
  }
487
194
  export async function withStartupProbeSession(fn) {
488
- const sql = getOrCreateQueryClient();
489
195
  const d = getOrCreateDriver();
490
- const pool = getOrCreateSessionPool();
491
- const ac = new AbortController();
492
- const t = setTimeout(() => ac.abort(), STARTUP_PROBE_SESSION_TIMEOUT_MS);
493
196
  try {
494
- // Ensure the startup probe timeout also applies to driver readiness.
495
- await d.ready(ac.signal);
496
- void pool.warmup(ac.signal);
497
- const ctxMod = await getQueryCtx();
498
- const leased = await pool.acquire(ac.signal);
499
- try {
500
- const store = {
501
- nodeId: leased.nodeId,
502
- sessionId: leased.sessionId,
503
- signal: ac.signal,
504
- };
505
- const result = await ctxMod.ctx.run(store, async () => {
506
- return await fn(sql, ac.signal);
507
- });
508
- pool.release(leased);
509
- return result;
510
- }
511
- catch (err) {
512
- pool.release(leased);
513
- throw err;
514
- }
197
+ return await d.tableClient.withSession(fn, STARTUP_PROBE_SESSION_TIMEOUT_MS);
515
198
  }
516
199
  catch (err) {
517
200
  void maybeRefreshDriverOnSessionError(err);
518
201
  throw err;
519
202
  }
520
- finally {
521
- clearTimeout(t);
522
- }
523
203
  }
524
204
  export async function isYdbAvailable(timeoutMs = YDB_HEALTHCHECK_READY_TIMEOUT_MS) {
525
205
  const d = getOrCreateDriver();
526
206
  try {
527
- await withTimeoutSignal(timeoutMs, async (signal) => {
528
- await d.ready(signal);
529
- });
530
- return true;
207
+ return await d.ready(timeoutMs);
531
208
  }
532
209
  catch {
533
210
  return false;
@@ -541,27 +218,9 @@ export async function destroyDriver() {
541
218
  if (!driver) {
542
219
  return;
543
220
  }
544
- logger.info("Destroying YDB driver");
221
+ logger.info("Destroying YDB driver and session pool");
545
222
  try {
546
- if (sessionPool) {
547
- try {
548
- await sessionPool.close();
549
- }
550
- catch (err) {
551
- logger.warn({ err }, "Error during session pool close (ignored)");
552
- }
553
- sessionPool = undefined;
554
- }
555
- if (sqlClient) {
556
- try {
557
- await sqlClient[Symbol.asyncDispose]();
558
- }
559
- catch (err) {
560
- logger.warn({ err }, "Error during query client disposal (ignored)");
561
- }
562
- sqlClient = undefined;
563
- }
564
- driver.close();
223
+ await driver.destroy();
565
224
  }
566
225
  catch (err) {
567
226
  logger.warn({ err }, "Error during driver destruction (ignored)");