@toa.io/storages.mongodb 1.0.0-alpha.331 → 1.0.0-alpha.332

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/CHANGELOG.md CHANGED
@@ -3,6 +3,13 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [1.0.0-alpha.332](https://github.com/toa-io/toa/compare/v1.0.0-alpha.331...v1.0.0-alpha.332) (2026-10-23)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **storages.mongodb:** commit a set where the URL sets timeoutMS ([0b51f02](https://github.com/toa-io/toa/commit/0b51f0281de218dbcb677b75d6dd58685c4e2958))
11
+
12
+
6
13
  # [1.0.0-alpha.331](https://github.com/toa-io/toa/compare/v1.0.0-alpha.330...v1.0.0-alpha.331) (2026-10-16)
7
14
 
8
15
  **Note:** Version bump only for package @toa.io/storages.mongodb
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toa.io/storages.mongodb",
3
- "version": "1.0.0-alpha.331",
3
+ "version": "1.0.0-alpha.332",
4
4
  "type": "module",
5
5
  "description": "Toa MongoDB Storage Connector",
6
6
  "author": "temich <tema.gurtovoy@gmail.com>",
@@ -20,13 +20,13 @@
20
20
  "test": "echo \"Error: run tests from root\" && exit 1"
21
21
  },
22
22
  "dependencies": {
23
- "@toa.io/conveyor": "1.0.0-alpha.330",
24
- "@toa.io/core": "1.0.0-alpha.330",
25
- "@toa.io/definitions": "1.0.0-alpha.331",
26
- "@toa.io/generic": "1.0.0-alpha.330",
27
- "@toa.io/pointer": "1.0.0-alpha.330",
23
+ "@toa.io/conveyor": "1.0.0-alpha.332",
24
+ "@toa.io/core": "1.0.0-alpha.332",
25
+ "@toa.io/definitions": "1.0.0-alpha.332",
26
+ "@toa.io/generic": "1.0.0-alpha.332",
27
+ "@toa.io/pointer": "1.0.0-alpha.332",
28
28
  "mongodb": "7.7.0",
29
29
  "openspan": "1.0.0-alpha.330"
30
30
  },
31
- "gitHead": "e7020d160910ca4f0c2ff9040b3cbb4fca8ea41f"
31
+ "gitHead": "37dbdefd96c01df050ea0d46a647da7f2f9f93f2"
32
32
  }
package/readme.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # MongoDB Storage
2
2
 
