@simtlix/simfinity-js 3.2.0 → 3.4.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/package.json CHANGED
@@ -1,56 +1,43 @@
1
- {
2
- "name": "@simtlix/simfinity-js",
3
- "version": "3.2.0",
4
- "description": "",
5
- "main": "src/index.js",
6
- "types": "./types/index.d.ts",
7
- "type": "module",
8
- "scripts": {
9
- "test": "vitest run",
10
- "test:watch": "vitest",
11
- "test:coverage": "vitest --coverage",
12
- "lint": "eslint \"**/*.js\"",
13
- "lint-fix": "eslint --fix \"**/*.js\"",
14
- "test:integration": "vitest run tests/integration",
15
- "test:packages": "node scripts/test-packages.js",
16
- "docs:install": "npm ci --prefix docs",
17
- "docs:dev": "npm run dev --prefix docs --",
18
- "docs:build": "npm run build --prefix docs",
19
- "docs:preview": "npm run preview --prefix docs --"
20
- },
21
- "engines": {
22
- "node": ">=18.18.0"
23
- },
24
- "author": "Simtlix",
25
- "license": "Apache-2.0",
26
- "repository": {
27
- "type": "git",
28
- "url": "https://github.com/simtlix/simfinity.js"
29
- },
30
- "peerDependencies": {
31
- "graphql": "^16.11.0",
32
- "mongoose": "^8.16.2"
33
- },
34
- "devDependencies": {
35
- "@eslint/js": "^9.30.1",
36
- "eslint": "^9.30.1",
37
- "globals": "^16.3.0",
38
- "vitest": "^4.1.9"
39
- },
40
- "config": {
41
- "owner": "simtlix"
42
- },
43
- "optionalDependencies": {
44
- "@modelcontextprotocol/sdk": "^1.13.0",
45
- "graphql": "^16.11.0",
46
- "graphql-middleware": "^6.1.35",
47
- "mongoose": "^8.16.2"
48
- },
49
- "workspaces": [
50
- "packages/*"
51
- ],
52
- "dependencies": {
53
- "@simtlix/simfinity-core": "3.2.0",
54
- "@simtlix/simfinity-mcp": "3.2.0"
55
- }
56
- }
1
+ {
2
+ "name": "@simtlix/simfinity-js",
3
+ "version": "3.4.0",
4
+ "description": "MongoDB/Mongoose adapter and compatibility facade for Simfinity GraphQL APIs",
5
+ "main": "src/index.js",
6
+ "types": "./types/index.d.ts",
7
+ "type": "module",
8
+ "engines": {
9
+ "node": ">=18.18.0"
10
+ },
11
+ "author": "Simtlix",
12
+ "license": "Apache-2.0",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/simtlix/simfinity.js",
16
+ "directory": "packages/mongodb"
17
+ },
18
+ "peerDependencies": {
19
+ "graphql": "^16.11.0",
20
+ "mongoose": "^8.16.2"
21
+ },
22
+ "config": {
23
+ "owner": "simtlix"
24
+ },
25
+ "optionalDependencies": {
26
+ "@modelcontextprotocol/sdk": "^1.13.0",
27
+ "graphql": "^16.11.0",
28
+ "graphql-middleware": "^6.1.35",
29
+ "mongoose": "^8.16.2"
30
+ },
31
+ "dependencies": {
32
+ "@simtlix/simfinity-core": "3.4.0",
33
+ "@simtlix/simfinity-mcp": "3.4.0"
34
+ },
35
+ "files": [
36
+ "src",
37
+ "types",
38
+ "README.md",
39
+ "LICENSE",
40
+ "AGGREGATION_EXAMPLE.md",
41
+ "AGGREGATION_CHANGES_SUMMARY.md"
42
+ ]
43
+ }
@@ -4,12 +4,14 @@ import { getNamedType, isListType, isNonNullType } from 'graphql';
4
4
  import { createMongoModel } from './models.js';
5
5
  import { createMongoQueries } from './queries.js';
6
6
  import { withMongoTransaction } from './transactions.js';
7
+ import { createMongoIntegrity } from './integrity.js';
7
8
 
8
9
  mongoose.set('strictQuery', false);
9
10
 
10
11
  const withSession = (query, session) => (session ? query.session(session) : query);
