@toa.io/storages.mongodb 1.0.0-alpha.32 → 1.0.0-alpha.320

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 ADDED
@@ -0,0 +1,282 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
+
6
+ # [1.0.0-alpha.320](https://github.com/toa-io/toa/compare/v1.0.0-alpha.319...v1.0.0-alpha.320) (2026-09-28)
7
+
8
+ ### Features
9
+
10
+ * page a stream by CREATED, and stop it after its first page ([038dc52](https://github.com/toa-io/toa/commit/038dc52f2db1a4dd58c6ec58a00de8914b3d38a3))
11
+
12
+
13
+ # [1.0.0-alpha.319](https://github.com/toa-io/toa/compare/v1.0.0-alpha.318...v1.0.0-alpha.319) (2026-09-27)
14
+
15
+ ### Features
16
+
17
+ * **storages.mongodb:** read a set and its changes from a token ([d0a8955](https://github.com/toa-io/toa/commit/d0a8955683b7566e704f7e9cde69f67272c7be1b))
18
+
19
+
20
+ # [1.0.0-alpha.318](https://github.com/toa-io/toa/compare/v1.0.0-alpha.317...v1.0.0-alpha.318) (2026-09-23)
21
+
22
+ **Note:** Version bump only for package @toa.io/storages.mongodb
23
+
24
+
25
+
26
+
27
+
28
+ # [1.0.0-alpha.317](https://github.com/toa-io/toa/compare/v1.0.0-alpha.316...v1.0.0-alpha.317) (2026-09-23)
29
+
30
+ **Note:** Version bump only for package @toa.io/storages.mongodb
31
+
32
+
33
+
34
+
35
+
36
+ # [1.0.0-alpha.316](https://github.com/toa-io/toa/compare/v1.0.0-alpha.315...v1.0.0-alpha.316) (2026-09-23)
37
+
38
+ **Note:** Version bump only for package @toa.io/storages.mongodb
39
+
40
+
41
+
42
+
43
+
44
+ # [1.0.0-alpha.315](https://github.com/toa-io/toa/compare/v1.0.0-alpha.314...v1.0.0-alpha.315) (2026-09-23)
45
+
46
+ **Note:** Version bump only for package @toa.io/storages.mongodb
47
+
48
+
49
+
50
+
51
+
52
+ # [1.0.0-alpha.314](https://github.com/toa-io/toa/compare/v1.0.0-alpha.313...v1.0.0-alpha.314) (2026-09-19)
53
+
54
+ * feat(core)!: let an operation state how long its calls are remembered ([af177c2](https://github.com/toa-io/toa/commit/af177c2ab3d00d475840b5ef9e612f04e82b72bd))
55
+
56
+ ### BREAKING CHANGES
57
+
58
+ * `inbox.retention` under 600 is refused. See
59
+ migrations/313.md.
60
+
61
+
62
+ # [1.0.0-alpha.313](https://github.com/toa-io/toa/compare/v1.0.0-alpha.312...v1.0.0-alpha.313) (2026-09-18)
63
+
64
+ **Note:** Version bump only for package @toa.io/storages.mongodb
65
+
66
+
67
+
68
+
69
+
70
+ # [1.0.0-alpha.312](https://github.com/toa-io/toa/compare/v1.0.0-alpha.311...v1.0.0-alpha.312) (2026-09-18)
71
+
72
+ **Note:** Version bump only for package @toa.io/storages.mongodb
73
+
74
+
75
+
76
+
77
+
78
+ # [1.0.0-alpha.311](https://github.com/toa-io/toa/compare/v1.0.0-alpha.310...v1.0.0-alpha.311) (2026-09-17)
79
+
80
+ ### Features
81
+
82
+ * name the database after the scope ([4d7203f](https://github.com/toa-io/toa/commit/4d7203fd18828895272a9dae76e351d92b49277f))
83
+
84
+
85
+ # [1.0.0-alpha.310](https://github.com/toa-io/toa/compare/v1.0.0-alpha.309...v1.0.0-alpha.310) (2026-09-16)
86
+
87
+ **Note:** Version bump only for package @toa.io/storages.mongodb
88
+
89
+
90
+
91
+
92
+
93
+ # [1.0.0-alpha.309](https://github.com/toa-io/toa/compare/v1.0.0-alpha.308...v1.0.0-alpha.309) (2026-09-16)
94
+
95
+ **Note:** Version bump only for package @toa.io/storages.mongodb
96
+
97
+
98
+
99
+
100
+
101
+ # [1.0.0-alpha.308](https://github.com/toa-io/toa/compare/v1.0.0-alpha.307...v1.0.0-alpha.308) (2026-09-15)
102
+
103
+ **Note:** Version bump only for package @toa.io/storages.mongodb
104
+
105
+
106
+
107
+
108
+
109
+ # [1.0.0-alpha.307](https://github.com/toa-io/toa/compare/v1.0.0-alpha.306...v1.0.0-alpha.307) (2026-09-15)
110
+
111
+ **Note:** Version bump only for package @toa.io/storages.mongodb
112
+
113
+
114
+
115
+
116
+
117
+ # [1.0.0-alpha.306](https://github.com/toa-io/toa/compare/v1.0.0-alpha.305...v1.0.0-alpha.306) (2026-09-14)
118
+
119
+ **Note:** Version bump only for package @toa.io/storages.mongodb
120
+
121
+
122
+
123
+
124
+
125
+ # [1.0.0-alpha.305](https://github.com/toa-io/toa/compare/v1.0.0-alpha.304...v1.0.0-alpha.305) (2026-09-13)
126
+
127
+ **Note:** Version bump only for package @toa.io/storages.mongodb
128
+
129
+
130
+
131
+
132
+
133
+ # [1.0.0-alpha.304](https://github.com/toa-io/toa/compare/v1.0.0-alpha.303...v1.0.0-alpha.304) (2026-09-13)
134
+
135
+ **Note:** Version bump only for package @toa.io/storages.mongodb
136
+
137
+
138
+
139
+
140
+
141
+ # [1.0.0-alpha.303](https://github.com/toa-io/toa/compare/v1.0.0-alpha.302...v1.0.0-alpha.303) (2026-09-12)
142
+
143
+ ### Features
144
+
145
+ * **metrics:** measure storage, stash, blob storages and fetch ([252298b](https://github.com/toa-io/toa/commit/252298bb9f34421694da4ee89742d62b47c39dfb))
146
+
147
+ ### Performance Improvements
148
+
149
+ * **storages.mongodb:** rename a record's _id to id in place ([4aeb2a1](https://github.com/toa-io/toa/commit/4aeb2a1d61f7f2b0ddab203b5887152e8917ef7f))
150
+
151
+
152
+ # [1.0.0-alpha.302](https://github.com/toa-io/toa/compare/v1.0.0-alpha.301...v1.0.0-alpha.302) (2026-09-11)
153
+
154
+ **Note:** Version bump only for package @toa.io/storages.mongodb
155
+
156
+
157
+
158
+
159
+
160
+ # [1.0.0-alpha.301](https://github.com/toa-io/toa/compare/v1.0.0-alpha.300...v1.0.0-alpha.301) (2026-09-11)
161
+
162
+ **Note:** Version bump only for package @toa.io/storages.mongodb
163
+
164
+
165
+
166
+
167
+
168
+ # [1.0.0-alpha.300](https://github.com/toa-io/toa/compare/v1.0.0-alpha.299...v1.0.0-alpha.300) (2026-09-11)
169
+
170
+ * feat(core)!: an operation may ask to run once ([dca616c](https://github.com/toa-io/toa/commit/dca616cb380a7a389e277875d3bd5ca55a6e4d45))
171
+
172
+ ### Bug Fixes
173
+
174
+ * **extensions:** what a factory remembers does not outlive the tree it was made for ([dd36631](https://github.com/toa-io/toa/commit/dd3663160f500eadfb701bf840ef3741a2d075f0))
175
+
176
+ ### Features
177
+
178
+ * **core:** an assignment may ask to run once as well ([10fbcbd](https://github.com/toa-io/toa/commit/10fbcbd86a5e82960e65f62c09d0a465ae8d0353))
179
+
180
+ ### BREAKING CHANGES
181
+
182
+ * `Storage.store`, `.upsert` and `.ensure` take one more
183
+ argument, and `Storage` has two more members. A connector that ignores them
184
+ works exactly as it did; a component that asks it for `once` is refused at
185
+ boot. See migrations/299.md.
186
+
187
+
188
+ # [1.0.0-alpha.299](https://github.com/toa-io/toa/compare/v1.0.0-alpha.298...v1.0.0-alpha.299) (2026-09-10)
189
+
190
+ ### Features
191
+
192
+ * **core:** a record carries the region that wrote it ([13ed187](https://github.com/toa-io/toa/commit/13ed187604f06dec66feec2e26ccf9dae30bb048))
193
+ * **core:** an outbox row is published to destinations ([cfb77b4](https://github.com/toa-io/toa/commit/cfb77b4826ff0f9560769a0996981e143d6525af))
194
+
195
+
196
+ # [1.0.0-alpha.298](https://github.com/toa-io/toa/compare/v1.0.0-alpha.297...v1.0.0-alpha.298) (2026-09-08)
197
+
198
+ **Note:** Version bump only for package @toa.io/storages.mongodb
199
+
200
+
201
+
202
+
203
+
204
+ # [1.0.0-alpha.297](https://github.com/toa-io/toa/compare/v1.0.0-alpha.296...v1.0.0-alpha.297) (2026-09-08)
205
+
206
+ **Note:** Version bump only for package @toa.io/storages.mongodb
207
+
208
+
209
+
210
+
211
+
212
+ # [1.0.0-alpha.296](https://github.com/toa-io/toa/compare/v1.0.0-alpha.295...v1.0.0-alpha.296) (2026-09-08)
213
+
214
+ **Note:** Version bump only for package @toa.io/storages.mongodb
215
+
216
+
217
+
218
+
219
+
220
+ # [1.0.0-alpha.295](https://github.com/toa-io/toa/compare/v1.0.0-alpha.294...v1.0.0-alpha.295) (2026-09-07)
221
+
222
+ **Note:** Version bump only for package @toa.io/storages.mongodb
223
+
224
+
225
+
226
+
227
+
228
+ # [1.0.0-alpha.294](https://github.com/toa-io/toa/compare/v1.0.0-alpha.293...v1.0.0-alpha.294) (2026-09-07)
229
+
230
+ **Note:** Version bump only for package @toa.io/storages.mongodb
231
+
232
+
233
+
234
+
235
+
236
+ # [1.0.0-alpha.293](https://github.com/toa-io/toa/compare/v1.0.0-alpha.292...v1.0.0-alpha.293) (2026-09-07)
237
+
238
+ **Note:** Version bump only for package @toa.io/storages.mongodb
239
+
240
+
241
+
242
+
243
+
244
+ # [1.0.0-alpha.292](https://github.com/toa-io/toa/compare/v1.0.0-alpha.291...v1.0.0-alpha.292) (2026-09-07)
245
+
246
+ ### Features
247
+
248
+ * **definitions:** what a package declares is read from one package ([7052341](https://github.com/toa-io/toa/commit/70523411b5d9e2b204c999aa02365173f3bb518d))
249
+
250
+
251
+ # [1.0.0-alpha.291](https://github.com/toa-io/toa/compare/v1.0.0-alpha.290...v1.0.0-alpha.291) (2026-09-07)
252
+
253
+ **Note:** Version bump only for package @toa.io/storages.mongodb
254
+
255
+
256
+
257
+
258
+
259
+ # [1.0.0-alpha.289](https://github.com/toa-io/toa/compare/v1.0.0-alpha.288...v1.0.0-alpha.289) (2026-09-07)
260
+
261
+ * A deploy moves only what changed, and a component sees none of the runtime's environment (#1073) ([f38e3db](https://github.com/toa-io/toa/commit/f38e3db533f24db866c57bfa1ef294eff1eaf499)), closes [#1073](https://github.com/toa-io/toa/issues/1073) [#1064](https://github.com/toa-io/toa/issues/1064) [#1066](https://github.com/toa-io/toa/issues/1066) [#1067](https://github.com/toa-io/toa/issues/1067) [#1068](https://github.com/toa-io/toa/issues/1068) [#1069](https://github.com/toa-io/toa/issues/1069) [#1071](https://github.com/toa-io/toa/issues/1071) [#1070](https://github.com/toa-io/toa/issues/1070) [#1072](https://github.com/toa-io/toa/issues/1072)
262
+
263
+ ### BREAKING CHANGES
264
+
265
+ * a component that read `process.env.TOA_*` reads `context` instead;
266
+ `echo(input)` no longer substitutes from the environment; a bash operation sees no
267
+ `TOA_*`; images no longer set `USER node` — see migrations/289.md.
268
+
269
+
270
+ # [1.0.0-alpha.288](https://github.com/toa-io/toa/compare/v1.0.0-alpha.287...v1.0.0-alpha.288) (2026-09-06)
271
+
272
+ **Note:** Version bump only for package @toa.io/storages.mongodb
273
+
274
+
275
+
276
+
277
+
278
+ # [1.0.0-alpha.287](https://github.com/toa-io/toa/compare/v1.0.0-alpha.286...v1.0.0-alpha.287) (2026-09-06)
279
+
280
+ ### Features
281
+
282
+ * **mongodb:** a migration says what it is doing ([a917a81](https://github.com/toa-io/toa/commit/a917a81fdc94eb23fd73182c0edf891e6b2df843))
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@toa.io/storages.mongodb",
3
- "version": "1.0.0-alpha.32",
3
+ "version": "1.0.0-alpha.320",
4
+ "type": "module",
4
5
  "description": "Toa MongoDB Storage Connector",
5
6
  "author": "temich <tema.gurtovoy@gmail.com>",
6
7
  "homepage": "https://github.com/toa-io/toa#readme",
@@ -19,13 +20,13 @@
19
20
  "test": "echo \"Error: run tests from root\" && exit 1"
20
21
  },
21
22
  "dependencies": {
22
- "@toa.io/console": "1.0.0-alpha.32",
23
- "@toa.io/conveyor": "1.0.0-alpha.32",
24
- "@toa.io/core": "1.0.0-alpha.32",
25
- "@toa.io/generic": "1.0.0-alpha.32",
26
- "@toa.io/pointer": "1.0.0-alpha.32",
27
- "mongodb": "6.3.0",
28
- "saslprep": "1.0.3"
23
+ "@toa.io/conveyor": "1.0.0-alpha.311",
24
+ "@toa.io/core": "1.0.0-alpha.320",
25
+ "@toa.io/definitions": "1.0.0-alpha.320",
26
+ "@toa.io/generic": "1.0.0-alpha.311",
27
+ "@toa.io/pointer": "1.0.0-alpha.311",
28
+ "mongodb": "7.6.0",
29
+ "openspan": "1.0.0-alpha.305"
29
30
  },
30
- "gitHead": "93ceb53797363abb77eb9c550db77e906691d42a"
31
+ "gitHead": "5ef92ab939a596e4ac820965c59c015820eba7b2"
31
32
  }
package/readme.md ADDED
@@ -0,0 +1,38 @@
1
+ # MongoDB Storage
2
+
3
+ ## Tracing
4
+
5
+ Commands are recorded as `client` spans within the trace of the current invocation.
6
+
7
+ Spans are named `{command} {collection}` and carry `db.*` attributes following the
8
+ [OpenTelemetry semantic conventions](https://opentelemetry.io/docs/specs/semconv/database/mongodb/):
9
+ `db.system`, `db.namespace`, `db.collection.name`, `db.operation.name`.
10
+
11
+ Commands executed outside of a sampled trace context (e.g. index management on startup)
12
+ and internal driver commands (`hello`, `ping`, authentication) are not recorded.
13
+
14
+ Monitoring is client-side only and does not affect the MongoDB server. Span recording
15
+ adds no waiting to the query path: exporting is buffered and happens in the background.
16
+
17
+ ## Stream tokens
18
+
19
+ A [stream](/documentation/collections.md) ends with a token of changes where two things hold:
20
+
21
+ - **MongoDB runs as a replica set**, which it does wherever there is an outbox.
22
+ - **The collection keeps images** of what a record was before each change, which is how a change
23
+ that takes an entry out of the collection is told from a change outside it. They cost a copy of every
24
+ changed record for as long as the oplog holds it, so a collection keeps them only by a
25
+ [migration](/documentation/component/declaration.md#migrations):
26
+
27
+ ```yaml
28
+ # migrations/0003-images.yaml
29
+ - images: true
30
+ ```
31
+
32
+ `- images: false` stops keeping them, and a token issued before is then refused with `410`.
33
+
34
+ A token lasts as long as the oplog of the replica set holds the point it names — its window, which
35
+ the size of the oplog and the rate of writes decide.
36
+
37
+ A read from a token scans the oplog from the point it names: it costs what the whole replica set
38
+ wrote since, whatever of it concerns the collection.
package/src/client.js CHANGED
@@ -1,28 +1,67 @@
1
- 'use strict'
2
-
3
1
  /**
4
2
  * @typedef {import('mongodb').MongoClient} MongoClient
5
3
  * @typedef {{ count: number, client: MongoClient }} Instance
6
4
  * @typedef {import('@toa.io/core').Locator} Locator
7
5
  */
8
6
 
9
- const { Connector } = require('@toa.io/core')
10
- const { resolve } = require('@toa.io/pointer')
11
- const { ID } = require('./deployment')
12
- const { MongoClient } = require('mongodb')
7
+ import { console } from 'openspan'
8
+ import { environment } from '@toa.io/generic'
9
+ import { Connector } from '@toa.io/core'
10
+ import { resolve } from '@toa.io/pointer'
11
+ import { ID } from '@toa.io/definitions/storages.mongodb'
12
+ import { MongoClient } from 'mongodb'
13
13
 
14
14
  /**
15
15
  * @type {Record<string, Promise<Instance>>}
16
16
  */
17
17
  const INSTANCES = {}
18
18
 
19
- class Client extends Connector {
19
+ export class Client extends Connector {
20
+ name
21
+
20
22
  /**
21
23
  * @public
22
24
  * @type {import('mongodb').Collection}
23
25
  */
24
26
  collection
25
27
 
28
+ /**
29
+ * The outbox rows of this component, absent unless something consumes its events. Created
30
+ * eagerly beside the entity collection, because a transaction cannot create a collection and
31
+ * an index build cannot run inside one.
32
+ *
33
+ * @public
34
+ * @type {import('mongodb').Collection | undefined}
35
+ */
36
+ outbox
37
+
38
+ /**
39
+ * The calls this component has made good on, absent unless it declares `once` anywhere.
40
+ * Created eagerly beside the entity collection, for the reason the outbox's is.
41
+ *
42
+ * @public
43
+ * @type {import('mongodb').Collection | undefined}
44
+ */
45
+ inbox
46
+
47
+ /**
48
+ * Whether this deployment can run transactions at all. A standalone mongod cannot, and an
49
+ * outbox without atomicity is worse than none, so the storage falls back to inline emission.
50
+ *
51
+ * @public
52
+ * @type {boolean}
53
+ */
54
+ transactional = false
55
+
56
+ /**
57
+ * The database this component's collections live in, which is where the migration state
58
+ * is kept as well.
59
+ *
60
+ * @public
61
+ * @type {import('mongodb').Db}
62
+ */
63
+ db
64
+
26
65
  /**
27
66
  * @private
28
67
  * @type {Locator}
@@ -41,13 +80,30 @@ class Client extends Connector {
41
80
  */
42
81
  key
43
82
 
83
+ /**
84
+ * @private
85
+ * @type {boolean}
86
+ */
87
+ publishes
88
+
89
+ /**
90
+ * @private
91
+ * @type {boolean}
92
+ */
93
+ claims
94
+
44
95
  /**
45
96
  * @param {Locator} locator
97
+ * @param {boolean} [publishes] whether this component publishes anything
98
+ * @param {boolean} [claims] whether any of its operations declares `once`
46
99
  */
47
- constructor (locator) {
100
+ constructor(locator, publishes = false, claims = false) {
48
101
  super()
49
102
 
50
103
  this.locator = locator
104
+ this.name = locator.lowercase
105
+ this.publishes = publishes
106
+ this.claims = claims
51
107
  }
52
108
 
53
109
  /**
@@ -55,29 +111,66 @@ class Client extends Connector {
55
111
  * @override
56
112
  * @return {Promise<void>}
57
113
  */
58
- async open () {
114
+ async open() {
59
115
  const urls = await this.resolveURLs()
60
116
  const dbname = this.resolveDB()
61
- const collname = this.locator.lowercase
62
117
 
63
118
  this.key = getKey(dbname, urls)
64
119
 
65
- INSTANCES[this.key] ??= this.createInstance(urls)
120
+ try {
121
+ INSTANCES[this.key] ??= this.createInstance(urls)
122
+ } catch (error) {
123
+ console.error('Failed to connect to MongoDB', { urls, error })
124
+ }
66
125
 
67
126
  this.instance = await INSTANCES[this.key]
68
127
  this.instance.count++
69
128
 
70
129
  const db = this.instance.client.db(dbname)
71
130
 
72
- try {
73
- this.collection = await db.createCollection(collname)
74
- } catch (e) {
75
- if (e.code !== ALREADY_EXISTS) {
76
- throw e
77
- }
131
+ this.db = db
132
+ this.collection = await collection(db, this.name)
133
+ this.transactional = await transactional(db)
78
134
 
79
- this.collection = db.collection(collname)
135
+ /*
136
+ * The outbox may fall back and this may not: inline emission still delivers, where a call
137
+ * that is not recorded is a call that will be made twice, which is the opposite of what was
138
+ * asked for. So this refuses rather than warns.
139
+ */
140
+ if (this.claims) {
141
+ if (!this.transactional)
142
+ throw new Error(
143
+ `Component '${this.name}' declares 'once', which needs a MongoDB replica set ` +
144
+ 'or a sharded cluster to commit a call with the entity it changed'
145
+ )
146
+
147
+ this.inbox = await collection(db, this.name + INBOX)
80
148
  }
149
+
150
+ if (!this.publishes) return
151
+
152
+ if (this.transactional) this.outbox = await collection(db, this.name + OUTBOX)
153
+ else
154
+ console.warn(
155
+ 'MongoDB is not a replica set; events are emitted inline, without an outbox',
156
+ { collection: this.name }
157
+ )
158
+ }
159
+
160
+ /**
161
+ * Runs `fn` in a transaction and answers what it returned. The driver may call `fn` more
162
+ * than once, so it must not hold state of its own — an outbox row is built by the caller
163
+ * and reused, and a rolled back attempt leaves nothing behind.
164
+ *
165
+ * @public
166
+ * @template T
167
+ * @param {(session: import('mongodb').ClientSession) => Promise<T>} fn
168
+ * @return {Promise<T>}
169
+ */
170
+ async transaction(fn) {
171
+ return this.instance.client.withSession(async (session) =>
172
+ session.withTransaction(async () => fn(session))
173
+ )
81
174
  }
82
175
 
83
176
  /**
@@ -85,14 +178,26 @@ class Client extends Connector {
85
178
  * @override
86
179
  * @return {Promise<void>}
87
180
  */
88
- async close () {
89
- const instance = await INSTANCES[this.key]
181
+ async close() {
182
+ /*
183
+ * What was never counted is not discounted. An `open` that threw between taking the
184
+ * instance and incrementing it leaves the count one high, and a client nothing ever
185
+ * closes — which a process that is taken down and built again, as a halt does, would
186
+ * otherwise leak once per cycle.
187
+ */
188
+ if (this.instance === undefined) return
189
+
190
+ const instance = this.instance
191
+
192
+ this.instance = undefined
90
193
 
91
194
  instance.count--
92
195
 
93
196
  if (instance.count === 0) {
94
197
  await instance.client.close()
95
- delete INSTANCES[this.key]
198
+
199
+ // another `open` may have taken it in the meantime, and that one is not this one
200
+ if ((await INSTANCES[this.key]) === instance) delete INSTANCES[this.key]
96
201
  }
97
202
  }
98
203
 
@@ -101,11 +206,11 @@ class Client extends Connector {
101
206
  * @param {string[]} urls
102
207
  * @return {Promise<Instance>}
103
208
  */
104
- async createInstance (urls) {
209
+ async createInstance(urls) {
105
210
  const client = new MongoClient(urls.join(','), OPTIONS)
106
211
  const hosts = urls.map((str) => new URL(str).host)
107
212
 
108
- console.info('Connecting to MongoDB:', hosts.join(', '))
213
+ console.info('Connecting to MongoDB', { address: hosts.join(', ') })
109
214
 
110
215
  await client.connect()
111
216
 
@@ -119,9 +224,11 @@ class Client extends Connector {
119
224
  * @private
120
225
  * @return {Promise<string[]>}
121
226
  */
122
- async resolveURLs () {
123
- if (process.env.TOA_DEV === '1') {
124
- return ['mongodb://developer:secret@localhost']
227
+ async resolveURLs() {
228
+ // Toa's own development stack is not on the conventional ports: the applications built on
229
+ // Toa are, and they share the machine. See CONTRIBUTING.md.
230
+ if (environment.get('TOA_DEV') === '1') {
231
+ return ['mongodb://developer:secret@localhost:31020']
125
232
  } else {
126
233
  return await resolve(ID, this.locator.id)
127
234
  }
@@ -131,29 +238,63 @@ class Client extends Connector {
131
238
  * @private
132
239
  * @return {string}
133
240
  */
134
- resolveDB () {
135
- if (process.env.TOA_CONTEXT !== undefined) {
136
- return process.env.TOA_CONTEXT
137
- }
241
+ resolveDB() {
242
+ const scope = environment.scope()
243
+ const length = Buffer.byteLength(scope)
138
244
 
139
- if (process.env.TOA_DEV === '1') {
140
- return 'toa-dev'
141
- }
245
+ // MongoDB refuses the name only when something is first written, far from what caused it
246
+ if (length > MAX_DB_LENGTH)
247
+ throw new Error(
248
+ `Database name '${scope}' is ${length} bytes, and MongoDB takes no more than ` +
249
+ `${MAX_DB_LENGTH}: shorten TOA_CONTEXT or TOA_SUFFIX`
250
+ )
142
251
 
143
- throw new Error('Environment variable TOA_CONTEXT is not defined')
252
+ return scope
144
253
  }
145
254
  }
146
255
 
147
- function getKey (db, urls) {
256
+ /** what MongoDB takes for a database name, in bytes */
257
+ const MAX_DB_LENGTH = 63
258
+
259
+ function getKey(db, urls) {
148
260
  return db + ':' + urls.sort().join(' ')
149
261
  }
150
262
 
263
+ /**
264
+ * Concurrent pods race to create the same collection, and losing that race is not an error.
265
+ */
266
+ async function collection(db, name) {
267
+ try {
268
+ return await db.createCollection(name)
269
+ } catch (e) {
270
+ if (e.code !== ALREADY_EXISTS) throw e
271
+
272
+ return db.collection(name)
273
+ }
274
+ }
275
+
276
+ async function transactional(db) {
277
+ try {
278
+ const hello = await db.admin().command({ hello: 1 })
279
+
280
+ return hello.setName !== undefined || hello.msg === 'isdbgrid'
281
+ } catch (e) {
282
+ console.warn('MongoDB transaction support could not be determined', { error: e })
283
+
284
+ return false
285
+ }
286
+ }
287
+
288
+ /**
289
+ * `monitorCommands` is deliberately absent. It makes the driver materialize every reply
290
+ * eagerly to populate the monitoring event (`CommandSucceededEvent`), which defeats the
291
+ * lazy per-document deserialization a cursor exists for — a 100-document batch is then
292
+ * deserialized twice. `Storage` times its own calls instead.
293
+ */
151
294
  const OPTIONS = {
152
- ignoreUndefined: true,
153
- connectTimeoutMS: 0,
154
- serverSelectionTimeoutMS: 0
295
+ ignoreUndefined: true
155
296
  }
156
297
 
157
298
  const ALREADY_EXISTS = 48
158
-
159
- exports.Client = Client
299
+ const OUTBOX = '_outbox'
300
+ const INBOX = '_inbox'
package/src/factory.js CHANGED
@@ -1,16 +1,10 @@
1
- 'use strict'
1
+ import { Client } from './client.js'
2
+ import { Storage } from './storage.js'
2
3
 
3
- const { Client } = require('./client')
4
- const { Collection } = require('./collection')
5
- const { Storage } = require('./storage')
4
+ export class Factory {
5
+ storage(locator, entity, options = {}) {
6
+ const client = new Client(locator, options.outbox === true, options.inbox === true)
6
7
 
7
- class Factory {
8
- storage (locator, entity) {
9
- const client = new Client(locator)
10
- const connection = new Collection(client)
11
-
12
- return new Storage(connection, entity)
8
+ return new Storage(client, entity)
13
9
  }
14
10
  }
15
-
16
- exports.Factory = Factory