@toa.io/storages.mongodb 1.0.0-alpha.31 → 1.0.0-alpha.310

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,203 @@
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.310](https://github.com/toa-io/toa/compare/v1.0.0-alpha.309...v1.0.0-alpha.310) (2026-09-16)
7
+
8
+ **Note:** Version bump only for package @toa.io/storages.mongodb
9
+
10
+
11
+
12
+
13
+
14
+ # [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)
15
+
16
+ **Note:** Version bump only for package @toa.io/storages.mongodb
17
+
18
+
19
+
20
+
21
+
22
+ # [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)
23
+
24
+ **Note:** Version bump only for package @toa.io/storages.mongodb
25
+
26
+
27
+
28
+
29
+
30
+ # [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)
31
+
32
+ **Note:** Version bump only for package @toa.io/storages.mongodb
33
+
34
+
35
+
36
+
37
+
38
+ # [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)
39
+
40
+ **Note:** Version bump only for package @toa.io/storages.mongodb
41
+
42
+
43
+
44
+
45
+
46
+ # [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)
47
+
48
+ **Note:** Version bump only for package @toa.io/storages.mongodb
49
+
50
+
51
+
52
+
53
+
54
+ # [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)
55
+
56
+ **Note:** Version bump only for package @toa.io/storages.mongodb
57
+
58
+
59
+
60
+
61
+
62
+ # [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)
63
+
64
+ ### Features
65
+
66
+ * **metrics:** measure storage, stash, blob storages and fetch ([252298b](https://github.com/toa-io/toa/commit/252298bb9f34421694da4ee89742d62b47c39dfb))
67
+
68
+ ### Performance Improvements
69
+
70
+ * **storages.mongodb:** rename a record's _id to id in place ([4aeb2a1](https://github.com/toa-io/toa/commit/4aeb2a1d61f7f2b0ddab203b5887152e8917ef7f))
71
+
72
+
73
+ # [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)
74
+
75
+ **Note:** Version bump only for package @toa.io/storages.mongodb
76
+
77
+
78
+
79
+
80
+
81
+ # [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)
82
+
83
+ **Note:** Version bump only for package @toa.io/storages.mongodb
84
+
85
+
86
+
87
+
88
+
89
+ # [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)
90
+
91
+ * feat(core)!: an operation may ask to run once ([dca616c](https://github.com/toa-io/toa/commit/dca616cb380a7a389e277875d3bd5ca55a6e4d45))
92
+
93
+ ### Bug Fixes
94
+
95
+ * **extensions:** what a factory remembers does not outlive the tree it was made for ([dd36631](https://github.com/toa-io/toa/commit/dd3663160f500eadfb701bf840ef3741a2d075f0))
96
+
97
+ ### Features
98
+
99
+ * **core:** an assignment may ask to run once as well ([10fbcbd](https://github.com/toa-io/toa/commit/10fbcbd86a5e82960e65f62c09d0a465ae8d0353))
100
+
101
+ ### BREAKING CHANGES
102
+
103
+ * `Storage.store`, `.upsert` and `.ensure` take one more
104
+ argument, and `Storage` has two more members. A connector that ignores them
105
+ works exactly as it did; a component that asks it for `once` is refused at
106
+ boot. See migrations/299.md.
107
+
108
+
109
+ # [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)
110
+
111
+ ### Features
112
+
113
+ * **core:** a record carries the region that wrote it ([13ed187](https://github.com/toa-io/toa/commit/13ed187604f06dec66feec2e26ccf9dae30bb048))
114
+ * **core:** an outbox row is published to destinations ([cfb77b4](https://github.com/toa-io/toa/commit/cfb77b4826ff0f9560769a0996981e143d6525af))
115
+
116
+
117
+ # [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)
118
+
119
+ **Note:** Version bump only for package @toa.io/storages.mongodb
120
+
121
+
122
+
123
+
124
+
125
+ # [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)
126
+
127
+ **Note:** Version bump only for package @toa.io/storages.mongodb
128
+
129
+
130
+
131
+
132
+
133
+ # [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)
134
+
135
+ **Note:** Version bump only for package @toa.io/storages.mongodb
136
+
137
+
138
+
139
+
140
+
141
+ # [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)
142
+
143
+ **Note:** Version bump only for package @toa.io/storages.mongodb
144
+
145
+
146
+
147
+
148
+
149
+ # [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)
150
+
151
+ **Note:** Version bump only for package @toa.io/storages.mongodb
152
+
153
+
154
+
155
+
156
+
157
+ # [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)
158
+
159
+ **Note:** Version bump only for package @toa.io/storages.mongodb
160
+
161
+
162
+
163
+
164
+
165
+ # [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)
166
+
167
+ ### Features
168
+
169
+ * **definitions:** what a package declares is read from one package ([7052341](https://github.com/toa-io/toa/commit/70523411b5d9e2b204c999aa02365173f3bb518d))
170
+
171
+
172
+ # [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)
173
+
174
+ **Note:** Version bump only for package @toa.io/storages.mongodb
175
+
176
+
177
+
178
+
179
+
180
+ # [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)
181
+
182
+ * 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)
183
+
184
+ ### BREAKING CHANGES
185
+
186
+ * a component that read `process.env.TOA_*` reads `context` instead;
187
+ `echo(input)` no longer substitutes from the environment; a bash operation sees no
188
+ `TOA_*`; images no longer set `USER node` — see migrations/289.md.
189
+
190
+
191
+ # [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)
192
+
193
+ **Note:** Version bump only for package @toa.io/storages.mongodb
194
+
195
+
196
+
197
+
198
+
199
+ # [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)
200
+
201
+ ### Features
202
+
203
+ * **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.31",
3
+ "version": "1.0.0-alpha.310",
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.31",
23
- "@toa.io/conveyor": "1.0.0-alpha.31",
24
- "@toa.io/core": "1.0.0-alpha.31",
25
- "@toa.io/generic": "1.0.0-alpha.31",
26
- "@toa.io/pointer": "1.0.0-alpha.31",
27
- "mongodb": "6.3.0",
28
- "saslprep": "1.0.3"
23
+ "@toa.io/conveyor": "1.0.0-alpha.310",
24
+ "@toa.io/core": "1.0.0-alpha.310",
25
+ "@toa.io/definitions": "1.0.0-alpha.310",
26
+ "@toa.io/generic": "1.0.0-alpha.310",
27
+ "@toa.io/pointer": "1.0.0-alpha.310",
28
+ "mongodb": "7.6.0",
29
+ "openspan": "1.0.0-alpha.305"
29
30
  },
30
- "gitHead": "91839a488176853a0731f49abc70dc7103ed7cca"
31
+ "gitHead": "d952c57f77279729e26d52d50c6df47138ad0a4f"
31
32
  }
package/readme.md ADDED
@@ -0,0 +1,15 @@
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.
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,56 @@ 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 context = environment.get('TOA_CONTEXT')
138
243
 
139
- if (process.env.TOA_DEV === '1') {
140
- return 'toa-dev'
141
- }
244
+ if (context !== undefined) return context
245
+
246
+ if (environment.get('TOA_DEV') === '1') return 'toa-dev'
142
247
 
143
248
  throw new Error('Environment variable TOA_CONTEXT is not defined')
144
249
  }
145
250
  }
146
251
 
147
- function getKey (db, urls) {
252
+ function getKey(db, urls) {
148
253
  return db + ':' + urls.sort().join(' ')
149
254
  }
150
255
 
256
+ /**
257
+ * Concurrent pods race to create the same collection, and losing that race is not an error.
258
+ */
259
+ async function collection(db, name) {
260
+ try {
261
+ return await db.createCollection(name)
262
+ } catch (e) {
263
+ if (e.code !== ALREADY_EXISTS) throw e
264
+
265
+ return db.collection(name)
266
+ }
267
+ }
268
+
269
+ async function transactional(db) {
270
+ try {
271
+ const hello = await db.admin().command({ hello: 1 })
272
+
273
+ return hello.setName !== undefined || hello.msg === 'isdbgrid'
274
+ } catch (e) {
275
+ console.warn('MongoDB transaction support could not be determined', { error: e })
276
+
277
+ return false
278
+ }
279
+ }
280
+
281
+ /**
282
+ * `monitorCommands` is deliberately absent. It makes the driver materialize every reply
283
+ * eagerly to populate the monitoring event (`CommandSucceededEvent`), which defeats the
284
+ * lazy per-document deserialization a cursor exists for — a 100-document batch is then
285
+ * deserialized twice. `Storage` times its own calls instead.
286
+ */
151
287
  const OPTIONS = {
152
- ignoreUndefined: true,
153
- connectTimeoutMS: 0,
154
- serverSelectionTimeoutMS: 0
288
+ ignoreUndefined: true
155
289
  }
156
290
 
157
291
  const ALREADY_EXISTS = 48
158
-
159
- exports.Client = Client
292
+ const OUTBOX = '_outbox'
293
+ 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
package/src/inbox.js ADDED
@@ -0,0 +1,85 @@
1
+ import { environment } from '@toa.io/generic'
2
+ import { index, prune } from './indexes.js'
3
+
4
+ /**
5
+ * The calls a component has already made good on. Its lifecycle is the Client's, so it is not
6
+ * a Connector.
7
+ *
8
+ * Core owns what a record means — its identity and the reply it holds. This only writes it,
9
+ * inside the transaction the entity is written in, and reads one back.
10
+ */
11
+ export class Inbox {
12
+ /** @type {import('mongodb').Collection} */
13
+ #collection
14
+
15
+ #retention
16
+
17
+ constructor(collection) {
18
+ this.#collection = collection
19
+ this.#retention = retention()
20
+ }
21
+
22
+ /**
23
+ * What the call under this identity answered, or `null` where it was never made.
24
+ *
25
+ * @param {string} id
26
+ * @returns {Promise<object | null>}
27
+ */
28
+ async recall(id) {
29
+ const record = await this.#collection.findOne(
30
+ { _id: id },
31
+ { projection: { reply: 1 } }
32
+ )
33
+
34
+ return record === null ? null : (record.reply ?? {})
35
+ }
36
+
37
+ /**
38
+ * Writes the call down, in the session the entity is written in. A duplicate key is the
39
+ * whole mechanism: the call has been made, this transaction changes nothing, and the caller
40
+ * is answered from what is already there.
41
+ *
42
+ * @param {{ id: string, reply: object }} call
43
+ * @param {import('mongodb').ClientSession} session
44
+ */
45
+ async insert(call, session) {
46
+ await this.#collection.insertOne(
47
+ { _id: call.id, reply: call.reply ?? {}, at: new Date() },
48
+ { session }
49
+ )
50
+ }
51
+
52
+ /**
53
+ * The runtime's own index, not the component's, so it is declared here rather than in a
54
+ * migration a component would have to write — including pruning, or a later change leaves
55
+ * the old index behind forever.
56
+ */
57
+ async index() {
58
+ const desired = {
59
+ // how long a call is remembered is how long a duplicate of it is caught
60
+ inbox_at: {
61
+ fields: { at: 1 },
62
+ options: { name: 'inbox_at', expireAfterSeconds: this.#retention }
63
+ }
64
+ }
65
+
66
+ for (const { fields, options } of Object.values(desired))
67
+ await index(this.#collection, fields, options)
68
+
69
+ await prune(this.#collection, Object.keys(desired))
70
+ }
71
+ }
72
+
73
+ function retention() {
74
+ const value = Number(environment.get('TOA_INBOX_RETENTION'))
75
+
76
+ return Number.isNaN(value) || value <= 0 ? RETENTION : value
77
+ }
78
+
79
+ /**
80
+ * Seconds a call is remembered. An hour, and not the outbox's day: what a duplicate arrives
81
+ * within is the broker's redelivery, the five attempts `comq` makes of a message, and whatever
82
+ * a client retries on — minutes. Every call of an operation that declares `once` is a document
83
+ * here for this long.
84
+ */
85
+ const RETENTION = 3600
package/src/index.js CHANGED
@@ -1,7 +1 @@
1
- 'use strict'
2
-
3
- const { deployment } = require('./deployment')
4
- const { Factory } = require('./factory')
5
-
6
- exports.deployment = deployment
7
- exports.Factory = Factory
1
+ export { Factory } from './factory.js'