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/BENCHMARKS.md +448 -0
- package/LICENSE +21 -0
- package/MIGRATION.md +69 -0
- package/README.md +255 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +112 -0
- package/dist/model.d.ts +27 -0
- package/dist/model.js +50 -0
- package/dist/shadow.d.ts +14 -0
- package/dist/shadow.js +53 -0
- package/index.d.ts +51 -0
- package/index.js +317 -0
- package/package.json +50 -0
- package/rumongo.linux-x64-gnu.node +0 -0
- package/worker/pool-worker.js +47 -0
- package/worker/pool.d.ts +42 -0
- package/worker/pool.js +100 -0
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 }))
|
package/worker/pool.d.ts
ADDED
|
@@ -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 }
|