@atscript/db-mongo 0.1.162 → 0.1.164

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -2212,6 +2212,37 @@ const validateMongoIdPlugin = (ctx, def, value) => {
2212
2212
  }
2213
2213
  };
2214
2214
  //#endregion
2215
+ //#region src/lib/mongo-transaction-support.ts
2216
+ const verdicts = /* @__PURE__ */ new WeakMap();
2217
+ /** Whether the client's server runs transactions, asked once per client; a failed probe is retried. */
2218
+ function transactionSupport(client) {
2219
+ let verdict = verdicts.get(client);
2220
+ if (!verdict) {
2221
+ verdict = probe(client).catch((error) => {
2222
+ verdicts.delete(client);
2223
+ throw error;
2224
+ });
2225
+ verdicts.set(client, verdict);
2226
+ }
2227
+ return verdict;
2228
+ }
2229
+ /**
2230
+ * A replica-set member reports `setName` (an uninitiated `--replSet` member
2231
+ * `isreplicaset: true`), a mongos — also behind a load balancer —
2232
+ * `msg: 'isdbgrid'`. Anything else is a standalone `mongod`.
2233
+ */
2234
+ async function probe(client) {
2235
+ const admin = client.db("admin");
2236
+ let reply;
2237
+ try {
2238
+ reply = await admin.command({ hello: 1 });
2239
+ } catch (error) {
2240
+ if (!(error instanceof mongodb.MongoServerError) || error.code !== 59) throw error;
2241
+ reply = await admin.command({ isMaster: 1 });
2242
+ }
2243
+ return reply.setName != null || reply.isreplicaset === true || reply.msg === "isdbgrid";
2244
+ }
2245
+ //#endregion
2215
2246
  //#region src/lib/mongo-adapter.ts
2216
2247
  /** Mirrors the `mongo.objectId` primitive's pattern — only definite hex ids convert. */
2217
2248
  const OBJECT_ID_HEX = /^[a-fA-F0-9]{24}$/;
@@ -2297,48 +2328,30 @@ var MongoAdapter = class MongoAdapter extends _atscript_db.BaseDbAdapter {
2297
2328
  return other instanceof MongoAdapter && other._transactionOwner() === this._transactionOwner() && other.db.databaseName === this.db.databaseName;
2298
2329
  }
2299
2330
  /**
2300
- * Per-client cache: whether transactions are unavailable (standalone MongoDB).
2301
- * Shared across all adapter instances for the same client so topology is probed once.
2302
- */
2303
- static _txDisabledClients = /* @__PURE__ */ new WeakSet();
2304
- get _txDisabled() {
2305
- return this.client ? MongoAdapter._txDisabledClients.has(this.client) : true;
2306
- }
2307
- set _txDisabled(value) {
2308
- if (value && this.client) MongoAdapter._txDisabledClients.add(this.client);
2309
- }
2310
- /**
2311
2331
  * Uses MongoDB's Convenient Transaction API (`session.withTransaction()`).
2312
2332
  * This handles txnNumber management and automatic retry for
2313
2333
  * `TransientTransactionError` / `UnknownTransactionCommitResult`.
2314
2334
  *
2335
+ * Transaction support is read from the server, not the connection mode: a
2336
+ * `directConnection=true` client to a replica-set primary runs real
2337
+ * transactions; a standalone `mongod` (or an adapter without a client)
2338
+ * runs `fn` without one.
2339
+ *
2315
2340
  * `opts.isolation` (since 0.1.162): a MongoDB transaction reads one
2316
2341
  * snapshot, which satisfies `'read committed'` and `'repeatable read'`;
2317
2342
  * `'serializable'` throws `ISOLATION_NOT_SUPPORTED` (snapshot isolation
2318
- * allows write skew), and so does any level on a topology without
2319
- * transactions (standalone), where `fn` would run without one.
2343
+ * allows write skew), and so does any level where `fn` would run without
2344
+ * a transaction.
2320
2345
  */
