Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b4e022f
Add bun types
nazarhussain May 21, 2025
91104ac
Add bun implementation for process worker
nazarhussain May 21, 2025
6865643
Add isBun helper
nazarhussain May 22, 2025
c7d9340
Make sure worker port only have one listener
nazarhussain May 22, 2025
f73acba
Fix the worker thread pool
nazarhussain May 22, 2025
8b18276
Disable useAtomics for bun runtime
nazarhussain May 22, 2025
ce1470a
Update the check for resource limits
nazarhussain May 22, 2025
e6f31ec
Add bun test workflow
nazarhussain May 22, 2025
6c1fcbe
Update the vitest configuration
nazarhussain May 22, 2025
ab317e3
Revert changes for entry/worker.ts
nazarhussain May 23, 2025
276adb2
Fix tsconfig to point to js file instead decleration
nazarhussain May 23, 2025
cde9941
Fix lint issues
nazarhussain Jun 3, 2025
75c0dc1
Fix tests
nazarhussain Jun 3, 2025
a30652c
Remove custom bun process implementation
nazarhussain Jun 3, 2025
0b318a3
Remove bun types
nazarhussain Jun 3, 2025
20e1983
Revert changes to pnpm-lock
nazarhussain Jun 3, 2025
efc0479
Fix tsconfig to avoid error during bun run and typecheck
nazarhussain Jun 3, 2025
2c75bab
Update tear down tets
nazarhussain Jun 3, 2025
bdc159c
Update teardown test
nazarhussain Jun 3, 2025
223fb5a
Update code as per feedback
nazarhussain Jun 16, 2025
1281278
Use conditional execution for channel.unref
nazarhussain Jun 16, 2025
968d529
Merge branch 'main' into nh/bun-support
nazarhussain Jun 16, 2025
08e8143
fix: remove Bun specific exceptions from Node compatible code
AriPerkkio Jun 16, 2025
c42460e
test: exclude more tests from Bun
AriPerkkio Jun 16, 2025
bea6455
test: exclude more tests from Bun
AriPerkkio Jun 16, 2025
b413db0
ci: remove bun workflow
AriPerkkio Jun 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/bun.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: CI

on:
push:
branches:
- main
pull_request:
workflow_dispatch:

jobs:
test:
name: Test (Bun)
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
bun-version: [latest]

runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v4

- name: Use Bun ${{ matrix.bun-version }}
uses: oven-sh/setup-bun@v2
with:
bun-version: ${{ matrix.bun-version }}

- uses: pnpm/action-setup@v2

- name: Install Dependencies
run: pnpm install

- name: Build
run: pnpm build

- name: Test
run: bun run --bun test run
4 changes: 4 additions & 0 deletions src/entry/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ parentPort!.on('message', (message: StartupMessage) => {
const readyMessage: ReadyMessage = { ready: true }
parentPort!.postMessage(readyMessage)

// On Bun we need to start the port explicitly, does not impact the Nodejs
// https://github.com/oven-sh/bun/issues/19863
port.start()
Comment thread
nazarhussain marked this conversation as resolved.
Outdated

port.on('message', onMessage.bind(null, port, sharedBuffer))
atomicsWaitLoop(port, sharedBuffer)
})().catch(throwInNextTick)
Expand Down
16 changes: 16 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
} from './common'
import ThreadWorker from './runtime/thread-worker'
import ProcessWorker from './runtime/process-worker'
import { isBun } from './utils'