11
12
 
12
- export const createMongoAdapter = () => {
13
+ export const createMongoAdapter = (options) => {
14
+ const integrity = createMongoIntegrity(options);
13
15
  let queries;
14
16
  const privateConnectionFields = new Map();
15
17
 
@@ -20,11 +22,13 @@ export const createMongoAdapter = () => {
20
22
 
21
23
  const adapter = {
22
24
  bind(binding) {
25
+ integrity.bind(binding);
23
26
  queries = createMongoQueries(binding);
24
27
  adapter.buildQuery = queries.buildQuery;
25
28
  adapter.buildFilterGroupMatch = queries.buildFilterGroupMatch;
26
29
  },
27
- prepare(registrations) {
30
+ prepare(registrations, preparationOptions) {
31
+ integrity.prepare(registrations, preparationOptions);
28
32
  for (const registration of registrations) {
29
33
  for (const field of Object.values(registration.gqltype.getFields())) {
30
34
  const relation = field.extensions?.relation;
@@ -45,31 +49,34 @@ export const createMongoAdapter = () => {
45
49
  }
46
50
  }
47
51
  },
48
- createModel(gqltype, onModelCreated, options) {
52
+ createModel(gqltype, onModelCreated, modelOptions) {
49
53
  return createMongoModel(gqltype, (model) => {
50
54
  for (const fieldName of privateConnectionFields.get(gqltype.name) || []) {
51
55
  model.schema.add({ [fieldName]: mongoose.Schema.Types.ObjectId });
52
56
  model.schema.index({ [fieldName]: 1 });
53
57
  }
54
58
  if (onModelCreated) onModelCreated(model);
55
- }, options);
59
+ }, integrity.enabled ? { ...modelOptions, createCollection: false } : modelOptions);
56
60
  },
57
61
  castId(value) {
58
62
  return new mongoose.Types.ObjectId(value);
59
63
  },
60
- withTransaction: withMongoTransaction,
64
+ initialize: integrity.initialize,
65
+ withTransaction: integrity.enabled ? integrity.withTransaction : withMongoTransaction,
61
66
  newRecord(Model, data, session) {
62
67
  const record = new Model(data);
63
68
  record.$session(session);
64
69
  return record;
65
70
  },
66
- saveRecord(Model, record) {
71
+ saveRecord(Model, record, session) {
72
+ if (integrity.enabled) return integrity.saveRecord(Model, record, session === undefined ? record.$session() : session);
67
73
  return record.save();
68
74
  },
69
75
  toObject(record) {
70
76
  return typeof record?.toObject === 'function' ? record.toObject() : record;
71
77
  },
72
78
  getById(Model, id, session, { projection, plain, requiredId } = {}) {
79
+ integrity.assertReady();
73
80
  let query = withSession(requiredId == null ? Model.findById(id, projection)
74
81
  : Model.findOne({ $and: [{ _id: requiredId }, { _id: id }] }, projection), session);
75
82
  if (plain) query = query.lean();
@@ -79,27 +86,36 @@ export const createMongoAdapter = () => {
79
86
  return Object.keys(unset).length > 0 ? { ...set, $unset: unset } : set;
80
87
  },
81
88
  update(Model, id, update, session) {
89
+ if (integrity.enabled) return integrity.updateRecord(Model, id, update, session);
82
90
  return withSession(Model.findByIdAndUpdate(id, update, { new: true }), session);
83
91
  },
84
92
  delete(Model, id, session) {
93
+ if (integrity.enabled) {
94
+ integrity.assertWrite(Model, session);
95
+ return integrity.deleteRecord(Model, id, session);
96
+ }
85
97
  return withSession(Model.findByIdAndDelete(id), session);
86
98
  },
87
99
  async find(Model, gqltype, args, session, { requiredId } = {}) {
100
+ integrity.assertReady();
88
101
  const pipeline = await queries.buildQuery(args, gqltype);
89
102
  if (requiredId != null) pipeline.unshift({ $match: { _id: Model.schema.path('_id').cast(requiredId) } });
90
103
  if (pipeline.length === 0) return withSession(Model.find({}), session);
91
104
  return withSession(Model.aggregate(pipeline), session);
92
105
  },
93
106
  async count(Model, gqltype, args, session) {
107
+ integrity.assertReady();
94
108
  const pipeline = await queries.buildQuery(args, gqltype, true);
95
109
  const result = await withSession(Model.aggregate(pipeline), session);
96
110
  return result[0] ? result[0].size : 0;
97
111
  },
98
112
  async aggregate(Model, gqltype, args, session) {
113
+ integrity.assertReady();
99
114
  const pipeline = await queries.buildAggregationQuery(args, gqltype, args.aggregation);
100
115
  return withSession(Model.aggregate(pipeline), session);
101
116
  },
102
117
  async findChildren(Model, gqltype, connectionField, parentId, args, session) {
118
+ integrity.assertReady();
103
119
  const pipeline = await queries.buildQuery(args, gqltype);
104
120
  const path = Model.schema.path(connectionField);
105
121
  pipeline.unshift({ $match: { [connectionField]: path ? path.cast(parentId) : parentId } });
@@ -107,5 +123,6 @@ export const createMongoAdapter = () => {
107
123
  },
108
124
  };
109
125
 
126
+ Object.defineProperty(adapter, 'referentialIntegrity', { value: integrity.mode, enumerable: true });
110
127
  return adapter;
111
128
  };
@@ -0,0 +1,268 @@
1
+ import mongoose from 'mongoose';
2
+ import { describeModels, SimfinityError } from '@simtlix/simfinity-core';
3
+ import { withMongoTransaction } from './transactions.js';
4
+
5
+ const LOCK_FIELD = '_simfinityReferenceLock';
6
+ const transactionOptions = { readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' } };
7
+ const violation = () => new SimfinityError('Reference constraint violated', 'REFERENCE_CONSTRAINT_VIOLATION', 409);
8
+ const invalid = (message) => new SimfinityError(message, 'INVALID_MONGO_INTEGRITY_CONFIGURATION', 400);
9
+
10
+ const referencePaths = (fields, prefix = []) => fields.flatMap((field) => {
11
+ const path = [...prefix, ...field.storageName.split('.')];
12
+ if (path.includes(LOCK_FIELD)) throw invalid(`${LOCK_FIELD} is reserved for reference integrity`);
13
+ if (field.kind === 'reference') return [{ path, target: field.target }];
14
+ if (field.kind === 'embedded') return referencePaths(field.fields, path);
15
+ return [];
16
+ });
17
+
18
+ const valuesAt = (value, path) => {
19
+ if (value == null) return [];
20
+ if (Array.isArray(value)) return value.flatMap((item) => valuesAt(item, path));
21
+ if (path.length === 0) return [value];
22
+ return valuesAt(value[path[0]], path.slice(1));
23
+ };
24
+
25
+ export const createMongoIntegrity = (options = {}) => {
26
+ if (!options || typeof options !== 'object' || Array.isArray(options)
27
+ || Object.keys(options).some((key) => key !== 'referentialIntegrity')) {
28
+ throw invalid('Expected Mongo adapter options with referentialIntegrity');
29
+ }
30
+ const mode = options.referentialIntegrity === undefined ? 'off' : options.referentialIntegrity;
31
+ if (!['off', 'transactional'].includes(mode)) {
32
+ throw invalid('referentialIntegrity must be off or transactional');
33
+ }
34
+ const enabled = mode === 'transactional';
35
+ let binding;
36
+ let registrations;
37
+ let entities;
38
+ let createCollection;
39
+ let initialization;
40
+ let ready = false;
41
+ const models = new Map();
42
+ const outgoing = new Map();
43
+ const incoming = new Map();
44
+
45
+ const assertRegistry = () => {
46
+ const current = binding.getRegistrations();
47
+ if (current.length !== registrations.length
48
+ || current.some((entry, index) => entry.gqltype !== registrations[index].gqltype)) {
49
+ throw invalid('Register all types before creating the protected schema');
50
+ }
51
+ };
52
+
53
+ const assertReady = () => {
54
+ if (!enabled) return;
55
+ if (!ready) {
56
+ throw new SimfinityError('Await Mongo adapter.initialize() before serving operations', 'MONGO_INTEGRITY_NOT_INITIALIZED', 503);
57
+ }
58
+ assertRegistry();
59
+ };
60
+
61
+ const assertSession = (session) => {
62
+ if (!session?.inTransaction()) {
63
+ throw new SimfinityError('Reference integrity requires an active transaction', 'ACTIVE_TRANSACTION_REQUIRED', 400);
64
+ }
65
+ if (session.transaction.options.readConcern?.level !== 'snapshot'
66
+ || session.transaction.options.writeConcern?.w !== 'majority') {
67
+ throw new SimfinityError('Reference integrity requires snapshot read concern and majority write concern', 'MONGO_INTEGRITY_TRANSACTION_OPTIONS', 400);
68
+ }
69
+ };
70
+
71
+ const assertWrite = (Model, session) => {
72
+ assertReady();
73
+ if (![...models.values()].includes(Model)) throw invalid('Model is outside the protected registry');
74
+ assertSession(session);
75
+ if (session.client !== Model.db.getClient()) {
76
+ throw invalid('The transaction and protected models must use the same MongoDB client');
77
+ }
78
+ };
79
+
80
+ const modelReferences = (Model, record) => {
81
+ const references = new Map();
82
+ for (const { path, target } of outgoing.get(Model) || []) {
83
+ for (const id of valuesAt(record, path)) {
84
+ // Strings in native storage do not join an ObjectId key, even if castable.
85
+ if (id?._bsontype !== 'ObjectId') throw violation();
86
+ references.set(`${target}:${id.toHexString()}`, { target, id });
87
+ }
88
+ }
89
+ return [...references.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([, value]) => value);
90
+ };
91
+
92
+ const lock = async (Model, id, session) => {
93
+ const result = await Model.collection.updateOne(
94
+ { _id: id }, { $set: { [LOCK_FIELD]: new mongoose.Types.ObjectId() } }, { session },
95
+ );
96
+ return result.matchedCount > 0;
97
+ };
98
+
99
+ const abortOnWriteError = async (session, body) => {
100
+ try {
101
+ return await body();
102
+ } catch (error) {
103
+ if (session.inTransaction()) {
104
+ // Mongoose post hooks can reject after persisting an unchecked write.
105
+ // A caller must not catch that error and commit a borrowed transaction.
106
+ try { await session.abortTransaction(); } catch { /* Preserve the write error and retry labels. */ }
107
+ }
108
+ throw error;
109
+ }
110
+ };
111
+
112
+ const validateRecord = async (Model, record, session) => abortOnWriteError(session, async () => {
113
+ // Read the stored post-image, including setters/defaults and save middleware changes.
114
+ if (!record?._id) throw violation();
115
+ const stored = await Model.collection.findOne({ _id: record._id }, { session });
116
+ if (!stored) throw violation();
117
+ for (const { target, id } of modelReferences(Model, stored)) {
118
+ if (!await lock(models.get(target), id, session)) throw violation();
119
+ }
120
+ });
121
+
122
+ const initialize = async () => {
123
+ if (!enabled) return;
124
+ if (!entities) throw invalid('Create the GraphQL schema before initializing reference integrity');
125
+ assertRegistry();
126
+ const collections = new Set();
127
+ let connection;
128
+ for (const entity of entities) {
129
+ const Model = binding.getModel(entity.gqltype);
130
+ if (!Model?.collection || !Model?.db) throw invalid(`No MongoDB model for ${entity.name}`);
131
+ if (Model.schema.path('_id')?.instance !== 'ObjectId') throw invalid(`Protected model ${entity.name} must use ObjectId primary keys`);
132
+ if (connection && connection !== Model.db) throw invalid('All protected models must share one MongoDB connection');
133
+ connection = Model.db;
134
+ const collection = Model.collection.collectionName;
135
+ if (collections.has(collection)) throw invalid(`Multiple protected models share collection ${collection}`);
136
+ collections.add(collection);
137
+ const reserved = Model.schema.path(LOCK_FIELD);
138
+ if (reserved && !reserved.options.simfinityReferenceLock) throw invalid(`${LOCK_FIELD} is reserved for reference integrity`);
139
+ if (!reserved) Model.schema.add({
140
+ [LOCK_FIELD]: { type: mongoose.Schema.Types.ObjectId, select: false, simfinityReferenceLock: true },
141
+ });
142
+ for (const { path } of referencePaths(entity.fields)) {
143
+ if (Model.schema.path(path.join('.'))?.instance !== 'ObjectId') {
144
+ throw invalid(`Expected an ObjectId storage path for ${entity.name}.${path.join('.')}`);
145
+ }
146
+ }
147
+ models.set(entity.name, Model);
148
+ outgoing.set(Model, referencePaths(entity.fields));
149
+ }
150
+ if (!connection || connection.readyState !== 1) throw invalid('Connect MongoDB before initializing reference integrity');
151
+ for (const [Model, paths] of outgoing) {
152
+ for (const reference of paths) {
153
+ const target = models.get(reference.target);
154
+ if (!incoming.has(target)) incoming.set(target, []);
155
+ incoming.get(target).push({ Model, path: reference.path.join('.') });
156
+ }
157
+ }
158
+ for (const Model of models.values()) {
159
+ if (createCollection) await Model.createCollection();
160
+ else if (!(await connection.db.listCollections({ name: Model.collection.collectionName }).toArray()).length) {
161
+ throw invalid(`Collection ${Model.collection.collectionName} must exist before initialization`);
162
+ }
163
+ // Model compilation may already have scheduled index builds. Finish them
164
+ // before the audit starts, so its transaction does not race our own DDL.
165
+ await Model.init();
166
+ }
167
+ const session = await connection.startSession();
168
+ try {
169
+ session.startTransaction({ readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' } });
170
+ for (const Model of models.values()) {
171
+ const cursor = Model.collection.find({}, { session });
172
+ try {
173
+ for await (const record of cursor) {
174
+ if (record[LOCK_FIELD] != null && record[LOCK_FIELD]?._bsontype !== 'ObjectId') {
175
+ throw invalid(`Existing ${LOCK_FIELD} data is incompatible with reference integrity`);
176
+ }
177
+ for (const { target, id } of modelReferences(Model, record)) {
178
+ if (!await models.get(target).collection.findOne({ _id: id }, { session, projection: { _id: 1 } })) {
179
+ throw violation();
180
+ }
181
+ }
182
+ }
183
+ } finally { await cursor.close(); }
184
+ }
185
+ // Read-only startup audit. Mutation attempts use independently owned or supplied sessions.
186
+ await session.abortTransaction();
187
+ } catch (error) {
188
+ if (session.inTransaction()) await session.abortTransaction().catch(() => {});
189
+ if (error.code === 20) {
190
+ throw new SimfinityError('Transactional reference integrity requires a MongoDB replica set or sharded cluster', 'MONGO_TRANSACTIONS_REQUIRED', 503);
191
+ }
192
+ throw error;
193
+ } finally { await session.endSession(); }
194
+ ready = true;
195
+ };
196
+
197
+ return {
198
+ mode,
199
+ enabled,
200
+ bind(value) {
201
+ if (!enabled) return;
202
+ if (binding) throw invalid('A protected Mongo adapter can only bind to one runtime');
203
+ binding = value;
204
+ },
205
+ prepare(values, options) {
206
+ if (!enabled) return;
207
+ if (entities) {
208
+ assertRegistry();
209
+ return;
210
+ }
211
+ registrations = values.map(({ gqltype }) => ({ gqltype }));
212
+ entities = describeModels(values).entities;
213
+ for (const entity of entities) referencePaths(entity.fields);
214
+ createCollection = options?.createCollection !== false;
215
+ },
216
+ assertReady,
217
+ assertWrite,
218
+ initialize() {
219
+ if (!initialization) initialization = initialize();
220
+ return initialization;
221
+ },
222
+ withTransaction(session, body, Model) {
223
+ assertReady();
224
+ if (session) assertSession(session);
225
+ return withMongoTransaction(session, body, Model || models.values().next().value, transactionOptions);
226
+ },
227
+ async saveRecord(Model, record, session) {
228
+ assertWrite(Model, session);
229
+ return abortOnWriteError(session, async () => {
230
+ const saved = await record.save({ session });
231
+ await validateRecord(Model, saved, session);
232
+ return saved;
233
+ });
234
+ },
235
+ async updateRecord(Model, id, update, session) {
236
+ assertWrite(Model, session);
237
+ return abortOnWriteError(session, async () => {
238
+ const objectId = Model.schema.path('_id').cast(id);
239
+ const updated = await Model.findByIdAndUpdate(objectId, update, { new: true }).session(session);
240
+ if (!updated) {
241
+ // A middleware can hide a completed write with a null result. Abort
242
+ // even on a genuine no-match; core still reports its normal NOT_VALID_ID.
243
+ await session.abortTransaction();
244
+ return null;
245
+ }
246
+ if (!updated._id?.equals(objectId)) throw violation();
247
+ await validateRecord(Model, updated, session);
248
+ return updated;
249
+ });
250
+ },
251
+ async deleteRecord(Model, id, session) {
252
+ return abortOnWriteError(session, async () => {
253
+ const objectId = Model.schema.path('_id').cast(id);
254
+ if (!await lock(Model, objectId, session)) return null;
255
+ for (const reference of incoming.get(Model) || []) {
256
+ const query = reference.Model === Model
257
+ ? { $and: [{ [reference.path]: objectId }, { _id: { $ne: objectId } }] }
258
+ : { [reference.path]: objectId };
259
+ if (await reference.Model.collection.findOne(query, { session, projection: { _id: 1 } })) throw violation();
260
+ }
261
+ const deleted = await Model.findByIdAndDelete(objectId).session(session);
262
+ if (!deleted?._id?.equals(objectId)
263
+ || await Model.collection.findOne({ _id: objectId }, { session, projection: { _id: 1 } })) throw violation();
264
+ return deleted;
265
+ });
266
+ },
267
+ };
268
+ };
@@ -29,7 +29,7 @@ const endOwnedSession = async (session, failed) => {
29
29
  }
30
30
  };
31
31
 
32
- export const withMongoTransaction = async (session, body, model) => {
32
+ export const withMongoTransaction = async (session, body, model, transactionOptions) => {
33
33
  const connection = model?.db || mongoose.connection;
34
34
  if (session) {
35
35
  if (!session.inTransaction()) {
@@ -46,7 +46,8 @@ export const withMongoTransaction = async (session, body, model) => {
46
46
  let failed = false;
47
47
  try {
48
48
  for (let attempt = 0; attempt <= MAX_TRANSIENT_RETRIES; attempt++) {
49
- await mySession.startTransaction();
49
+ if (transactionOptions) await mySession.startTransaction(transactionOptions);
50
+ else await mySession.startTransaction();
50
51
  try {
51
52
  const result = await body(mySession);
52
53
  await commitWithRetry(mySession);
package/types/index.d.ts CHANGED
@@ -41,7 +41,16 @@ export type {
41
41
  } from '@simtlix/simfinity-core';
42
42
  export * from '@simtlix/simfinity-mcp';
43
43
  export { default as mcp } from '@simtlix/simfinity-mcp';
44
- export function createMongoAdapter(): DatabaseAdapter;
44
+ export interface MongoAdapterOptions {
45
+ /** Fixed at adapter creation. Defaults to off; transactional mode requires initialize(). */
46
+ referentialIntegrity?: 'off' | 'transactional';
47
+ }
48
+ export interface MongoAdapter extends DatabaseAdapter {
49
+ readonly referentialIntegrity: 'off' | 'transactional';
50
+ /** After connecting MongoDB and createSchema(), await the transaction capability and existing-reference audit. */
51
+ initialize(): Promise<void>;
52
+ }
53
+ export function createMongoAdapter(options?: MongoAdapterOptions): MongoAdapter;
45
54
  export function getRegistrations(): RuntimeRegistration[];
46
55
 
47
56
  /* ========================================================================== *
@@ -146,6 +155,8 @@ export function getInputType(type: GraphQLObjectType | { name: string }): GraphQ
146
155
  * Without a session, owns a transaction on the model's connection and awaits cleanup.
147
156
  * A supplied session must have an active transaction and belong to the model's MongoDB client.
148
157
  * The caller then owns retries, commit, abort, and cleanup; inactive sessions are rejected.
158
+ * Exception: transactional reference integrity aborts on a violation, guarded-write error or ambiguous update result.
159
+ * That mode requires snapshot read concern and majority write concern on supplied transactions.
149
160
  * Owned transactions retry transient failures and uncertain commits separately, up to five times each.
150
161
  */
151
162