Skip to content

Commit 7f7e256

Browse files
committed
feat: run mongo ourselves and disable meteor mongo
1 parent 31d30c8 commit 7f7e256

10 files changed

Lines changed: 247 additions & 164 deletions

File tree

.env.example

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Local dev configuration for `yarn dev` / `yarn start`.
2+
# Copy to `.env` (which is gitignored) and adjust as needed. All values are optional.
3+
4+
# --- MongoDB ---
5+
# In dev, `yarn dev` spawns its own MongoDB (single-node replica set, required for change
6+
# streams) via mongodb-memory-server.
7+
# It reuses Meteor's existing data dir (meteor/.meteor/local/db), so existing dev data is kept.
8+
9+
# mongod version to run. Default matches the mongod bundled with Meteor 3.4.1, so the existing
10+
# data dir opens cleanly. Avoid setting an OLDER version than what last wrote the data dir.
11+
# MONGO_VERSION=7.0.16
12+
13+
# Port the dev MongoDB listens on. A stable, predictable port so tooling (Compass, mongo shell)
14+
# can always connect to the same address. If the port is in use, the dev script errors out.
15+
# MONGO_PORT=3001
16+
17+
# Logical database name used in the connection URL. Default matches Meteor's dev default.
18+
# MONGO_DEV_DB=meteor
19+
20+
# Set MONGO_URL to use an external MongoDB instead. When set, the dev script does NOT spawn its
21+
# own MongoDB and the variables above are ignored. The server still requires a replica set.
22+
# MONGO_URL=mongodb://127.0.0.1:27017/meteor?replicaSet=rs0

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
_gsdata_/
22
settings.json
3+
.env
34
meteor/.coverage/
45
node_modules
56
**/yarn-error.log

meteor/.meteor/packages

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,8 @@ meteor@2.3.0
1212
webapp@2.1.2
1313
ddp@1.4.2
1414

15-
mongo@2.3.0 # The database Meteor supports right now
16-
1715
ecmascript@0.18.0 # Enable ECMAScript2015+ syntax in app code
1816
typescript@5.10.0 # Enable TypeScript syntax in .ts and .tsx modules
1917

20-
tracker@1.3.4 # Meteor's client-side reactive programming library
21-
2218
zodern:types
2319
rspack

meteor/.meteor/versions

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
allow-deny@2.1.0
21
babel-compiler@7.14.0
32
babel-runtime@1.5.2
43
base64@1.0.13
5-
binary-heap@1.0.12
64
boilerplate-generator@2.1.0
75
callback-hook@1.7.0
86
check@1.5.0
@@ -20,21 +18,14 @@ ecmascript-runtime-server@0.11.1
2018
ejson@1.1.5
2119
facts-base@1.0.2
2220
fetch@0.1.6
23-
geojson-utils@1.0.12
2421
id-map@1.2.0
2522
inter-process-messaging@0.1.2
2623
logging@1.3.6
2724
meteor@2.3.0
28-
minimongo@2.1.0
2925
modern-browsers@0.2.3
3026
modules@0.20.3
3127
modules-runtime@0.13.2
32-
mongo@2.3.0
33-
mongo-decimal@0.2.0
34-
mongo-dev-server@1.1.1
3528
mongo-id@1.0.9
36-
npm-mongo@6.16.1
37-
ordered-dict@1.2.0
3829
promise@1.0.0
3930
random@1.2.2
4031
react-fast-refresh@0.3.0

