Object.defineProperty(target, key, descriptor)
}
+const cloneOwnPropertiesWithNullPrototype = (
+ source: object,
+ materializedKeys: readonly PropertyKey[],
+ materializeEnumerableProperties = false,
+ omittedKeys: readonly PropertyKey[] = []
+): Record<PropertyKey, unknown> => {
+ const clone = Object.create(null) as Record<PropertyKey, unknown>
+ for (const key of Reflect.ownKeys(source)) {
+ const descriptor = Object.getOwnPropertyDescriptor(source, key)
+ if (descriptor == null) {
+ continue
+ }
+ if (omittedKeys.includes(key)) {
+ if (materializeEnumerableProperties && descriptor.enumerable === true) {
+ Reflect.get(source, key)
+ }
+ continue
+ }
+ if (
+ materializedKeys.includes(key) ||
+ (materializeEnumerableProperties && descriptor.enumerable === true)
+ ) {
+ defineEnumerableDataProperty(clone, key, Reflect.get(source, key))
+ } else {
+ Object.setPrototypeOf(descriptor, null)
+ Object.defineProperty(clone, key, descriptor)
+ }
+ }
+ return clone
+}
+
+const threadWorkerOptionKeys: readonly PropertyKey[] = [
+ 'argv',
+ 'env',
+ 'eval',
+ 'execArgv',
+ 'name',
+ 'resourceLimits',
+ 'stderr',
+ 'stdin',
+ 'stdout',
+ 'trackUnmanagedFds',
+ 'transferList',
+ 'workerData',
+]
+
const cloneDenseExecArgv = (
execArgv: readonly string[],
optionName: string
export const buildThreadWorkerOptions = (
workerOptions: undefined | WorkerOptions
): WorkerOptions => {
- const normalizedWorkerOptions = withNullPrototype<WorkerOptions>(
- { env: SHARE_ENV },
- workerOptions
- )
+ const normalizedWorkerOptions =
+ workerOptions == null
+ ? withNullPrototype<WorkerOptions>()
+ : (cloneOwnPropertiesWithNullPrototype(
+ workerOptions,
+ threadWorkerOptionKeys,
+ true
+ ) as WorkerOptions)
+ if (!Object.hasOwn(normalizedWorkerOptions, 'env')) {
+ defineEnumerableDataProperty(normalizedWorkerOptions, 'env', SHARE_ENV)
+ }
if (Array.isArray(normalizedWorkerOptions.argv)) {
normalizedWorkerOptions.argv = cloneArrayOwnSlots(
normalizedWorkerOptions.argv
case WorkerTypes.cluster:
validateAndMaterializeClusterStdio(cluster.settings.stdio)
return cluster.fork(
- buildClusterWorkerEnvironment(process.env, opts.env)
+ buildClusterWorkerEnvironment(
+ process.env,
+ Object.hasOwn(opts, 'env') ? opts.env : undefined
+ )
) as unknown as Worker
case WorkerTypes.thread:
return new ThreadWorker(
filePath,
- buildThreadWorkerOptions(opts.workerOptions)
+ buildThreadWorkerOptions(
+ Object.hasOwn(opts, 'workerOptions') ? opts.workerOptions : undefined
+ )
) as unknown as Worker
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
super()
checkWorkerNodeArguments(type, filePath, opts)
this.worker = createWorker<Worker>(type, filePath, {
- env: opts.env,
- workerOptions: opts.workerOptions,
+ env: Object.hasOwn(opts, 'env') ? opts.env : undefined,
+ workerOptions: Object.hasOwn(opts, 'workerOptions')
+ ? opts.workerOptions
+ : undefined,
})
this.info = initWorkerInfo(this.worker)
this.usage = this.initWorkerUsage()
import { Worker as ClusterWorker } from 'node:cluster'
+import { once } from 'node:events'
import { PassThrough } from 'node:stream'
import { SHARE_ENV, Worker as ThreadWorker } from 'node:worker_threads'
import { describe, expect, it } from 'vitest'
expect(workerOptions.execArgv).not.toBe(execArgv)
})
+ it('preserves non-enumerable own thread worker options', () => {
+ let accessorCalls = 0
+ const accessorGetter = () => {
+ accessorCalls++
+ return 'ignored'
+ }
+ const environment = { EXPLICIT_ENV: 'worker' }
+ const execArgv = ['--no-warnings']
+ const explicitWorkerOptions = Object.create({ name: 'inherited' })
+ Object.defineProperties(explicitWorkerOptions, {
+ env: { value: environment },
+ execArgv: { value: execArgv },
+ ignored: { get: accessorGetter },
+ })
+
+ const workerOptions = buildThreadWorkerOptions(explicitWorkerOptions)
+
+ expect(Object.getPrototypeOf(workerOptions)).toBeNull()
+ expect(Object.hasOwn(workerOptions, 'env')).toBe(true)
+ expect(workerOptions.env).toBe(environment)
+ expect(Object.hasOwn(workerOptions, 'execArgv')).toBe(true)
+ expect(workerOptions.execArgv).toStrictEqual(execArgv)
+ expect(workerOptions.execArgv).not.toBe(execArgv)
+ expect(Object.hasOwn(workerOptions, 'name')).toBe(false)
+ expect(Object.getOwnPropertyDescriptor(workerOptions, 'ignored').get).toBe(
+ accessorGetter
+ )
+ expect(accessorCalls).toBe(0)
+ })
+
+ it('preserves non-enumerable restrictions at the thread worker boundary', async () => {
+ const environmentKey = 'POOLIFIER_EXPLICIT_WORKER_ENV'
+ const originalEnvironmentValue = process.env[environmentKey]
+ process.env[environmentKey] = 'parent'
+ const explicitWorkerOptions = {}
+ Object.defineProperties(explicitWorkerOptions, {
+ env: { value: { [environmentKey]: 'worker' } },
+ eval: { value: true },
+ execArgv: { value: ['--no-warnings'] },
+ })
+ let worker
+ try {
+ worker = new ThreadWorker(
+ `const { parentPort } = require('node:worker_threads')
+parentPort.postMessage({
+ envValue: process.env.${environmentKey},
+ execArgv: process.execArgv
+})`,
+ buildThreadWorkerOptions(explicitWorkerOptions)
+ )
+ const [message] = await once(worker, 'message')
+ expect(message).toStrictEqual({
+ envValue: 'worker',
+ execArgv: ['--no-warnings'],
+ })
+ } finally {
+ if (worker != null) {
+ await worker.terminate()
+ }
+ if (originalEnvironmentValue == null) {
+ Reflect.deleteProperty(process.env, environmentKey)
+ } else {
+ process.env[environmentKey] = originalEnvironmentValue
+ }
+ }
+ })
+
it('normalizes worker option arrays and resource limits by own slots', () => {
const argvValue = { value: 'argv' }
const argv = new Array(2)