rumongo 0.1.0

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/index.js ADDED
@@ -0,0 +1,317 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+ /* prettier-ignore */
4
+
5
+ /* auto-generated by NAPI-RS */
6
+
7
+ const { existsSync, readFileSync } = require('fs')
8
+ const { join } = require('path')
9
+
10
+ const { platform, arch } = process
11
+
12
+ let nativeBinding = null
13
+ let localFileExisted = false
14
+ let loadError = null
15
+
16
+ function isMusl() {
17
+ // For Node 10
18
+ if (!process.report || typeof process.report.getReport !== 'function') {
19
+ try {
20
+ const lddPath = require('child_process').execSync('which ldd').toString().trim()
21
+ return readFileSync(lddPath, 'utf8').includes('musl')
22
+ } catch (e) {
23
+ return true
24
+ }
25
+ } else {
26
+ const { glibcVersionRuntime } = process.report.getReport().header
27
+ return !glibcVersionRuntime
28
+ }
29
+ }
30
+
31
+ switch (platform) {
32
+ case 'android':
33
+ switch (arch) {
34
+ case 'arm64':
35
+ localFileExisted = existsSync(join(__dirname, 'rumongo.android-arm64.node'))
36
+ try {
37
+ if (localFileExisted) {
38
+ nativeBinding = require('./rumongo.android-arm64.node')
39
+ } else {
40
+ nativeBinding = require('rumongo-android-arm64')
41
+ }
42
+ } catch (e) {
43
+ loadError = e
44
+ }
45
+ break
46
+ case 'arm':
47
+ localFileExisted = existsSync(join(__dirname, 'rumongo.android-arm-eabi.node'))
48
+ try {
49
+ if (localFileExisted) {
50
+ nativeBinding = require('./rumongo.android-arm-eabi.node')
51
+ } else {
52
+ nativeBinding = require('rumongo-android-arm-eabi')
53
+ }
54
+ } catch (e) {
55
+ loadError = e
56
+ }
57
+ break
58
+ default:
59
+ throw new Error(`Unsupported architecture on Android ${arch}`)
60
+ }
61
+ break
62
+ case 'win32':
63
+ switch (arch) {
64
+ case 'x64':
65
+ localFileExisted = existsSync(
66
+ join(__dirname, 'rumongo.win32-x64-msvc.node')
67
+ )
68
+ try {
69
+ if (localFileExisted) {
70
+ nativeBinding = require('./rumongo.win32-x64-msvc.node')
71
+ } else {
72
+ nativeBinding = require('rumongo-win32-x64-msvc')
73
+ }
74
+ } catch (e) {
75
+ loadError = e
76
+ }
77
+ break
78
+ case 'ia32':
79
+ localFileExisted = existsSync(
80
+ join(__dirname, 'rumongo.win32-ia32-msvc.node')
81
+ )
82
+ try {
83
+ if (localFileExisted) {
84
+ nativeBinding = require('./rumongo.win32-ia32-msvc.node')
85
+ } else {
86
+ nativeBinding = require('rumongo-win32-ia32-msvc')
87
+ }
88
+ } catch (e) {
89
+ loadError = e
90
+ }
91
+ break
92
+ case 'arm64':
93
+ localFileExisted = existsSync(
94
+ join(__dirname, 'rumongo.win32-arm64-msvc.node')
95
+ )
96
+ try {
97
+ if (localFileExisted) {
98
+ nativeBinding = require('./rumongo.win32-arm64-msvc.node')
99
+ } else {
100
+ nativeBinding = require('rumongo-win32-arm64-msvc')
101
+ }
102
+ } catch (e) {
103
+ loadError = e
104
+ }
105
+ break
106
+ default:
107
+ throw new Error(`Unsupported architecture on Windows: ${arch}`)
108
+ }
109
+ break
110
+ case 'darwin':
111
+ localFileExisted = existsSync(join(__dirname, 'rumongo.darwin-universal.node'))
112
+ try {
113
+ if (localFileExisted) {
114
+ nativeBinding = require('./rumongo.darwin-universal.node')
115
+ } else {
116
+ nativeBinding = require('rumongo-darwin-universal')
117
+ }
118
+ break
119
+ } catch {}
120
+ switch (arch) {
121
+ case 'x64':
122
+ localFileExisted = existsSync(join(__dirname, 'rumongo.darwin-x64.node'))
123
+ try {
124
+ if (localFileExisted) {
125
+ nativeBinding = require('./rumongo.darwin-x64.node')
126
+ } else {
127
+ nativeBinding = require('rumongo-darwin-x64')
128
+ }
129
+ } catch (e) {
130
+ loadError = e
131
+ }
132
+ break
133
+ case 'arm64':
134
+ localFileExisted = existsSync(
135
+ join(__dirname, 'rumongo.darwin-arm64.node')
136
+ )
137
+ try {
138
+ if (localFileExisted) {
139
+ nativeBinding = require('./rumongo.darwin-arm64.node')
140
+ } else {
141
+ nativeBinding = require('rumongo-darwin-arm64')
142
+ }
143
+ } catch (e) {
144
+ loadError = e
145
+ }
146
+ break
147
+ default:
148
+ throw new Error(`Unsupported architecture on macOS: ${arch}`)
149
+ }
150
+ break
151
+ case 'freebsd':
152
+ if (arch !== 'x64') {
153
+ throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
154
+ }
155
+ localFileExisted = existsSync(join(__dirname, 'rumongo.freebsd-x64.node'))
156
+ try {
157
+ if (localFileExisted) {
158
+ nativeBinding = require('./rumongo.freebsd-x64.node')
159
+ } else {
160
+ nativeBinding = require('rumongo-freebsd-x64')
161
+ }
162
+ } catch (e) {
163
+ loadError = e
164
+ }
165
+ break
166
+ case 'linux':
167
+ switch (arch) {
168
+ case 'x64':
169
+ if (isMusl()) {
170
+ localFileExisted = existsSync(
171
+ join(__dirname, 'rumongo.linux-x64-musl.node')
172
+ )
173
+ try {
174
+ if (localFileExisted) {
175
+ nativeBinding = require('./rumongo.linux-x64-musl.node')
176
+ } else {
177
+ nativeBinding = require('rumongo-linux-x64-musl')
178
+ }
179
+ } catch (e) {
180
+ loadError = e
181
+ }
182
+ } else {
183
+ localFileExisted = existsSync(
184
+ join(__dirname, 'rumongo.linux-x64-gnu.node')
185
+ )
186
+ try {
187
+ if (localFileExisted) {
188
+ nativeBinding = require('./rumongo.linux-x64-gnu.node')
189
+ } else {
190
+ nativeBinding = require('rumongo-linux-x64-gnu')
191
+ }
192
+ } catch (e) {
193
+ loadError = e
194
+ }
195
+ }
196
+ break
197
+ case 'arm64':
198
+ if (isMusl()) {
199
+ localFileExisted = existsSync(
200
+ join(__dirname, 'rumongo.linux-arm64-musl.node')
201
+ )
202
+ try {
203
+ if (localFileExisted) {
204
+ nativeBinding = require('./rumongo.linux-arm64-musl.node')
205
+ } else {
206
+ nativeBinding = require('rumongo-linux-arm64-musl')
207
+ }
208
+ } catch (e) {
209
+ loadError = e
210
+ }
211
+ } else {
212
+ localFileExisted = existsSync(
213
+ join(__dirname, 'rumongo.linux-arm64-gnu.node')
214
+ )
215
+ try {
216
+ if (localFileExisted) {
217
+ nativeBinding = require('./rumongo.linux-arm64-gnu.node')
218
+ } else {
219
+ nativeBinding = require('rumongo-linux-arm64-gnu')
220
+ }
221
+ } catch (e) {
222
+ loadError = e
223
+ }
224
+ }
225
+ break
226
+ case 'arm':
227
+ if (isMusl()) {
228
+ localFileExisted = existsSync(
229
+ join(__dirname, 'rumongo.linux-arm-musleabihf.node')
230
+ )
231
+ try {
232
+ if (localFileExisted) {
233
+ nativeBinding = require('./rumongo.linux-arm-musleabihf.node')
234
+ } else {
235
+ nativeBinding = require('rumongo-linux-arm-musleabihf')
236
+ }
237
+ } catch (e) {
238
+ loadError = e
239
+ }
240
+ } else {
241
+ localFileExisted = existsSync(
242
+ join(__dirname, 'rumongo.linux-arm-gnueabihf.node')
243
+ )
244
+ try {
245
+ if (localFileExisted) {
246
+ nativeBinding = require('./rumongo.linux-arm-gnueabihf.node')
247
+ } else {
248
+ nativeBinding = require('rumongo-linux-arm-gnueabihf')
249
+ }
250
+ } catch (e) {
251
+ loadError = e
252
+ }
253
+ }
254
+ break
255
+ case 'riscv64':
256
+ if (isMusl()) {
257
+ localFileExisted = existsSync(
258
+ join(__dirname, 'rumongo.linux-riscv64-musl.node')
259
+ )
260
+ try {
261
+ if (localFileExisted) {
262
+ nativeBinding = require('./rumongo.linux-riscv64-musl.node')
263
+ } else {
264
+ nativeBinding = require('rumongo-linux-riscv64-musl')
265
+ }
266
+ } catch (e) {
267
+ loadError = e
268
+ }
269
+ } else {
270
+ localFileExisted = existsSync(
271
+ join(__dirname, 'rumongo.linux-riscv64-gnu.node')
272
+ )
273
+ try {
274
+ if (localFileExisted) {
275
+ nativeBinding = require('./rumongo.linux-riscv64-gnu.node')
276
+ } else {
277
+ nativeBinding = require('rumongo-linux-riscv64-gnu')
278
+ }
279
+ } catch (e) {
280
+ loadError = e
281
+ }
282
+ }
283
+ break
284
+ case 's390x':
285
+ localFileExisted = existsSync(
286
+ join(__dirname, 'rumongo.linux-s390x-gnu.node')
287
+ )
288
+ try {
289
+ if (localFileExisted) {
290
+ nativeBinding = require('./rumongo.linux-s390x-gnu.node')
291
+ } else {
292
+ nativeBinding = require('rumongo-linux-s390x-gnu')
293
+ }
294
+ } catch (e) {
295
+ loadError = e
296
+ }
297
+ break
298
+ default:
299
+ throw new Error(`Unsupported architecture on Linux: ${arch}`)
300
+ }
301
+ break
302
+ default:
303
+ throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
304
+ }
305
+
306
+ if (!nativeBinding) {
307
+ if (loadError) {
308
+ throw loadError
309
+ }
310
+ throw new Error(`Failed to load native binding`)
311
+ }
312
+
313
+ const { MongoClient, FindCursor, RawDoc } = nativeBinding
314
+
315
+ module.exports.MongoClient = MongoClient
316
+ module.exports.FindCursor = FindCursor
317
+ module.exports.RawDoc = RawDoc
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "rumongo",
3
+ "version": "0.1.0",
4
+ "description": "Rust-native MongoDB read driver for Node.js — faster reads than the official driver and Mongoose",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "license": "MIT",
8
+ "author": "Piyush Bhangale",
9
+ "keywords": [
10
+ "mongodb",
11
+ "mongo",
12
+ "driver",
13
+ "rust",
14
+ "napi",
15
+ "bson",
16
+ "mongoose",
17
+ "performance",
18
+ "native"
19
+ ],
20
+ "engines": {
21
+ "node": ">=16"
22
+ },
23
+ "napi": {
24
+ "name": "rumongo"
25
+ },
26
+ "files": [
27
+ "index.js",
28
+ "index.d.ts",
29
+ "dist/",
30
+ "worker/",
31
+ "rumongo.*.node",
32
+ "MIGRATION.md",
33
+ "BENCHMARKS.md",
34
+ "README.md"
35
+ ],
36
+ "scripts": {
37
+ "build": "napi build --platform --release",
38
+ "build:debug": "napi build --platform",
39
+ "build:ts": "tsc",
40
+ "typecheck": "tsc --noEmit",
41
+ "prepublishOnly": "napi build --platform --release && tsc",
42
+ "test:integration": "node --test __tests__/integration/"
43
+ },
44
+ "devDependencies": {
45
+ "@napi-rs/cli": "^2.18.4",
46
+ "mongodb": "^6.8.0",
47
+ "mongoose": "^8.5.0",
48
+ "typescript": "^5.4.5"
49
+ }
50
+ }
Binary file
@@ -0,0 +1,47 @@
1
+ // Worker-side entry for the rumongo worker pool.
2
+ // Loads the native addon, holds its own MongoClient, answers find requests.
3
+ // Runs on its OWN V8 isolate, so BSON->JS materialization here does not block
4
+ // the main event loop.
5
+
6
+ const { parentPort, workerData } = require('worker_threads')
7
+ const { MongoClient } = require('../index.js')
8
+
9
+ let client
10
+
11
+ async function handle(msg) {
12
+ const { id, op, db, coll, filter, opts } = msg
13
+ try {
14
+ if (op === 'close') {
15
+ if (client) await client.close()
16
+ parentPort.postMessage({ id, ok: true })
17
+ return
18
+ }
19
+ if (op === 'find') {
20
+ // Return one JSON string per doc (same shape the addon returns). The
21
+ // worker did the fetch + BSON decode on this isolate; main only receives.
22
+ const rows = await client.find(db, coll, filter, opts)
23
+ parentPort.postMessage({ id, rows })
24
+ return
25
+ }
26
+ if (op === 'reduce') {
27
+ // Generic "do the work in the worker" path: fetch + parse + run the
28
+ // caller's reducer here, return only the accumulated result to main.
29
+ // reducerSource is a function source string: (acc, doc) => acc
30
+ const reducer = (0, eval)(`(${msg.reducerSource})`)
31
+ const rows = await client.find(db, coll, filter, opts)
32
+ let acc = msg.init
33
+ for (const r of rows) acc = reducer(acc, JSON.parse(r))
34
+ parentPort.postMessage({ id, acc, count: rows.length })
35
+ return
36
+ }
37
+ parentPort.postMessage({ id, error: `unknown op ${op}` })
38
+ } catch (e) {
39
+ parentPort.postMessage({ id, error: e.message })
40
+ }
41
+ }
42
+
43
+ ;(async () => {
44
+ client = await MongoClient.connect(workerData.uri)
45
+ parentPort.on('message', handle)
46
+ parentPort.postMessage({ ready: true })
47
+ })().catch((e) => parentPort.postMessage({ fatal: e.message }))
@@ -0,0 +1,42 @@
1
+ // Type declarations for the opt-in worker pool (worker/pool.js).
2
+
3
+ export interface WorkerPoolOptions {
4
+ /** MongoDB connection string. */
5
+ uri: string
6
+ /** Number of worker threads. Default: cpus - 2 (min 2). */
7
+ size?: number
8
+ }
9
+
10
+ /**
11
+ * Opt-in pool of Node worker threads, each running the rumongo addon on its
12
+ * own V8 isolate. Use `reduce` to run work in a worker and keep the main event
13
+ * loop free. `find` returns rows to main (no jitter benefit — prefer the direct
14
+ * client for that).
15
+ */
16
+ export declare class WorkerPool {
17
+ static create(opts: WorkerPoolOptions): Promise<WorkerPool>
18
+
19
+ /** Query in a worker, return parsed rows to main (data crosses the boundary). */
20
+ find<T = Record<string, unknown>>(
21
+ db: string,
22
+ coll: string,
23
+ filter?: object,
24
+ opts?: object,
25
+ ): Promise<T[]>
26
+
27
+ /**
28
+ * Run a reducer over the query results INSIDE a worker; only the accumulator
29
+ * crosses back to main. The reducer is shipped as source, so it must be pure
30
+ * (no closure over main-thread variables): `(acc, doc) => acc`.
31
+ */
32
+ reduce<A>(
33
+ db: string,
34
+ coll: string,
35
+ filter: object,
36
+ opts: object,
37
+ reducer: (acc: A, doc: Record<string, unknown>) => A,
38
+ init: A,
39
+ ): Promise<{ acc: A; count: number }>
40
+
41
+ close(): Promise<void>
42
+ }
package/worker/pool.js ADDED
@@ -0,0 +1,100 @@
1
+ // Main-side worker pool for rumongo.
2
+ // Spawns N Node worker threads (each loads the addon + its own MongoClient),
3
+ // round-robins requests, matches replies by id. Keeps the main event loop free
4
+ // of BSON->JS work for the queries it dispatches.
5
+
6
+ const { Worker } = require('worker_threads')
7
+ const path = require('path')
8
+
9
+ class WorkerPool {
10
+ constructor() {
11
+ this._workers = []
12
+ this._next = 0
13
+ this._seq = 0
14
+ this._pending = new Map() // id -> {resolve, reject}
15
+ }
16
+
17
+ static async create({ uri, size = Math.max(2, require('os').cpus().length - 2) } = {}) {
18
+ const pool = new WorkerPool()
19
+ await Promise.all(
20
+ Array.from({ length: size }, () => pool._spawn(uri)),
21
+ )
22
+ return pool
23
+ }
24
+
25
+ _spawn(uri) {
26
+ return new Promise((resolve, reject) => {
27
+ const w = new Worker(path.join(__dirname, 'pool-worker.js'), { workerData: { uri } })
28
+ w.on('message', (msg) => {
29
+ if (msg.ready) return resolve()
30
+ if (msg.fatal) return reject(new Error(msg.fatal))
31
+ const p = this._pending.get(msg.id)
32
+ if (!p) return
33
+ this._pending.delete(msg.id)
34
+ if (msg.error) p.reject(new Error(msg.error))
35
+ else p.resolve(msg)
36
+ })
37
+ w.on('error', reject)
38
+ this._workers.push(w)
39
+ })
40
+ }
41
+
42
+ _dispatch(payload) {
43
+ const id = ++this._seq
44
+ const w = this._workers[this._next++ % this._workers.length]
45
+ return new Promise((resolve, reject) => {
46
+ this._pending.set(id, { resolve, reject })
47
+ w.postMessage({ id, ...payload })
48
+ })
49
+ }
50
+
51
+ /** find -> array of parsed JS objects (main parses the JSON strings). */
52
+ async find(db, coll, filter = {}, opts = {}) {
53
+ const msg = await this._dispatch({
54
+ op: 'find',
55
+ db,
56
+ coll,
57
+ filter: JSON.stringify(filter),
58
+ opts: JSON.stringify(opts),
59
+ })
60
+ return msg.rows.map((r) => JSON.parse(r))
61
+ }
62
+
63
+ /**
64
+ * Run a reducer over the query results INSIDE a worker; only the accumulated
65
+ * result crosses back to main. The reducer must be a pure function with no
66
+ * closure over main-thread state (it is shipped as source and rebuilt in the
67
+ * worker): `(acc, doc) => acc`.
68
+ *
69
+ * await pool.reduce('db','users', {active:true}, {}, (a,d)=>a+d.age, 0)
70
+ */
71
+ async reduce(db, coll, filter, opts, reducer, init) {
72
+ const msg = await this._dispatch({
73
+ op: 'reduce',
74
+ db,
75
+ coll,
76
+ filter: JSON.stringify(filter || {}),
77
+ opts: JSON.stringify(opts || {}),
78
+ reducerSource: reducer.toString(),
79
+ init,
80
+ })
81
+ return { acc: msg.acc, count: msg.count }
82
+ }
83
+
84
+ async close() {
85
+ // Send a close to EACH worker (not round-robin), then terminate.
86
+ await Promise.all(
87
+ this._workers.map(
88
+ (w) =>
89
+ new Promise((resolve) => {
90
+ const id = ++this._seq
91
+ this._pending.set(id, { resolve, reject: resolve })
92
+ w.postMessage({ id, op: 'close' })
93
+ }),
94
+ ),
95
+ )
96
+ await Promise.all(this._workers.map((w) => w.terminate()))
97
+ }
98
+ }
99
+
100
+ module.exports = { WorkerPool }