meteor/__mocks__/_setupMocks.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ jest.mock('nanoid', (...args) => require('./random').setup(args), { virtual: tru
1313
jest.mock('meteor/meteor', (...args) => require('./meteor').setup(args), { virtual: true })
1414
jest.mock('meteor/random', (...args) => require('./random').setup(args), { virtual: true })
1515
jest.mock('meteor/check', (...args) => require('./check').setup(args), { virtual: true })
16-
jest.mock('meteor/tracker', (...args) => require('./tracker').setup(args), { virtual: true })
1716
jest.mock('meteor/ejson', (...args) => require('./meteor-ejson').setup(args), { virtual: true })
1817

1918
jest.mock('meteor/mdg:validated-method', (...args) => require('./validated-method').setup(args), { virtual: true })

meteor/__mocks__/tracker.ts

Lines changed: 0 additions & 129 deletions
This file was deleted.

meteor/scripts/dev-mongo.mjs

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import fs from 'fs'
2+
import net from 'net'
3+
import { MongoMemoryServer } from 'mongodb-memory-server'
4+
import { MongoClient } from 'mongodb'
5+
6+
// Use the same replica-set name Meteor uses for its dev mongod, so the same data dir works on both this
7+
// branch (our dev-mongo) and an older branch (Meteor's bundled mongod)
8+
const REPLSET_NAME = 'meteor'
9+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
10+
11+
/**
12+
* Throw a clear error if `port` is already in use, so a conflict fails loudly instead of
13+
* mongodb-memory-server silently falling back to a random port.
14+
*/
15+
function assertPortFree(port) {
16+
return new Promise((resolve, reject) => {
17+
const tester = net
18+
.createServer()
19+
.once('error', (err) => {
20+
if (err.code === 'EADDRINUSE') {
21+
reject(
22+
new Error(
23+
`MongoDB dev port ${port} is already in use. Free that port, or set MONGO_PORT in .env to a free one.`
24+
)
25+
)
26+
} else {
27+
reject(err)
28+
}
29+
})
30+
.once('listening', () => tester.close(() => resolve()))
31+
.listen(port, '127.0.0.1')
32+
})
33+
}
34+
35+
/**
36+
* Bring up a single-node replica set on an already-running mongod (started with --replSet but NOT
37+
* initiated), and make it PRIMARY with the given `host`. Non-destructive to existing data:
38+
*
39+
* - Fresh data dir (no config) -> replSetInitiate with our host.
40+
* - Existing config that lists our host -> nothing; mongod becomes primary on its own.
41+
* - Existing config with a stale host -> replSetReconfig({force:true}) to our host.
42+
*
43+
* The stale-host case is normal: both our dev-mongo and Meteor's mongod reconfigure the member host to
44+
* their own port on startup. We do this via reconfig (not by clearing the config), which never
45+
* re-initiates the oplog and so never risks the data.
46+
*/
47+
async function ensureSingleNodePrimary(serverUri, host) {
48+
const client = new MongoClient(serverUri, { directConnection: true })
49+
await client.connect()
50+
try {
51+
const adb = client.db('admin')
52+
53+
let state = 'ok'
54+
try {
55+
await adb.command({ replSetGetStatus: 1 })
56+
} catch (e) {
57+
if (e.code === 94)
58+
state = 'uninitialized' // NotYetInitialized
59+
else if (e.code === 93)
60+
state = 'notmember' // InvalidReplicaSetConfig (we're not in the stored config)
61+
else throw e
62+
}
63+
64+
if (state === 'uninitialized') {
65+
await adb.command({ replSetInitiate: { _id: REPLSET_NAME, members: [{ _id: 0, host }] } })
66+
} else {
67+
const cfg = (await adb.command({ replSetGetConfig: 1 })).config
68+
const currentHost = cfg.members?.[0]?.host
69+
if (currentHost !== host || state === 'notmember') {
70+
cfg.members = [{ ...cfg.members[0], _id: 0, host }]
71+
cfg.version = (cfg.version || 0) + 1
72+
await adb.command({ replSetReconfig: cfg, force: true })
73+
}
74+
}
75+
76+
const deadline = Date.now() + 30000
77+
for (;;) {
78+
const hello = await adb.command({ hello: 1 })
79+
if (hello.isWritablePrimary) break
80+
if (Date.now() > deadline) throw new Error('MongoDB did not become primary within 30s')
81+
await sleep(200)
82+
}
83+
} finally {
84+
await client.close().catch(() => {})
85+
}
86+
}
87+
88+
/**
89+
* Spawn a local MongoDB for development using mongodb-memory-server.
90+
*
91+
* It runs as a single-node replica set (change streams - which the backend relies on - require a
92+
* replica set), and stores its data in `dbPath`. The data is persistent: stop() does not delete
93+
* `dbPath`, and bring-up never clears or re-initiates user data.
94+
*
95+
* @param {{ dbPath: string, port: number, dbName: string, version: string }} opts
96+
* @returns {Promise<{ uri: string, stop: () => Promise<void> }>}
97+
*/
98+
export async function startDevMongo({ dbPath, port, dbName, version }) {
99+
// Fail loudly on a port conflict rather than letting mongodb-memory-server pick a random port.
100+
await assertPortFree(port)
101+
102+
// mongodb-memory-server expects the data directory to exist.
103+
fs.mkdirSync(dbPath, { recursive: true })
104+
105+
// Start mongod as a replica-set member but WITHOUT mongodb-memory-server's auto-initiation.
106+
// We drive initiate/reconfig ourselves so we can adopt an existing data dir non-destructively.
107+
const server = await MongoMemoryServer.create({
108+
binary: { version },
109+
instance: { dbPath, port, storageEngine: 'wiredTiger', args: ['--replSet', REPLSET_NAME] },
110+
})
111+
112+
try {
113+
// Assert the requested port BEFORE bringing up the replica set, and derive the member host from the
114+
// actual bound port - so a reconfig can never install a host that doesn't match this node.
115+
const actualPort = Number(new URL(server.getUri()).port)
116+
if (actualPort !== port) {
117+
throw new Error(`MongoDB started on unexpected port ${actualPort} (requested ${port}).`)
118+
}
119+
await ensureSingleNodePrimary(server.getUri(), `127.0.0.1:${actualPort}`)
120+
} catch (e) {
121+
await server.stop({ doCleanup: false }).catch(() => {})
122+
throw e
123+
}
124+
125+
const uri = `mongodb://127.0.0.1:${port}/${dbName}?replicaSet=${REPLSET_NAME}`
126+
const stop = async () => {
127+
// doCleanup: false keeps the on-disk data dir so dev data persists between runs.
128+
await server.stop({ doCleanup: false })
129+
}
130+
131+
return { uri, stop }
132+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"install:packages": "cd packages && yarn install",
1414
"start": "yarn install && run install-and-build && run dev",
1515
"install-and-build": "node ./scripts/install-and-build.mjs",
16-
"dev": "node ./scripts/run.mjs",
16+
"dev": "node --env-file-if-exists=.env ./scripts/run.mjs",
1717
"restart:meteor": "node ./scripts/meteor-force-restart.js",
1818
"build:packages": "cd packages && run build",
1919
"test:packages": "cd packages && run test",

packages/documentation/docs/user-guide/installation/quick-install.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ sidebar_position: 20
88

99
### **Prerequisites**
1010

11-
* **Linux**: Install [Docker](https://docs.docker.com/install/linux/docker-ce/ubuntu/) and [docker-compose](https://www.digitalocean.com/community/tutorials/how-to-install-docker-compose-on-ubuntu-18-04).
12-
* **Windows**: Install [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) and use an *Ubuntu* terminal to install Docker and docker-compose.
11+
- **Linux**: Install [Docker](https://docs.docker.com/install/linux/docker-ce/ubuntu/) and [docker-compose](https://www.digitalocean.com/community/tutorials/how-to-install-docker-compose-on-ubuntu-18-04).
12+
- **Windows**: Install [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) and use an _Ubuntu_ terminal to install Docker and docker-compose.
1313

1414
### Installation
1515

@@ -55,7 +55,6 @@ services:
5555
environment:
5656
PORT: '3000'
5757
MONGO_URL: 'mongodb://db:27017/meteor'
58-
MONGO_OPLOG_URL: 'mongodb://db:27017/local'
5958
ROOT_URL: 'http://localhost:3000'
6059
SOFIE_STORE_PATH: '/mnt/sofie-store'
6160
networks:

0 commit comments

Comments
 (0)