3
+ ## Server
4
+
5
+ MongoDB 8.0 or later. An earlier server refuses a `transition` of scope `entries`
6
+ ([Operations](/documentation/component/declaration.md#operations)).
7
+
3
8
  ## Tracing
4
9
 
5
10
  Commands are recorded as `client` spans within the trace of the current invocation.
package/src/outbox.js CHANGED
@@ -27,7 +27,13 @@ export class Outbox {
27
27
  async insertMany(rows, session) {
28
28
  if (rows.length === 0) return
29
29
 
30
- await this.#collection.insertMany(rows.map(to), { session })
30
+ const namespace = this.#collection.namespace
31
+
32
+ // the client's bulk write, not `insertMany`: see `massStore` in ./storage.js
33
+ await this.#collection.client.bulkWrite(
34
+ rows.map((row) => ({ name: 'insertOne', namespace, document: to(row) })),
35
+ { session }
36
+ )
31
37
  }
32
38
 
33
39
  /**
package/src/storage.js CHANGED
@@ -287,33 +287,35 @@ export class Storage extends Connector {
287
287
  async massStore(entities, rows = undefined, attempt = 0) {
288
288
  if (entities.length === 0) return true
289
289
 
290
+ const namespace = this.#collection.namespace
291
+
290
292
  const operations = entities.map((entity) => {
291
293
  const record = this.#to(entity)
292
294
 
293
295
  if (entity.VERSION === 1) {
294
296
  const { VERSION, ...rest } = record
295
297
 
298
+ // one the read did not find: absent, and inserted, or deleted, and revived — a live
299
+ // one makes the upsert collide on `_id`, which is the swap lost
296
300
  return {
297
- // one the read did not find: absent, and inserted, or deleted, and revived — a live
298
- // one makes the upsert collide on `_id`, which is the swap lost
299
- updateOne: {
300
- filter: { _id: entity.id, DELETED: { $ne: null } },
301
- update: {
302
- $set: {
303
- ...rest,
304
- DELETED: null
305
- },
306
- $inc: { VERSION: 1 }
301
+ name: 'updateOne',
302
+ namespace,
303
+ filter: { _id: entity.id, DELETED: { $ne: null } },
304
+ update: {
305
+ $set: {
306
+ ...rest,
307
+ DELETED: null
307
308
  },
308
- upsert: true
309
- }
309
+ $inc: { VERSION: 1 }
310
+ },
311
+ upsert: true
310
312
  }
311
313
  } else
312
314
  return {
313
- replaceOne: {
314
- filter: { _id: entity.id, VERSION: entity.VERSION - 1 },
315
- replacement: record
316
- }
315
+ name: 'replaceOne',
316
+ namespace,
317
+ filter: { _id: entity.id, VERSION: entity.VERSION - 1 },
318
+ replacement: record
317
319
  }
318
320
  })
319
321
 
@@ -327,10 +329,13 @@ export class Storage extends Connector {
327
329
  result = await this.command(
328
330
  'bulkWrite',
329
331
  { operations: operations.length },
330
- async () => await this.#collection.bulkWrite(operations, { session })
332
+ // the client's bulk write, not the collection's: the latter takes a `timeoutMS` the
333
+ // URL sets for one the call passed, and refuses it inside a transaction that has
334
+ // its own
335
+ async () => await this.#collection.client.bulkWrite(operations, { session })
331
336
  )
332
337
  } catch (error) {
333
- if (!collidedOnId(error)) throw error
338
+ if (!collidedOnId(firstWriteError(error))) throw error
334
339
 
335
340
  await session.abortTransaction()
336
341
 
@@ -353,7 +358,7 @@ export class Storage extends Connector {
353
358
  } catch (error) {
354
359
  console.error('MongoDB error', error)
355
360
 
356
- const retry = await retriable(error, attempt)
361
+ const retry = await retriable(firstWriteError(error), attempt)
357
362
 
358
363
  if (retry) return await this.massStore(entities, rows, attempt + 1)
359
364
  else return false
@@ -608,6 +613,13 @@ const ERR_DUPLICATE_KEY = 11000
608
613
  /** what the transaction answers where the call it carries has already been made */
609
614
  const MADE = Symbol('made')
610
615
 
616
+ /** What an ordered bulk write stopped at, or the error itself where no write failed. */
617
+ function firstWriteError(error) {
618
+ return error?.writeErrors instanceof Map && error.writeErrors.size > 0
619
+ ? error.writeErrors.values().next().value
620
+ : error
621
+ }
622
+
611
623
  /** whether the key a write collided on is `_id` */
612
624
  function collidedOnId(error) {
613
625
  if (error?.code !== ERR_DUPLICATE_KEY) return false
@@ -12,6 +12,8 @@ let storage
12
12
  beforeEach(async () => {
13
13
  collection = {
14
14
  collectionName: 'test',
15
+ namespace: 'db.test',
16
+ client: {},
15
17
  findOne: mock.fn(async () => null),
16
18
  find: mock.fn(() => cursor()),
17
19
  updateMany: mock.fn(async () => ({ modifiedCount: 0 })),
@@ -270,10 +272,10 @@ describe('massStore', () => {
270
272
  const stored = { id: 'a1', VERSION: 3, foo: 1 }
271
273
  const created = { id: 'b2', VERSION: 1, foo: 1 }
272
274
 
273
- const operations = () => collection.bulkWrite.mock.calls[0].arguments[0]
275
+ const operations = () => collection.client.bulkWrite.mock.calls[0].arguments[0]
274
276
 
275
277
  beforeEach(() => {
276
- collection.bulkWrite = mock.fn(async (operations) => ({
278
+ collection.client.bulkWrite = mock.fn(async (operations) => ({
277
279
  matchedCount: operations.length,
278
280
  upsertedCount: 0
279
281
  }))
@@ -282,14 +284,19 @@ describe('massStore', () => {
282
284
  it('should replace a record only at the version it was read at', async () => {
283
285
  await storage.massStore([stored])
284
286
 
285
- assert.deepEqual(operations()[0].replaceOne.filter, { _id: 'a1', VERSION: 2 })
287
+ const { name, namespace, filter } = operations()[0]
288
+
289
+ assert.equal(name, 'replaceOne')
290
+ assert.equal(namespace, 'db.test')
291
+ assert.deepEqual(filter, { _id: 'a1', VERSION: 2 })
286
292
  })
287
293
 
288
294
  it('should create a record only where none but a deleted one holds its id', async () => {
289
295
  await storage.massStore([created])
290
296
 
291
- const { filter, upsert } = operations()[0].updateOne
297
+ const { name, filter, upsert } = operations()[0]
292
298
 
299
+ assert.equal(name, 'updateOne')
293
300
  assert.deepEqual(filter, { _id: 'b2', DELETED: { $ne: null } })
294
301
  assert.equal(upsert, true)
295
302
  })
@@ -300,20 +307,48 @@ describe('massStore', () => {
300
307
  })
301
308
 
302
309
  it('should abort and answer false where a record was not matched', async () => {
303
- collection.bulkWrite = mock.fn(async () => ({ matchedCount: 1, upsertedCount: 0 }))
310
+ collection.client.bulkWrite = mock.fn(async () => ({
311
+ matchedCount: 1,
312
+ upsertedCount: 0
313
+ }))
304
314
 
305
315
  assert.equal(await storage.massStore([stored, { ...stored, id: 'c3' }]), false)
306
316
  assert.equal(session.abortTransaction.mock.callCount(), 1)
307
317
  })
308
318
 
309
319
  it('should abort and answer false where a record being created exists', async () => {
310
- collection.bulkWrite = mock.fn(async () => {
311
- throw Object.assign(new Error('E11000 duplicate key error index: _id_ dup key'), {
312
- code: 11000
320
+ collection.client.bulkWrite = mock.fn(async () => {
321
+ const collision = Object.assign(
322
+ new Error('E11000 duplicate key error index: _id_ dup key'),
323
+ {
324
+ code: 11000
325
+ }
326
+ )
327
+
328
+ throw Object.assign(new Error('ordered bulk write encountered a write error'), {
329
+ writeErrors: new Map([[0, collision]])
313
330
  })
314
331
  })
315
332
 
316
333
  assert.equal(await storage.massStore([created]), false)
317
334
  assert.equal(session.abortTransaction.mock.callCount(), 1)
318
335
  })
336
+
337
+ it('should throw a duplicate where a record collides on a key other than its id', async () => {
338
+ collection.client.bulkWrite = mock.fn(async () => {
339
+ const collision = Object.assign(
340
+ new Error('E11000 duplicate key error index: foo_1'),
341
+ {
342
+ code: 11000,
343
+ keyPattern: { foo: 1 }
344
+ }
345
+ )
346
+
347
+ throw Object.assign(new Error('ordered bulk write encountered a write error'), {
348
+ writeErrors: new Map([[0, collision]])
349
+ })
350
+ })
351
+
352
+ await assert.rejects(storage.massStore([created]), { code: 306 })
353
+ })
319
354
  })