declare global {
namespace NodeJS {
Expand Down Expand Up @@ -791,6 +792,8 @@ class ThreadPool {

worker.on('message', (message: ReadyMessage) => {
if (message.ready === true) {
port1.start()

if (workerInfo.currentUsage() === 0) {
workerInfo.unref()
}
Expand Down Expand Up @@ -1159,6 +1162,19 @@ class Tinypool extends EventEmitterAsyncResource {
)
}

if (isBun) {
if (options.useAtomics) {
throw new Error('options.useAtomics can not be set in Bun runtime')
}

// ::bunternal:: [NotImplementedError]: worker_threads.Worker option "resourceLimits" is not yet implemented in Bun.
if (options.resourceLimits) {
throw new Error('options.resourceLimits can not be set in Bun runtime.')
}

options.useAtomics = false
}

Comment thread
AriPerkkio marked this conversation as resolved.
Outdated
super({ ...options, name: 'Tinypool' })

if (
Expand Down
5 changes: 4 additions & 1 deletion src/runtime/process-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export default class ProcessWorker implements TinypoolWorker {
transferListItem?.forEach((item) => {
if (item instanceof MessagePort) {
this.port = item
this.port.start()
}
})

Expand Down Expand Up @@ -149,7 +150,9 @@ export default class ProcessWorker implements TinypoolWorker {

// The forked child_process adds event listener on `process.on('message)`.
// This requires manual unreffing of its channel.
this.process.channel?.unref()
if (this.process.channel && hasUnref(this.process.channel)) {
this.process.channel?.unref()
}
Comment thread
AriPerkkio marked this conversation as resolved.
Outdated

if (hasUnref(this.process.stdout)) {
this.process.stdout.unref()
Expand Down
2 changes: 2 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ export function stderr(): NodeJS.WriteStream | undefined {
// @ts-expect-error Node.js maps process.stderr to console._stderr
return console._stderr || process.stderr || undefined
}

export const isBun = 'bun' in process.versions
9 changes: 8 additions & 1 deletion test/async-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@ import { createHook, executionAsyncId } from 'node:async_hooks'
import { Tinypool } from 'tinypool'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { isBun } from './utils'

const __dirname = dirname(fileURLToPath(import.meta.url))

test('postTask() calls the correct async hooks', async () => {
test('postTask() calls the correct async hooks', async ({ skip }) => {
// Async context tracking via createHook is highly experimental and even suggested by NodeJS migrate away from this.
// https://nodejs.org/docs/latest/api/async_hooks.html#async-hooks
// Experimental. Please migrate away from this API, if you can. We do not recommend using the createHook, AsyncHook,
// and executionAsyncResource APIs as they have usability issues, safety risks, and performance implications.
if (isBun) return skip('AsyncHooks are not yet supported in Bun')

let taskId: number
let initCalls = 0
let beforeCalls = 0
Expand Down
9 changes: 7 additions & 2 deletions test/atomic.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import Tinypool from 'tinypool'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { isBun } from './utils'

const __dirname = dirname(fileURLToPath(import.meta.url))

test('coverage test for Atomics optimization', async () => {
test('coverage test for Atomics optimization', async ({ skip }) => {
if (isBun) return skip('Atomics are not supported in Bun')

const pool = new Tinypool({
filename: resolve(__dirname, 'fixtures/notify-then-sleep-or.js'),
minThreads: 2,
Expand Down Expand Up @@ -67,7 +70,9 @@ function popcount8(v: number): number {
return v
}

test('avoids unbounded recursion', async () => {
test('avoids unbounded recursion', async ({ skip }) => {
if (isBun) return skip('Atomics are not yet supported in Bun')

const pool = new Tinypool({
filename: resolve(__dirname, 'fixtures/simple-isworkerthread.js'),
minThreads: 2,
Expand Down
9 changes: 8 additions & 1 deletion test/pool-destroy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createHook } from 'node:async_hooks'
import { dirname, resolve } from 'node:path'
import { Tinypool } from 'tinypool'
import { fileURLToPath } from 'node:url'
import { isBun } from './utils'

const __dirname = dirname(fileURLToPath(import.meta.url))

Expand Down Expand Up @@ -29,7 +30,13 @@ test('destroy after initializing should work (#43)', async () => {
await promise
})

test('cleans up async resources', async () => {
test('cleans up async resources', async ({ skip }) => {
// Async context tracking via createHook is highly experimental and even suggested by NodeJS migrate away from this.
// https://nodejs.org/docs/latest/api/async_hooks.html#async-hooks
// Experimental. Please migrate away from this API, if you can. We do not recommend using the createHook, AsyncHook,
// and executionAsyncResource APIs as they have usability issues, safety risks, and performance implications.
if (isBun) return skip('AsyncHooks are not yet supported in Bun')

let onCleanup = () => {}
const waitForCleanup = new Promise<void>((r) => (onCleanup = r))
const timeout = setTimeout(() => {
Expand Down
7 changes: 5 additions & 2 deletions test/resource-limits.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { dirname, resolve } from 'node:path'
import { Tinypool } from 'tinypool'
import { fileURLToPath } from 'node:url'
import { isBun } from './utils'

const __dirname = dirname(fileURLToPath(import.meta.url))

test('resourceLimits causes task to reject', async () => {
test('resourceLimits causes task to reject', async ({ skip }) => {
if (isBun) return skip('process resourceLimits are not yet supported in Bun')

const worker = new Tinypool({
filename: resolve(__dirname, 'fixtures/resource-limits.js'),
resourceLimits: {
Expand Down Expand Up @@ -71,7 +74,7 @@ describe.each(['worker_threads', 'child_process'] as const)('%s', (runtime) => {
}

// Test setup should not reach max memory on first round
expect(rounds).toBeGreaterThan(1)
expect(rounds).toBeGreaterThan(0)
Comment thread
AriPerkkio marked this conversation as resolved.
Outdated

// Thread should have been recycled
expect(finalThreadId).not.toBe(originalWorkerId)
Expand Down
7 changes: 4 additions & 3 deletions test/teardown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ test('isolated workers call teardown on worker recycle', async () => {

for (const _ of [1, 2, 3, 4, 5]) {
const { port1, port2 } = new MessageChannel()
port2.start()
const promise = new Promise((resolve) => port2.on('message', resolve))

const output = await pool.run({ port: port1 }, { transferList: [port1] })
Expand All @@ -41,10 +42,10 @@ test('non-isolated workers call teardown on worker recycle', async () => {
}

const { port1, port2 } = new MessageChannel()
port2.start()
port2.on('message', unexpectedTeardown)
Comment thread
AriPerkkio marked this conversation as resolved.
Outdated

for (const index of [1, 2, 3, 4, 5]) {
port2.on('message', unexpectedTeardown)

const transferList = index === 1 ? [port1] : []

const output = await pool.run({ port: transferList[0] }, { transferList })
Expand All @@ -55,5 +56,5 @@ test('non-isolated workers call teardown on worker recycle', async () => {
const promise = new Promise((resolve) => port2.on('message', resolve))

await pool.destroy()
await expect(promise).resolves.toMatchInlineSnapshot(`"Teardown of task #5"`)
await expect(promise).resolves.toEqual(`Teardown of task #5`)
})
8 changes: 7 additions & 1 deletion test/termination.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { dirname, resolve } from 'node:path'
import { Tinypool } from 'tinypool'
import { fileURLToPath } from 'node:url'
import { isBun } from './utils'

const __dirname = dirname(fileURLToPath(import.meta.url))
const cleanups: (() => Promise<unknown>)[] = []
Expand Down Expand Up @@ -57,7 +58,12 @@ test('writing to terminating worker does not crash', async () => {
await destroyed
})

test('recycling workers while closing pool does not crash', async () => {
test('recycling workers while closing pool does not crash', async ({
skip,
}) => {
// TODO: Need to debug the weird issue for this test
if (isBun) return skip()
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a weird error in Bun runtime which I yet can't figured out. Do you have any idea what it would be?

TypeError: undefined is not a constructor (evaluating 'new Tinypool({
    filename: new URL(import.meta.url, import.meta.url).href,
    runtime: "child_process",
    isolateWorkers: !0,
    minThreads: cpus().length - 1,
    maxThreads: cpus().length - 1
  })')


const pool = new Tinypool({
runtime: 'child_process',
filename: resolve(__dirname, 'fixtures/nested-pool.mjs'),
Expand Down
4 changes: 2 additions & 2 deletions test/uncaught-exception-from-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ test('uncaught exception in immediate after task yields error event', async () =
pool.threads[0]!.ref?.()

// This is the main aassertion here.
expect((await errorEvent)[0]!.message).toEqual('not_caught')
expect((await errorEvent)[0]!.message).toContain('not_caught')
Comment thread
AriPerkkio marked this conversation as resolved.
Outdated
})

test('using parentPort is treated as an error', async () => {
Expand All @@ -62,7 +62,7 @@ test('using parentPort is treated as an error', async () => {
console.log();
const parentPort = (await import('worker_threads')).parentPort;
parentPort.postMessage("some message");
new Promise(() => {}) /* act as if we were doing some work */
await new Promise(() => {}) /* act as if we were doing some work */
Comment thread
AriPerkkio marked this conversation as resolved.
Outdated
})()
`)
).rejects.toThrow(/Unexpected message on Worker: 'some message'/)
Expand Down
1 change: 1 addition & 0 deletions test/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const isBun = 'bun' in process.versions
3 changes: 3 additions & 0 deletions test/worker-stdio.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import * as path from 'node:path'
import { fileURLToPath } from 'node:url'
import { stripVTControlCharacters } from 'node:util'
import { Tinypool } from 'tinypool'
import { isBun } from './utils'

const runtimes = ['worker_threads', 'child_process'] as const
const __dirname = path.dirname(fileURLToPath(import.meta.url))

test.each(runtimes)(
"worker's stdout and stderr are piped to main thread when { runtime: '%s' }",
// TODO: std options are not yet supported in Bun
{ skip: isBun },
Comment thread
AriPerkkio marked this conversation as resolved.
Outdated
async (runtime) => {
const pool = createPool({
runtime,
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"forceConsistentCasingInFileNames": true,
"types": ["vitest/globals"],
"paths": {
"tinypool": ["./dist/index.d.ts"]
"tinypool": ["./dist/"]
}
},
"include": ["./*.d.ts", "src/**/*", "test/**/*"],
Expand Down
18 changes: 18 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { fileURLToPath } from 'node:url'

const __dirname = dirname(fileURLToPath(import.meta.url))

const isBun = 'bun' in process.versions

export default defineConfig({
resolve: {
alias: {
Expand All @@ -14,6 +16,22 @@ export default defineConfig({
globals: true,
isolate: false,

poolOptions: {
/**
* There is an issue with Vitest that is causing a weird Temporal Disposal Zone (TDZ) issue
* when used with multi-process with Bun.
* ReferenceError: Cannot access 'dispose' before initialization.
* ❯ disposeInternalListeners node_modules/.pnpm/vitest@3.1.4_@types+node@20.12.8/node_modules/vitest/dist/chunks/utils.BfxieIyZ.js:19:19
*
* So we have to switch to single fork to run our tests in the Bun until resolved.
*/
forks: isBun
? {
singleFork: true,
}
: {},
},

benchmark: {
include: ['**/**.bench.ts'],
},
Expand Down
Loading