@toa.io/storages.mongodb 1.0.0-alpha.28 → 1.0.0-alpha.283
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 +139 -0
- package/package.json +9 -8
- package/readme.md +16 -0
- package/src/client.js +99 -24
- package/src/deployment.js +3 -8
- package/src/factory.js +6 -12
- package/src/index.js +2 -7
- package/src/outbox.js +125 -0
- package/src/record.js +6 -24
- package/src/storage.js +383 -93
- package/src/translate/criteria.js +2 -6
- package/src/translate/options.js +2 -6
- package/src/translate/rename.js +1 -5
- package/src/translate.js +13 -11
- package/test/query/criteria.fixtures.js +2 -7
- package/test/query/criteria.test.js +5 -4
- package/test/record.test.js +6 -23
- package/test/storage.test.js +73 -0
- package/test/translate.test.js +7 -6
- package/types/connection.d.ts +1 -1
- package/src/collection.js +0 -69
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
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.283](https://github.com/toa-io/toa/compare/v1.0.0-alpha.282...v1.0.0-alpha.283) (2026-09-04)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @toa.io/storages.mongodb
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# [1.0.0-alpha.282](https://github.com/toa-io/toa/compare/v1.0.0-alpha.281...v1.0.0-alpha.282) (2026-09-03)
|
|
15
|
+
|
|
16
|
+
**Note:** Version bump only for package @toa.io/storages.mongodb
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# [1.0.0-alpha.278](https://github.com/toa-io/toa/compare/v1.0.0-alpha.277...v1.0.0-alpha.278) (2026-09-03)
|
|
23
|
+
|
|
24
|
+
**Note:** Version bump only for package @toa.io/storages.mongodb
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# [1.0.0-alpha.277](https://github.com/toa-io/toa/compare/v1.0.0-alpha.276...v1.0.0-alpha.277) (2026-09-03)
|
|
31
|
+
|
|
32
|
+
**Note:** Version bump only for package @toa.io/storages.mongodb
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# [1.0.0-alpha.274](https://github.com/toa-io/toa/compare/v1.0.0-alpha.273...v1.0.0-alpha.274) (2026-09-02)
|
|
39
|
+
|
|
40
|
+
**Note:** Version bump only for package @toa.io/storages.mongodb
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# [1.0.0-alpha.273](https://github.com/toa-io/toa/compare/v1.0.0-alpha.272...v1.0.0-alpha.273) (2026-09-02)
|
|
47
|
+
|
|
48
|
+
**Note:** Version bump only for package @toa.io/storages.mongodb
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# [1.0.0-alpha.272](https://github.com/toa-io/toa/compare/v1.0.0-alpha.271...v1.0.0-alpha.272) (2026-09-01)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
* refactor(core)!: pump the outbox in one cycle ([bf590fe](https://github.com/toa-io/toa/commit/bf590fe8ed59c701b5fd1a91ba4874685b79242f))
|
|
58
|
+
* feat(core)!: commit events with the state that produced them ([1eb68cc](https://github.com/toa-io/toa/commit/1eb68cc435dbfa03faa16009fceb866693d22e1a)), closes [#20](https://github.com/toa-io/toa/issues/20)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
### BREAKING CHANGES
|
|
62
|
+
|
|
63
|
+
* `Storage.outbox.pending` takes a fourth argument, the id to
|
|
64
|
+
continue from.
|
|
65
|
+
|
|
66
|
+
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
67
|
+
* `event.changeset` is removed; use `origin` and `state`. `State`
|
|
68
|
+
takes an `Outbox` in place of an `Emission`. `difference` is dropped from
|
|
69
|
+
`@toa.io/generic` along with its last caller.
|
|
70
|
+
|
|
71
|
+
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# [1.0.0-alpha.270](https://github.com/toa-io/toa/compare/v1.0.0-alpha.269...v1.0.0-alpha.270) (2026-08-31)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
### Performance Improvements
|
|
81
|
+
|
|
82
|
+
* **boot:** stop paying for what a composition does not need to start ([1619c27](https://github.com/toa-io/toa/commit/1619c2743072f4706939ce72f3074e615d26a91e))
|
|
83
|
+
* **storages.mongodb:** time the call instead of monitoring the command ([135f1a9](https://github.com/toa-io/toa/commit/135f1a97c753b1caed9291f85cea0521ef68b0e3))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# [1.0.0-alpha.266](https://github.com/toa-io/toa/compare/v1.0.0-alpha.265...v1.0.0-alpha.266) (2026-08-29)
|
|
90
|
+
|
|
91
|
+
**Note:** Version bump only for package @toa.io/storages.mongodb
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# [1.0.0-alpha.265](https://github.com/toa-io/toa/compare/v1.0.0-alpha.264...v1.0.0-alpha.265) (2026-08-29)
|
|
98
|
+
|
|
99
|
+
**Note:** Version bump only for package @toa.io/storages.mongodb
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# [1.0.0-alpha.264](https://github.com/toa-io/toa/compare/v1.0.0-alpha.263...v1.0.0-alpha.264) (2026-08-29)
|
|
106
|
+
|
|
107
|
+
**Note:** Version bump only for package @toa.io/storages.mongodb
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# [1.0.0-alpha.263](https://github.com/toa-io/toa/compare/v1.0.0-alpha.262...v1.0.0-alpha.263) (2026-08-29)
|
|
114
|
+
|
|
115
|
+
**Note:** Version bump only for package @toa.io/storages.mongodb
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# [1.0.0-alpha.262](https://github.com/toa-io/toa/compare/v1.0.0-alpha.261...v1.0.0-alpha.262) (2026-08-28)
|
|
122
|
+
|
|
123
|
+
**Note:** Version bump only for package @toa.io/storages.mongodb
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# [1.0.0-alpha.259](https://github.com/toa-io/toa/compare/v1.0.0-alpha.258...v1.0.0-alpha.259) (2026-08-24)
|
|
130
|
+
|
|
131
|
+
**Note:** Version bump only for package @toa.io/storages.mongodb
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# [1.0.0-alpha.257](https://github.com/toa-io/toa/compare/v1.0.0-alpha.256...v1.0.0-alpha.257) (2026-08-24)
|
|
138
|
+
|
|
139
|
+
**Note:** Version bump only for package @toa.io/storages.mongodb
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toa.io/storages.mongodb",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.283",
|
|
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/
|
|
23
|
-
"@toa.io/
|
|
24
|
-
"@toa.io/
|
|
25
|
-
"@toa.io/
|
|
26
|
-
"
|
|
27
|
-
"
|
|
23
|
+
"@toa.io/conveyor": "1.0.0-alpha.283",
|
|
24
|
+
"@toa.io/core": "1.0.0-alpha.283",
|
|
25
|
+
"@toa.io/generic": "1.0.0-alpha.283",
|
|
26
|
+
"@toa.io/pointer": "1.0.0-alpha.283",
|
|
27
|
+
"mongodb": "7.6.0",
|
|
28
|
+
"openspan": "1.0.0-alpha.277",
|
|
28
29
|
"saslprep": "1.0.3"
|
|
29
30
|
},
|
|
30
|
-
"gitHead": "
|
|
31
|
+
"gitHead": "2f52b023bed0ccb2ecdb2b82fe59fbe38bf282e1"
|
|
31
32
|
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# MongoDB Storage
|
|
2
|
+
|
|
3
|
+
## Tracing
|
|
4
|
+
|
|
5
|
+
Commands are recorded as `client` spans within the trace of the current invocation,
|
|
6
|
+
using the [driver command monitoring events](https://www.mongodb.com/docs/drivers/node/current/monitoring/command-monitoring/).
|
|
7
|
+
|
|
8
|
+
Spans are named `{command} {collection}` and carry `db.*` attributes following the
|
|
9
|
+
[OpenTelemetry semantic conventions](https://opentelemetry.io/docs/specs/semconv/database/mongodb/):
|
|
10
|
+
`db.system`, `db.namespace`, `db.collection.name`, `db.operation.name`.
|
|
11
|
+
|
|
12
|
+
Commands executed outside of a sampled trace context (e.g. index management on startup)
|
|
13
|
+
and internal driver commands (`hello`, `ping`, authentication) are not recorded.
|
|
14
|
+
|
|
15
|
+
Monitoring is client-side only and does not affect the MongoDB server. Span recording
|
|
16
|
+
adds no waiting to the query path: exporting is buffered and happens in the background.
|
package/src/client.js
CHANGED
|
@@ -1,28 +1,48 @@
|
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
import { console } from 'openspan'
|
|
8
|
+
import { Connector } from '@toa.io/core'
|
|
9
|
+
import { resolve } from '@toa.io/pointer'
|
|
10
|
+
import { ID } from './deployment.js'
|
|
11
|
+
import { MongoClient } from 'mongodb'
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
14
|
* @type {Record<string, Promise<Instance>>}
|
|
16
15
|
*/
|
|
17
16
|
const INSTANCES = {}
|
|
18
17
|
|
|
19
|
-
class Client extends Connector {
|
|
18
|
+
export class Client extends Connector {
|
|
19
|
+
name
|
|
20
|
+
|
|
20
21
|
/**
|
|
21
22
|
* @public
|
|
22
23
|
* @type {import('mongodb').Collection}
|
|
23
24
|
*/
|
|
24
25
|
collection
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* The outbox rows of this component, absent unless something consumes its events. Created
|
|
29
|
+
* eagerly beside the entity collection, because a transaction cannot create a collection and
|
|
30
|
+
* an index build cannot run inside one.
|
|
31
|
+
*
|
|
32
|
+
* @public
|
|
33
|
+
* @type {import('mongodb').Collection | undefined}
|
|
34
|
+
*/
|
|
35
|
+
outbox
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Whether this deployment can run transactions at all. A standalone mongod cannot, and an
|
|
39
|
+
* outbox without atomicity is worse than none, so the storage falls back to inline emission.
|
|
40
|
+
*
|
|
41
|
+
* @public
|
|
42
|
+
* @type {boolean}
|
|
43
|
+
*/
|
|
44
|
+
transactional = false
|
|
45
|
+
|
|
26
46
|
/**
|
|
27
47
|
* @private
|
|
28
48
|
* @type {Locator}
|
|
@@ -41,13 +61,22 @@ class Client extends Connector {
|
|
|
41
61
|
*/
|
|
42
62
|
key
|
|
43
63
|
|
|
64
|
+
/**
|
|
65
|
+
* @private
|
|
66
|
+
* @type {boolean}
|
|
67
|
+
*/
|
|
68
|
+
publishes
|
|
69
|
+
|
|
44
70
|
/**
|
|
45
71
|
* @param {Locator} locator
|
|
72
|
+
* @param {boolean} [publishes] whether this component publishes anything
|
|
46
73
|
*/
|
|
47
|
-
constructor (locator) {
|
|
74
|
+
constructor (locator, publishes = false) {
|
|
48
75
|
super()
|
|
49
76
|
|
|
50
77
|
this.locator = locator
|
|
78
|
+
this.name = locator.lowercase
|
|
79
|
+
this.publishes = publishes
|
|
51
80
|
}
|
|
52
81
|
|
|
53
82
|
/**
|
|
@@ -58,26 +87,44 @@ class Client extends Connector {
|
|
|
58
87
|
async open () {
|
|
59
88
|
const urls = await this.resolveURLs()
|
|
60
89
|
const dbname = this.resolveDB()
|
|
61
|
-
const collname = this.locator.lowercase
|
|
62
90
|
|
|
63
91
|
this.key = getKey(dbname, urls)
|
|
64
92
|
|
|
65
|
-
|
|
93
|
+
try {
|
|
94
|
+
INSTANCES[this.key] ??= this.createInstance(urls)
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error('Failed to connect to MongoDB', { urls, error })
|
|
97
|
+
}
|
|
66
98
|
|
|
67
99
|
this.instance = await INSTANCES[this.key]
|
|
68
100
|
this.instance.count++
|
|
69
101
|
|
|
70
102
|
const db = this.instance.client.db(dbname)
|
|
71
103
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
} catch (e) {
|
|
75
|
-
if (e.code !== ALREADY_EXISTS) {
|
|
76
|
-
throw e
|
|
77
|
-
}
|
|
104
|
+
this.collection = await collection(db, this.name)
|
|
105
|
+
this.transactional = await transactional(db)
|
|
78
106
|
|
|
79
|
-
|
|
80
|
-
|
|
107
|
+
if (!this.publishes) return
|
|
108
|
+
|
|
109
|
+
if (this.transactional) this.outbox = await collection(db, this.name + OUTBOX)
|
|
110
|
+
else
|
|
111
|
+
console.warn('MongoDB is not a replica set; events are emitted inline, without an outbox',
|
|
112
|
+
{ collection: this.name })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Runs `fn` in a transaction and answers what it returned. The driver may call `fn` more
|
|
117
|
+
* than once, so it must not hold state of its own — an outbox row is built by the caller
|
|
118
|
+
* and reused, and a rolled back attempt leaves nothing behind.
|
|
119
|
+
*
|
|
120
|
+
* @public
|
|
121
|
+
* @template T
|
|
122
|
+
* @param {(session: import('mongodb').ClientSession) => Promise<T>} fn
|
|
123
|
+
* @return {Promise<T>}
|
|
124
|
+
*/
|
|
125
|
+
async transaction (fn) {
|
|
126
|
+
return this.instance.client.withSession(async (session) =>
|
|
127
|
+
session.withTransaction(async () => fn(session)))
|
|
81
128
|
}
|
|
82
129
|
|
|
83
130
|
/**
|
|
@@ -105,7 +152,7 @@ class Client extends Connector {
|
|
|
105
152
|
const client = new MongoClient(urls.join(','), OPTIONS)
|
|
106
153
|
const hosts = urls.map((str) => new URL(str).host)
|
|
107
154
|
|
|
108
|
-
console.info('Connecting to MongoDB
|
|
155
|
+
console.info('Connecting to MongoDB', { address: hosts.join(', ') })
|
|
109
156
|
|
|
110
157
|
await client.connect()
|
|
111
158
|
|
|
@@ -148,12 +195,40 @@ function getKey (db, urls) {
|
|
|
148
195
|
return db + ':' + urls.sort().join(' ')
|
|
149
196
|
}
|
|
150
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Concurrent pods race to create the same collection, and losing that race is not an error.
|
|
200
|
+
*/
|
|
201
|
+
async function collection (db, name) {
|
|
202
|
+
try {
|
|
203
|
+
return await db.createCollection(name)
|
|
204
|
+
} catch (e) {
|
|
205
|
+
if (e.code !== ALREADY_EXISTS) throw e
|
|
206
|
+
|
|
207
|
+
return db.collection(name)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function transactional (db) {
|
|
212
|
+
try {
|
|
213
|
+
const hello = await db.admin().command({ hello: 1 })
|
|
214
|
+
|
|
215
|
+
return hello.setName !== undefined || hello.msg === 'isdbgrid'
|
|
216
|
+
} catch (e) {
|
|
217
|
+
console.warn('MongoDB transaction support could not be determined', { error: e })
|
|
218
|
+
|
|
219
|
+
return false
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* `monitorCommands` is deliberately absent. It makes the driver materialize every reply
|
|
225
|
+
* eagerly to populate the monitoring event (`CommandSucceededEvent`), which defeats the
|
|
226
|
+
* lazy per-document deserialization a cursor exists for — a 100-document batch is then
|
|
227
|
+
* deserialized twice. `Storage` times its own calls instead.
|
|
228
|
+
*/
|
|
151
229
|
const OPTIONS = {
|
|
152
|
-
ignoreUndefined: true
|
|
153
|
-
connectTimeoutMS: 0,
|
|
154
|
-
serverSelectionTimeoutMS: 0
|
|
230
|
+
ignoreUndefined: true
|
|
155
231
|
}
|
|
156
232
|
|
|
157
233
|
const ALREADY_EXISTS = 48
|
|
158
|
-
|
|
159
|
-
exports.Client = Client
|
|
234
|
+
const OUTBOX = '_outbox'
|
package/src/deployment.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { createVariables } from '@toa.io/pointer'
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
const deployment = (instances, annotation) => {
|
|
3
|
+
export const deployment = (instances, annotation) => {
|
|
6
4
|
const requests = instances.map((instance) => createRequest(instance))
|
|
7
5
|
const variables = createVariables(ID, annotation, requests)
|
|
8
6
|
|
|
@@ -16,7 +14,4 @@ function createRequest (instance) {
|
|
|
16
14
|
}
|
|
17
15
|
}
|
|
18
16
|
|
|
19
|
-
const ID = 'mongodb'
|
|
20
|
-
|
|
21
|
-
exports.ID = ID
|
|
22
|
-
exports.deployment = deployment
|
|
17
|
+
export const ID = 'mongodb'
|
package/src/factory.js
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import { Client } from './client.js'
|
|
2
|
+
import { Storage } from './storage.js'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
4
|
+
export class Factory {
|
|
5
|
+
storage (locator, entity, options = {}) {
|
|
6
|
+
const client = new Client(locator, options.outbox === true)
|
|
6
7
|
|
|
7
|
-
|
|
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/index.js
CHANGED
package/src/outbox.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { console } from 'openspan'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The outbox rows of one component. Its lifecycle is the Client's, so it is not a Connector.
|
|
5
|
+
*
|
|
6
|
+
* Core owns what a row means — its id, its lane and when it becomes due. This only writes
|
|
7
|
+
* them, reads back what is due, and marks what has been published.
|
|
8
|
+
*/
|
|
9
|
+
export class Outbox {
|
|
10
|
+
/** @type {import('mongodb').Collection} */
|
|
11
|
+
#collection
|
|
12
|
+
|
|
13
|
+
#retention
|
|
14
|
+
|
|
15
|
+
constructor (collection) {
|
|
16
|
+
this.#collection = collection
|
|
17
|
+
this.#retention = retention()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** @param {import('mongodb').ClientSession} session */
|
|
21
|
+
async insert (row, session) {
|
|
22
|
+
await this.#collection.insertOne(to(row), { session })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** @param {import('mongodb').ClientSession} session */
|
|
26
|
+
async insertMany (rows, session) {
|
|
27
|
+
if (rows.length === 0) return
|
|
28
|
+
|
|
29
|
+
await this.#collection.insertMany(rows.map(to), { session })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* One page of what this replica should publish: due, still unpublished, and in a lane it
|
|
34
|
+
* owns. In steady state the first page is empty.
|
|
35
|
+
*
|
|
36
|
+
* `after` continues from the last id of the page before. Ids are uuid v7, so their order is
|
|
37
|
+
* the order rows were written and a page is never read twice within a cycle — which matters
|
|
38
|
+
* because a row stays unpublished in the database until the cycle that sent it marks it.
|
|
39
|
+
*/
|
|
40
|
+
async pending (lanes, now, limit, after = undefined) {
|
|
41
|
+
const criteria = { lane: { $in: lanes }, published: false, pending: { $lte: now } }
|
|
42
|
+
|
|
43
|
+
if (after !== undefined) criteria._id = { $gt: after }
|
|
44
|
+
|
|
45
|
+
const rows = await this.#collection
|
|
46
|
+
.find(criteria)
|
|
47
|
+
.sort({ _id: 1 })
|
|
48
|
+
.limit(limit)
|
|
49
|
+
.toArray()
|
|
50
|
+
|
|
51
|
+
return rows.map(from)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* One batched write for many events, which is why the ids are held in memory until the
|
|
56
|
+
* tick rather than updated one by one.
|
|
57
|
+
*/
|
|
58
|
+
async settle (ids) {
|
|
59
|
+
if (ids.length === 0) return
|
|
60
|
+
|
|
61
|
+
await this.#collection.updateMany({ _id: { $in: ids } },
|
|
62
|
+
{ $set: { published: true, publishedAt: new Date() } })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The entity collection's index management does not reach here, so this collection keeps
|
|
67
|
+
* its own — including pruning, or a later change leaves the old index behind forever.
|
|
68
|
+
*/
|
|
69
|
+
async index () {
|
|
70
|
+
const desired = {
|
|
71
|
+
// holds only what is not published yet, so it stays at in-flight size
|
|
72
|
+
outbox_pending: {
|
|
73
|
+
fields: { lane: 1, pending: 1 },
|
|
74
|
+
options: { name: 'outbox_pending', partialFilterExpression: { published: false } }
|
|
75
|
+
},
|
|
76
|
+
// an unpublished row has no `publishedAt`, and the TTL monitor skips those — so a row
|
|
77
|
+
// that never made it out is never reaped
|
|
78
|
+
outbox_published_at: {
|
|
79
|
+
fields: { publishedAt: 1 },
|
|
80
|
+
options: { name: 'outbox_published_at', expireAfterSeconds: this.#retention }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const { fields, options } of Object.values(desired))
|
|
85
|
+
await this.#collection.createIndex(fields, options)
|
|
86
|
+
.catch((e) => console.warn('MongoDB outbox index creation failed',
|
|
87
|
+
{ collection: this.#collection.collectionName, name: options.name, error: e }))
|
|
88
|
+
|
|
89
|
+
await this.#prune(Object.keys(desired))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** @private */
|
|
93
|
+
async #prune (desired) {
|
|
94
|
+
let current
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
current = await this.#collection.listIndexes().toArray()
|
|
98
|
+
} catch {
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const obsolete = current
|
|
103
|
+
.map(({ name }) => name)
|
|
104
|
+
.filter((name) => name !== '_id_' && !desired.includes(name))
|
|
105
|
+
|
|
106
|
+
if (obsolete.length === 0) return
|
|
107
|
+
|
|
108
|
+
console.info('Removing obsolete outbox indexes',
|
|
109
|
+
{ collection: this.#collection.collectionName, indexes: obsolete.join(', ') })
|
|
110
|
+
|
|
111
|
+
await Promise.all(obsolete.map((name) => this.#collection.dropIndex(name)))
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const to = ({ id, ...rest }) => ({ _id: id, ...rest })
|
|
116
|
+
const from = ({ _id, ...rest }) => ({ id: _id, ...rest })
|
|
117
|
+
|
|
118
|
+
function retention () {
|
|
119
|
+
const value = Number(process.env.TOA_OUTBOX_RETENTION)
|
|
120
|
+
|
|
121
|
+
return Number.isNaN(value) || value < 0 ? RETENTION : value
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** seconds a published row is kept as a change log before the TTL monitor reaps it */
|
|
125
|
+
const RETENTION = 86400
|
package/src/record.js
CHANGED
|
@@ -1,32 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @param {toa.core.storages.Record} entity
|
|
5
|
-
* @returns {toa.mongodb.Record}
|
|
6
|
-
*/
|
|
7
|
-
const to = (entity) => {
|
|
8
|
-
const {
|
|
9
|
-
id,
|
|
10
|
-
...rest
|
|
11
|
-
} = entity
|
|
1
|
+
export function to (entity) {
|
|
2
|
+
const { id, ...rest } = entity
|
|
12
3
|
|
|
13
4
|
return /** @type {toa.mongodb.Record} */ { _id: id, ...rest }
|
|
14
5
|
}
|
|
15
6
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
*/
|
|
20
|
-
const from = (record) => {
|
|
21
|
-
if (record === undefined || record === null) return null
|
|
7
|
+
export function from (record) {
|
|
8
|
+
if (record === undefined || record === null)
|
|
9
|
+
return null
|
|
22
10
|
|
|
23
|
-
const {
|
|
24
|
-
_id,
|
|
25
|
-
...rest
|
|
26
|
-
} = record
|
|
11
|
+
const { _id, ...rest } = record
|
|
27
12
|
|
|
28
13
|
return { id: _id, ...rest }
|
|
29
14
|
}
|
|
30
|
-
|
|
31
|
-
exports.to = to
|
|
32
|
-
exports.from = from
|