2321
2346
  async withTransaction(fn, opts) {
2322
2347
  const isolation = this._checkIsolation(opts);
2323
2348
  if (this._getTransactionState()) return fn();
2324
- const passThrough = () => {
2325
- if (isolation) throw this._isolationNotSupported(`Isolation '${isolation}' needs a MongoDB transaction — this topology (standalone) has none`);
2349
+ const passThrough = (reason) => {
2350
+ if (isolation) throw this._isolationNotSupported(`Isolation '${isolation}' needs a MongoDB transaction — ${reason}`);
2326
2351
  return fn();
2327
2352
  };
2328
- if (this._txDisabled || !this._client) return passThrough();
2329
- try {
2330
- const topology = this._client.topology;
2331
- if (topology) {
2332
- const type = (topology.description ?? topology.s?.description)?.type;
2333
- if (type === "Single" || type === "Unknown") {
2334
- this._txDisabled = true;
2335
- return passThrough();
2336
- }
2337
- }
2338
- } catch {
2339
- this._txDisabled = true;
2340
- return passThrough();
2341
- }
2353
+ if (!this._client) return passThrough("the adapter was created without a MongoClient");
2354
+ if (!await transactionSupport(this._client)) return passThrough("the server is a standalone mongod (transactions need a replica set or mongos)");
2342
2355
  const session = this._client.startSession();
2343
2356
  try {
2344
2357
  let result;
package/dist/index.d.cts CHANGED
@@ -318,23 +318,21 @@ declare class MongoAdapter extends BaseDbAdapter {
318
318
  * client may serve several (since 0.1.147).
319
319
  */
320
320
  sharesStoreWith(other: BaseDbAdapter): boolean;
321
- /**
322
- * Per-client cache: whether transactions are unavailable (standalone MongoDB).
323
- * Shared across all adapter instances for the same client so topology is probed once.
324
- */
325
- private static readonly _txDisabledClients;
326
- private get _txDisabled();
327
- private set _txDisabled(value);
328
321
  /**
329
322
  * Uses MongoDB's Convenient Transaction API (`session.withTransaction()`).
330
323
  * This handles txnNumber management and automatic retry for
331
324
  * `TransientTransactionError` / `UnknownTransactionCommitResult`.
332
325
  *
326
+ * Transaction support is read from the server, not the connection mode: a
327
+ * `directConnection=true` client to a replica-set primary runs real
328
+ * transactions; a standalone `mongod` (or an adapter without a client)
329
+ * runs `fn` without one.
330
+ *
333
331
  * `opts.isolation` (since 0.1.162): a MongoDB transaction reads one
334
332
  * snapshot, which satisfies `'read committed'` and `'repeatable read'`;
335
333
  * `'serializable'` throws `ISOLATION_NOT_SUPPORTED` (snapshot isolation
336
- * allows write skew), and so does any level on a topology without
337
- * transactions (standalone), where `fn` would run without one.
334
+ * allows write skew), and so does any level where `fn` would run without
335
+ * a transaction.
338
336
  */
339
337
  withTransaction<T>(fn: () => Promise<T>, opts?: TTransactionOptions): Promise<T>;
340
338
  private static readonly _ISOLATION_LEVELS;
package/dist/index.d.mts CHANGED
@@ -318,23 +318,21 @@ declare class MongoAdapter extends BaseDbAdapter {
318
318
  * client may serve several (since 0.1.147).
319
319
  */
320
320
  sharesStoreWith(other: BaseDbAdapter): boolean;
321
- /**
322
- * Per-client cache: whether transactions are unavailable (standalone MongoDB).
323
- * Shared across all adapter instances for the same client so topology is probed once.
324
- */
325
- private static readonly _txDisabledClients;
326
- private get _txDisabled();
327
- private set _txDisabled(value);
328
321
  /**
329
322
  * Uses MongoDB's Convenient Transaction API (`session.withTransaction()`).
330
323
  * This handles txnNumber management and automatic retry for
331
324
  * `TransientTransactionError` / `UnknownTransactionCommitResult`.
332
325
  *
326
+ * Transaction support is read from the server, not the connection mode: a
327
+ * `directConnection=true` client to a replica-set primary runs real
328
+ * transactions; a standalone `mongod` (or an adapter without a client)
329
+ * runs `fn` without one.
330
+ *
333
331
  * `opts.isolation` (since 0.1.162): a MongoDB transaction reads one
334
332
  * snapshot, which satisfies `'read committed'` and `'repeatable read'`;
335
333
  * `'serializable'` throws `ISOLATION_NOT_SUPPORTED` (snapshot isolation
336
- * allows write skew), and so does any level on a topology without
337
- * transactions (standalone), where `fn` would run without one.
334
+ * allows write skew), and so does any level where `fn` would run without
335
+ * a transaction.
338
336
  */
339
337
  withTransaction<T>(fn: () => Promise<T>, opts?: TTransactionOptions): Promise<T>;
340
338
  private static readonly _ISOLATION_LEVELS;
package/dist/index.mjs CHANGED
@@ -2211,6 +2211,37 @@ const validateMongoIdPlugin = (ctx, def, value) => {
2211
2211
  }
2212
2212
  };
2213
2213
  //#endregion
2214
+ //#region src/lib/mongo-transaction-support.ts
2215
+ const verdicts = /* @__PURE__ */ new WeakMap();
2216
+ /** Whether the client's server runs transactions, asked once per client; a failed probe is retried. */
2217
+ function transactionSupport(client) {
2218
+ let verdict = verdicts.get(client);
2219
+ if (!verdict) {
2220
+ verdict = probe(client).catch((error) => {
2221
+ verdicts.delete(client);
2222
+ throw error;
2223
+ });
2224
+ verdicts.set(client, verdict);
2225
+ }
2226
+ return verdict;
2227
+ }
2228
+ /**
2229
+ * A replica-set member reports `setName` (an uninitiated `--replSet` member
2230
+ * `isreplicaset: true`), a mongos — also behind a load balancer —
2231
+ * `msg: 'isdbgrid'`. Anything else is a standalone `mongod`.
2232
+ */
2233
+ async function probe(client) {
2234
+ const admin = client.db("admin");
2235
+ let reply;
2236
+ try {
2237
+ reply = await admin.command({ hello: 1 });
2238
+ } catch (error) {
2239
+ if (!(error instanceof MongoServerError) || error.code !== 59) throw error;
2240
+ reply = await admin.command({ isMaster: 1 });
2241
+ }
2242
+ return reply.setName != null || reply.isreplicaset === true || reply.msg === "isdbgrid";
2243
+ }
2244
+ //#endregion
2214
2245
  //#region src/lib/mongo-adapter.ts
2215
2246
  /** Mirrors the `mongo.objectId` primitive's pattern — only definite hex ids convert. */
2216
2247
  const OBJECT_ID_HEX = /^[a-fA-F0-9]{24}$/;
@@ -2296,48 +2327,30 @@ var MongoAdapter = class MongoAdapter extends BaseDbAdapter {
2296
2327
  return other instanceof MongoAdapter && other._transactionOwner() === this._transactionOwner() && other.db.databaseName === this.db.databaseName;
2297
2328
  }
2298
2329
  /**
2299
- * Per-client cache: whether transactions are unavailable (standalone MongoDB).
2300
- * Shared across all adapter instances for the same client so topology is probed once.
2301
- */
2302
- static _txDisabledClients = /* @__PURE__ */ new WeakSet();
2303
- get _txDisabled() {
2304
- return this.client ? MongoAdapter._txDisabledClients.has(this.client) : true;
2305
- }
2306
- set _txDisabled(value) {
2307
- if (value && this.client) MongoAdapter._txDisabledClients.add(this.client);
2308
- }
2309
- /**
2310
2330
  * Uses MongoDB's Convenient Transaction API (`session.withTransaction()`).
2311
2331
  * This handles txnNumber management and automatic retry for
2312
2332
  * `TransientTransactionError` / `UnknownTransactionCommitResult`.
2313
2333
  *
2334
+ * Transaction support is read from the server, not the connection mode: a
2335
+ * `directConnection=true` client to a replica-set primary runs real
2336
+ * transactions; a standalone `mongod` (or an adapter without a client)
2337
+ * runs `fn` without one.
2338
+ *
2314
2339
  * `opts.isolation` (since 0.1.162): a MongoDB transaction reads one
2315
2340
  * snapshot, which satisfies `'read committed'` and `'repeatable read'`;
2316
2341
  * `'serializable'` throws `ISOLATION_NOT_SUPPORTED` (snapshot isolation
2317
- * allows write skew), and so does any level on a topology without
2318
- * transactions (standalone), where `fn` would run without one.
2342
+ * allows write skew), and so does any level where `fn` would run without
2343
+ * a transaction.
2319
2344
  */
2320
2345
  async withTransaction(fn, opts) {
2321
2346
  const isolation = this._checkIsolation(opts);
2322
2347
  if (this._getTransactionState()) return fn();
2323
- const passThrough = () => {
2324
- if (isolation) throw this._isolationNotSupported(`Isolation '${isolation}' needs a MongoDB transaction — this topology (standalone) has none`);
2348
+ const passThrough = (reason) => {
2349
+ if (isolation) throw this._isolationNotSupported(`Isolation '${isolation}' needs a MongoDB transaction — ${reason}`);
2325
2350
  return fn();
2326
2351
  };
2327
- if (this._txDisabled || !this._client) return passThrough();
2328
- try {
2329
- const topology = this._client.topology;
2330
- if (topology) {
2331
- const type = (topology.description ?? topology.s?.description)?.type;
2332
- if (type === "Single" || type === "Unknown") {
2333
- this._txDisabled = true;
2334
- return passThrough();
2335
- }
2336
- }
2337
- } catch {
2338
- this._txDisabled = true;
2339
- return passThrough();
2340
- }
2352
+ if (!this._client) return passThrough("the adapter was created without a MongoClient");
2353
+ if (!await transactionSupport(this._client)) return passThrough("the server is a standalone mongod (transactions need a replica set or mongos)");
2341
2354
  const session = this._client.startSession();
2342
2355
  try {
2343
2356
  let result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db-mongo",
3
- "version": "0.1.162",
3
+ "version": "0.1.164",
4
4
  "description": "Mongodb plugin for atscript.",
5
5
  "keywords": [
6
6
  "atscript",
@@ -58,7 +58,7 @@
58
58
  "@atscript/typescript": "^0.1.109",
59
59
  "@uniqu/core": "^0.1.15",
60
60
  "mongodb": "^6.17.0",
61
- "@atscript/db": "^0.1.162"
61
+ "@atscript/db": "^0.1.164"
62
62
  },
63
63
  "scripts": {
64
64
  "build": "vp pack",