Compare commits

...

5 Commits

Author SHA1 Message Date
Claude Bot
e87c40cb7d fix: bundler plugins with different namespaces and same path now work correctly
Previously, when multiple plugins resolved different imports to the same
path but different namespaces, the bundler would incorrectly treat them
as the same module. This was because PathToSourceIndexMap used only the
path text as the cache key, ignoring the namespace.

This fix makes the cache namespace-aware by:
- Including namespace in the cache key for non-file namespaces
- Using "namespace:::::path" format to avoid collisions
- Updating all PathToSourceIndexMap operations to handle namespaces
- Making the resolve queue namespace-aware to prevent duplicate ParseTasks

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 01:05:19 +00:00
csvlad
4250ce6157 fix: vi typing in bun:test (#24248) 2025-11-04 08:27:30 -08:00
nkxxll
f8dce87f24 docs(bun-types): Replace depricated readableStreamToText in type docu… (#24372)
Co-authored-by: Alistair Smith <hi@alistair.sh>
2025-11-04 07:43:46 -08:00
Jarred Sumner
359f04d81f Improve NAPI property and element handling (#24358)
### What does this PR do?

Refactored NAPI property and element access to use inline methods and
improved error handling. Added comprehensive tests for default value
behavior and numeric string key operations in NAPI, ensuring correct
handling of missing properties, integer keys, and property deletion.
Updated TypeScript tests to cover new scenarios.

### How did you verify your code works?

Tests

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-11-04 03:21:07 -08:00
robobun
9ce2504554 fix(node:http): unref poll_ref on WebSocket upgrade to prevent CPU spin (#24271)
## Summary

Fixes 100% CPU usage on idle WebSocket servers between bun-v1.2.23 and
bun-v1.3.0.

Many users reported WebSocket server CPU usage jumping to 100% on idle
connections after upgrading to v1.3.0. Investigation revealed a missing
`poll_ref.unref()` call in the WebSocket upgrade path.

## Root Cause

In commit 625e537f5d (#23348), the `OnBeforeOpen` callback mechanism was
removed as part of refactoring the WebSocket upgrade process. However,
this callback contained a critical cleanup step:

```zig
defer ctx.this.poll_ref.unref(ctx.globalObject.bunVM());
```

When a `NodeHTTPResponse` is created, `poll_ref.ref()` is called (line
314) to keep the event loop alive while handling the HTTP request. After
a WebSocket upgrade, the HTTP response object is no longer relevant and
its `poll_ref` must be unref'd to indicate the request processing is
complete.

Without this unref, the event loop maintains an active reference even
after the upgrade completes, causing the CPU to spin at 100% waiting for
events on what should be an idle connection.

## Changes

- Added `poll_ref.unref()` call in `NodeHTTPResponse.upgrade()` after
setting the `upgraded` flag
- Added regression test to verify event loop properly exits after
WebSocket upgrade

## Test Plan

- [x] Code compiles successfully
- [x] Existing WebSocket tests pass
- [x] Manual testing confirms CPU usage returns to normal on idle
WebSocket connections

## Related Issues

Fixes issue reported by users between bun-v1.2.23 and bun-v1.3.0
regarding 100% CPU usage on idle WebSocket servers.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-11-03 23:27:26 -08:00
13 changed files with 647 additions and 43 deletions

View File

@@ -5791,11 +5791,11 @@ declare module "bun" {
* @category Process Management
*
* ```js
* const subprocess = Bun.spawn({
* const proc = Bun.spawn({
* cmd: ["echo", "hello"],
* stdout: "pipe",
* });
* const text = await readableStreamToText(subprocess.stdout);
* const text = await proc.stdout.text();
* console.log(text); // "hello\n"
* ```
*
@@ -5829,8 +5829,8 @@ declare module "bun" {
* Spawn a new process
*
* ```js
* const {stdout} = Bun.spawn(["echo", "hello"]);
* const text = await readableStreamToText(stdout);
* const proc = Bun.spawn(["echo", "hello"]);
* const text = await proc.stdout.text();
* console.log(text); // "hello\n"
* ```
*

View File

@@ -172,7 +172,7 @@ declare module "bun:test" {
/**
* Mock a module
*/
module: typeof mock.module;
mock: typeof mock.module;
/**
* Restore all mocks to their original implementation
*/

View File

@@ -193,6 +193,9 @@ pub fn upgrade(this: *NodeHTTPResponse, data_value: JSValue, sec_websocket_proto
if (this.raw_response) |raw_response| {
this.raw_response = null;
this.flags.upgraded = true;
// Unref the poll_ref since the socket is now upgraded to WebSocket
// and will have its own lifecycle management
this.poll_ref.unref(this.server.globalThis().bunVM());
_ = raw_response.upgrade(*ServerWebSocket, ws, websocket_key, sec_websocket_protocol_value, sec_websocket_extensions_value, upgrade_ctx);
}
return true;

View File

@@ -416,8 +416,7 @@ extern "C" napi_status napi_set_property(napi_env env, napi_value target,
JSValue jsValue = toJS(value);
// Ignoring the return value matches JS sloppy mode
(void)object->methodTable()->put(object, globalObject, identifier, jsValue, slot);
(void)object->putInline(globalObject, identifier, jsValue, slot);
NAPI_RETURN_SUCCESS_UNLESS_EXCEPTION(env);
}
@@ -432,12 +431,12 @@ extern "C" napi_status napi_set_element(napi_env env, napi_value object_,
JSValue value = toJS(value_);
NAPI_RETURN_EARLY_IF_FALSE(env, !object.isEmpty() && !value.isEmpty(), napi_invalid_arg);
auto globalObject = toJS(env);
JSObject* jsObject = object.getObject();
NAPI_RETURN_EARLY_IF_FALSE(env, jsObject, napi_array_expected);
jsObject->methodTable()->putByIndex(jsObject, toJS(env), index, value, false);
NAPI_RETURN_IF_EXCEPTION(env);
NAPI_RETURN_SUCCESS(env);
(void)jsObject->putByIndexInline(globalObject, index, value, false);
NAPI_RETURN_SUCCESS_UNLESS_EXCEPTION(env);
}
extern "C" napi_status napi_has_element(napi_env env, napi_value object_,
@@ -454,9 +453,9 @@ extern "C" napi_status napi_has_element(napi_env env, napi_value object_,
NAPI_RETURN_EARLY_IF_FALSE(env, jsObject, napi_array_expected);
bool has_property = jsObject->hasProperty(toJS(env), index);
NAPI_RETURN_IF_EXCEPTION(env);
*result = has_property;
NAPI_RETURN_SUCCESS_UNLESS_EXCEPTION(env);
NAPI_RETURN_SUCCESS(env);
}
extern "C" napi_status napi_has_property(napi_env env, napi_value object,
@@ -472,8 +471,13 @@ extern "C" napi_status napi_has_property(napi_env env, napi_value object,
NAPI_RETURN_IF_EXCEPTION(env);
auto keyProp = toJS(key);
*result = target->hasProperty(globalObject, keyProp.toPropertyKey(globalObject));
NAPI_RETURN_SUCCESS_UNLESS_EXCEPTION(env);
JSC::PropertyName name = keyProp.toPropertyKey(globalObject);
NAPI_RETURN_IF_EXCEPTION(env);
bool hasProperty = target->hasProperty(globalObject, name);
NAPI_RETURN_IF_EXCEPTION(env);
*result = hasProperty;
NAPI_RETURN_SUCCESS(env);
}
extern "C" napi_status napi_get_date_value(napi_env env, napi_value value, double* result)
@@ -507,8 +511,26 @@ extern "C" napi_status napi_get_property(napi_env env, napi_value object,
auto keyProp = toJS(key);
JSC::EnsureStillAliveScope ensureAlive2(keyProp);
*result = toNapi(target->get(globalObject, keyProp.toPropertyKey(globalObject)), globalObject);
NAPI_RETURN_SUCCESS_UNLESS_EXCEPTION(env);
PropertySlot slot(target, PropertySlot::InternalMethodType::Get);
auto propertyName = keyProp.toPropertyKey(globalObject);
NAPI_RETURN_IF_EXCEPTION(env);
const auto index = parseIndex(propertyName);
bool hasProperty = index ? target->getPropertySlot(globalObject, *index, slot)
: target->getNonIndexPropertySlot(globalObject, propertyName, slot);
NAPI_RETURN_IF_EXCEPTION(env);
if (!hasProperty) {
*result = toNapi(jsUndefined(), globalObject);
} else {
JSValue resultValue = slot.getValue(globalObject, propertyName);
NAPI_RETURN_IF_EXCEPTION(env);
*result = toNapi(resultValue, globalObject);
}
NAPI_RETURN_SUCCESS(env);
}
extern "C" napi_status napi_delete_property(napi_env env, napi_value object,
@@ -524,7 +546,11 @@ extern "C" napi_status napi_delete_property(napi_env env, napi_value object,
NAPI_RETURN_IF_EXCEPTION(env);
auto keyProp = toJS(key);
auto deleteResult = target->deleteProperty(globalObject, keyProp.toPropertyKey(globalObject));
auto name = JSC::PropertyName(keyProp.toPropertyKey(globalObject));
NAPI_RETURN_IF_EXCEPTION(env);
auto deleteResult = target->deleteProperty(globalObject, name);
NAPI_RETURN_IF_EXCEPTION(env);
if (result) [[likely]] {
@@ -550,8 +576,13 @@ extern "C" napi_status napi_has_own_property(napi_env env, napi_value object,
JSValue keyProp = toJS(key);
NAPI_RETURN_EARLY_IF_FALSE(env, keyProp.isString() || keyProp.isSymbol(), napi_name_expected);
*result = target->hasOwnProperty(globalObject, JSC::PropertyName(keyProp.toPropertyKey(globalObject)));
NAPI_RETURN_SUCCESS_UNLESS_EXCEPTION(env);
auto name = JSC::PropertyName(keyProp.toPropertyKey(globalObject));
NAPI_RETURN_IF_EXCEPTION(env);
bool hasOwnProperty = target->hasOwnProperty(globalObject, name);
NAPI_RETURN_IF_EXCEPTION(env);
*result = hasOwnProperty;
NAPI_RETURN_SUCCESS(env);
}
extern "C" napi_status napi_set_named_property(napi_env env, napi_value object,
@@ -575,11 +606,10 @@ extern "C" napi_status napi_set_named_property(napi_env env, napi_value object,
JSC::EnsureStillAliveScope ensureAlive2(target);
auto nameStr = WTF::String::fromUTF8({ utf8name, strlen(utf8name) });
auto identifier = JSC::Identifier::fromString(vm, WTFMove(nameStr));
auto name = JSC::PropertyName(JSC::Identifier::fromString(vm, WTFMove(nameStr)));
PutPropertySlot slot(target, false);
target->methodTable()->put(target, globalObject, identifier, jsValue, slot);
target->putInline(globalObject, name, jsValue, slot);
NAPI_RETURN_SUCCESS_UNLESS_EXCEPTION(env);
}

View File

@@ -633,6 +633,25 @@ pub fn unrefConcurrently(this: *EventLoop) void {
this.wakeup();
}
/// Testing API to expose event loop state
pub fn getActiveTasks(globalObject: *jsc.JSGlobalObject, _: *jsc.CallFrame) bun.JSError!jsc.JSValue {
const vm = globalObject.bunVM();
const event_loop = vm.event_loop;
const result = jsc.JSValue.createEmptyObject(globalObject, 3);
result.put(globalObject, jsc.ZigString.static("activeTasks"), jsc.JSValue.jsNumber(vm.active_tasks));
result.put(globalObject, jsc.ZigString.static("concurrentRef"), jsc.JSValue.jsNumber(event_loop.concurrent_ref.load(.seq_cst)));
// Get num_polls from uws loop (POSIX) or active_handles from libuv (Windows)
const num_polls: i32 = if (Environment.isWindows)
@intCast(bun.windows.libuv.Loop.get().active_handles)
else
uws.Loop.get().num_polls;
result.put(globalObject, jsc.ZigString.static("numPolls"), jsc.JSValue.jsNumber(num_polls));
return result;
}
pub const AnyEventLoop = @import("./event_loop/AnyEventLoop.zig").AnyEventLoop;
pub const ConcurrentPromiseTask = @import("./event_loop/ConcurrentPromiseTask.zig").ConcurrentPromiseTask;
pub const WorkTask = @import("./event_loop/WorkTask.zig").WorkTask;

View File

@@ -5,38 +5,62 @@ const PathToSourceIndexMap = @This();
/// We assume it's arena allocated.
map: Map = .{},
const Map = bun.StringHashMapUnmanaged(Index.Int);
/// HashMap context that makes path lookups namespace-aware.
/// For file namespace, uses only path.text for backwards compatibility.
/// For other namespaces, combines namespace and path in the hash.
const PathHashContext = struct {
pub fn hash(_: @This(), path: Fs.Path) u64 {
return path.hashKey();
}
pub fn eql(_: @This(), a: Fs.Path, b: Fs.Path) bool {
// For file namespace, only compare path text
if (a.isFile() and b.isFile()) {
return bun.strings.eqlLong(a.text, b.text, true);
}
// For non-file namespaces, compare both namespace and path
return bun.strings.eqlLong(a.namespace, b.namespace, true) and
bun.strings.eqlLong(a.text, b.text, true);
}
};
const Map = std.HashMapUnmanaged(Fs.Path, Index.Int, PathHashContext, std.hash_map.default_max_load_percentage);
pub fn getPath(this: *const PathToSourceIndexMap, path: *const Fs.Path) ?Index.Int {
return this.get(path.text);
return this.map.get(path.*);
}
pub fn get(this: *const PathToSourceIndexMap, text: []const u8) ?Index.Int {
return this.map.get(text);
const file_path = Fs.Path.init(text);
return this.map.get(file_path);
}
pub fn putPath(this: *PathToSourceIndexMap, allocator: std.mem.Allocator, path: *const Fs.Path, value: Index.Int) bun.OOM!void {
try this.map.put(allocator, path.text, value);
try this.map.put(allocator, path.*, value);
}
pub fn put(this: *PathToSourceIndexMap, allocator: std.mem.Allocator, text: []const u8, value: Index.Int) bun.OOM!void {
try this.map.put(allocator, text, value);
const file_path = Fs.Path.init(text);
try this.map.put(allocator, file_path, value);
}
pub fn getOrPutPath(this: *PathToSourceIndexMap, allocator: std.mem.Allocator, path: *const Fs.Path) bun.OOM!Map.GetOrPutResult {
return this.getOrPut(allocator, path.text);
return try this.map.getOrPut(allocator, path.*);
}
pub fn getOrPut(this: *PathToSourceIndexMap, allocator: std.mem.Allocator, text: []const u8) bun.OOM!Map.GetOrPutResult {
return try this.map.getOrPut(allocator, text);
const file_path = Fs.Path.init(text);
return try this.map.getOrPut(allocator, file_path);
}
pub fn remove(this: *PathToSourceIndexMap, text: []const u8) bool {
return this.map.remove(text);
const file_path = Fs.Path.init(text);
return this.map.remove(file_path);
}
pub fn removePath(this: *PathToSourceIndexMap, path: *const Fs.Path) bool {
return this.remove(path.text);
return this.map.remove(path.*);
}
const std = @import("std");

View File

@@ -657,7 +657,7 @@ pub const BundleV2 = struct {
const entry = bun.handleOom(this.pathToSourceIndexMap(target).getOrPut(this.allocator(), path.text));
if (!entry.found_existing) {
path.* = bun.handleOom(this.pathWithPrettyInitialized(path.*, target));
entry.key_ptr.* = path.text;
entry.key_ptr.* = path.*;
const loader: Loader = brk: {
const record: *ImportRecord = &this.graph.ast.items(.import_records)[import_record.importer_source_index].slice()[import_record.import_record_index];
if (record.loader) |out_loader| {
@@ -699,9 +699,9 @@ pub const BundleV2 = struct {
.browser => .{ this.pathToSourceIndexMap(this.transpiler.options.target), this.pathToSourceIndexMap(.bake_server_components_ssr) },
.bake_server_components_ssr => .{ this.pathToSourceIndexMap(this.transpiler.options.target), this.pathToSourceIndexMap(.browser) },
};
bun.handleOom(a.put(this.allocator(), entry.key_ptr.*, entry.value_ptr.*));
bun.handleOom(a.putPath(this.allocator(), entry.key_ptr, entry.value_ptr.*));
if (this.framework.?.server_components.?.separate_ssr_graph)
bun.handleOom(b.put(this.allocator(), entry.key_ptr.*, entry.value_ptr.*));
bun.handleOom(b.putPath(this.allocator(), entry.key_ptr, entry.value_ptr.*));
}
} else {
out_source_index = Index.init(entry.value_ptr.*);
@@ -736,7 +736,7 @@ pub const BundleV2 = struct {
path = bun.handleOom(this.pathWithPrettyInitialized(path, target));
path.assertPrettyIsValid();
entry.key_ptr.* = path.text;
entry.key_ptr.* = path;
entry.value_ptr.* = source_index.get();
bun.handleOom(this.graph.ast.append(this.allocator(), JSAst.empty));
@@ -798,7 +798,7 @@ pub const BundleV2 = struct {
path.* = bun.handleOom(this.pathWithPrettyInitialized(path.*, target));
path.assertPrettyIsValid();
entry.key_ptr.* = path.text;
entry.key_ptr.* = path.*;
entry.value_ptr.* = source_index.get();
bun.handleOom(this.graph.ast.append(this.allocator(), JSAst.empty));
@@ -2455,7 +2455,8 @@ pub const BundleV2 = struct {
if (!existing.found_existing) {
this.free_list.appendSlice(&.{ result.namespace, result.path }) catch {};
path = bun.handleOom(this.pathWithPrettyInitialized(path, resolve.import_record.original_target));
existing.key_ptr.* = path.text;
// Update the key to use the arena-allocated path strings
existing.key_ptr.* = path;
// We need to parse this
const source_index = Index.init(@as(u32, @intCast(this.graph.ast.len)));
@@ -3429,7 +3430,7 @@ pub const BundleV2 = struct {
const is_html_entrypoint = import_record_loader == .html and target.isServerSide() and this.transpiler.options.dev_server == null;
if (this.pathToSourceIndexMap(target).get(path.text)) |id| {
if (this.pathToSourceIndexMap(target).getPath(path)) |id| {
if (this.transpiler.options.dev_server != null and loader != .html) {
import_record.path = this.graph.input_files.items(.source)[id].path;
} else {
@@ -3442,7 +3443,13 @@ pub const BundleV2 = struct {
import_record.kind = .html_manifest;
}
const resolve_entry = resolve_queue.getOrPut(path.text) catch |err| bun.handleOom(err);
// Generate namespace-aware key for the resolve queue
const resolve_key = if (path.isFile())
path.text
else
std.fmt.allocPrint(this.allocator(), "{s}:::::{s}", .{ path.namespace, path.text }) catch |err| bun.handleOom(err);
const resolve_entry = resolve_queue.getOrPut(resolve_key) catch |err| bun.handleOom(err);
if (resolve_entry.found_existing) {
import_record.path = resolve_entry.value_ptr.*.path;
continue;
@@ -3451,7 +3458,11 @@ pub const BundleV2 = struct {
path.* = bun.handleOom(this.pathWithPrettyInitialized(path.*, target));
import_record.path = path.*;
resolve_entry.key_ptr.* = path.text;
// Update key to use the arena-allocated path after pathWithPrettyInitialized
resolve_entry.key_ptr.* = if (path.isFile())
path.text
else
std.fmt.allocPrint(this.allocator(), "{s}:::::{s}", .{ path.namespace, path.text }) catch |err| bun.handleOom(err);
debug("created ParseTask: {s}", .{path.text});
const resolve_task = bun.handleOom(bun.default_allocator.create(ParseTask));
resolve_task.* = ParseTask.init(&resolve_result, Index.invalid, this);

View File

@@ -211,6 +211,9 @@ export const structuredCloneAdvanced: (
export const lsanDoLeakCheck = $newCppFunction("InternalForTesting.cpp", "jsFunction_lsanDoLeakCheck", 1);
export const getEventLoopStats: () => { activeTasks: number; concurrentRef: number; numPolls: number } =
$newZigFunction("event_loop.zig", "getActiveTasks", 0);
export const hostedGitInfo = {
parseUrl: $newZigFunction("hosted_git_info.zig", "TestingAPIs.jsParseUrl", 1),
fromUrl: $newZigFunction("hosted_git_info.zig", "TestingAPIs.jsFromUrl", 1),

View File

@@ -301,5 +301,73 @@ describe("bundler", () => {
};
});
}
// Test that onLoad is called for different namespaces with same path
itBundled("plugin/NamespaceOnLoadCalled", ({ root }) => {
const callOrder: string[] = [];
return {
files: {
"index.ts": /* ts */ `
import { value1 } from "module1";
import { value2 } from "module2";
console.log(value1, value2);
`,
},
plugins: [
{
name: "pluginA",
setup(builder) {
// Resolve module1 to namespace plugin-a
builder.onResolve({ filter: /^module1$/ }, args => {
callOrder.push("pluginA-resolve");
return {
path: "shared-path.js",
namespace: "plugin-a",
};
});
},
},
{
name: "pluginB",
setup(builder) {
// Resolve module2 to namespace plugin-b
builder.onResolve({ filter: /^module2$/ }, args => {
callOrder.push("pluginB-resolve");
return {
path: "shared-path.js",
namespace: "plugin-b",
};
});
// Handle onLoad for namespace plugin-a
builder.onLoad({ filter: /.*/, namespace: "plugin-a" }, args => {
callOrder.push("pluginB-load-a");
return {
contents: 'export const value1 = "from plugin-a namespace";',
loader: "js",
};
});
// Handle onLoad for namespace plugin-b
builder.onLoad({ filter: /.*/, namespace: "plugin-b" }, args => {
callOrder.push("pluginB-load-b");
return {
contents: 'export const value2 = "from plugin-b namespace";',
loader: "js",
};
});
},
},
],
run: {
stdout: "from plugin-a namespace from plugin-b namespace",
},
onAfterBundle() {
// Both onLoad callbacks should have been called with their respective namespaces
expect(callOrder).toEqual(["pluginA-resolve", "pluginB-resolve", "pluginB-load-a", "pluginB-load-b"]);
},
};
});
});
});

View File

@@ -1,10 +1,65 @@
import { expect, test } from "bun:test";
import { tls as options } from "harness";
import { bunEnv, bunExe, tls as options } from "harness";
import https from "https";
import type { AddressInfo } from "node:net";
import tls from "tls";
import { WebSocketServer } from "ws";
test("should not crash when closing sockets after upgrade", async () => {
test.concurrent("WebSocket upgrade should unref poll_ref from response", async () => {
// Regression test for bug where poll_ref was not unref'd on WebSocket upgrade
// The bug: NodeHTTPResponse.poll_ref stayed active after upgrade
// This test verifies activeTasks is correctly decremented after upgrade
const script = /* js */ `
const http = require("http");
const { WebSocketServer } = require("ws");
const { getEventLoopStats } = require("bun:internal-for-testing");
const server = http.createServer();
const wsServer = new WebSocketServer({ server });
let initialStats;
process.exitCode = 1;
wsServer.on("connection", (ws) => {
// After WebSocket upgrade completes, check active tasks
const stats = getEventLoopStats();
ws.close();
wsServer.close();
server.close();
// With the bug: poll_ref from NodeHTTPResponse stays active (activeTasks = 1)
// With the fix: poll_ref.unref() was called on upgrade (activeTasks should be 0)
if (stats.activeTasks !== initialStats.activeTasks) {
console.error("BUG_DETECTED: activeTasks=" + stats.activeTasks + " (expected 0 after upgrade)");
process.exit(1);
}
process.exitCode = 0;
});
initialStats = getEventLoopStats();
server.listen(0, "127.0.0.1", () => {
const port = server.address().port;
const ws = new WebSocket("ws://127.0.0.1:" + port);
});
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", script],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
// Should exit cleanly without detecting the bug
expect(stderr).not.toContain("BUG_DETECTED");
expect(stderr).toBe("");
expect(exitCode).toBe(0);
});
test.concurrent("should not crash when closing sockets after upgrade", async () => {
const { promise, resolve } = Promise.withResolvers();
let http_sockets: tls.TLSSocket[] = [];

View File

@@ -554,6 +554,386 @@ static napi_value test_is_typedarray(const Napi::CallbackInfo &info) {
return ok(env);
}
static napi_value test_napi_get_default_values(const Napi::CallbackInfo &info) {
napi_env env = info.Env();
#ifndef _WIN32
BlockingStdoutScope stdout_scope;
#endif
napi_value obj;
NODE_API_CALL(env, napi_create_object(env, &obj));
// Test 1: Get property that doesn't exist (should return undefined)
napi_value unknown_key;
NODE_API_CALL(env, napi_create_string_utf8(env, "nonexistent",
NAPI_AUTO_LENGTH, &unknown_key));
napi_value result;
napi_status get_status = napi_get_property(env, obj, unknown_key, &result);
if (get_status == napi_ok) {
napi_valuetype result_type;
napi_status type_status = napi_typeof(env, result, &result_type);
if (type_status == napi_ok && result_type == napi_undefined) {
printf("PASS: napi_get_property for unknown key returned undefined\n");
} else {
printf("FAIL: napi_get_property for unknown key returned type %d "
"(expected napi_undefined)\n",
result_type);
}
} else {
printf("FAIL: napi_get_property for unknown key failed with status %d\n",
get_status);
}
// Test 2: Get element at index that doesn't exist on array
napi_value array;
NODE_API_CALL(env, napi_create_array_with_length(env, 2, &array));
napi_value element_result;
napi_status element_status = napi_get_element(env, array, 5, &element_result);
if (element_status == napi_ok) {
napi_valuetype element_type;
napi_status element_type_status =
napi_typeof(env, element_result, &element_type);
if (element_type_status == napi_ok && element_type == napi_undefined) {
printf("PASS: napi_get_element for out-of-bounds index returned "
"undefined\n");
} else {
printf("FAIL: napi_get_element for out-of-bounds index returned type %d "
"(expected napi_undefined)\n",
element_type);
}
} else {
printf("FAIL: napi_get_element for out-of-bounds index failed with status "
"%d\n",
element_status);
}
// Test 3: Get named property that doesn't exist
napi_value named_result;
napi_status named_status =
napi_get_named_property(env, obj, "missing_prop", &named_result);
if (named_status == napi_ok) {
napi_valuetype named_type;
napi_status named_type_status = napi_typeof(env, named_result, &named_type);
if (named_type_status == napi_ok && named_type == napi_undefined) {
printf("PASS: napi_get_named_property for unknown property returned "
"undefined\n");
} else {
printf("FAIL: napi_get_named_property for unknown property returned type "
"%d (expected napi_undefined)\n",
named_type);
}
} else {
printf("FAIL: napi_get_named_property for unknown property failed with "
"status %d\n",
named_status);
}
// Test 4: Set a property and verify we can get it back
napi_value test_key;
napi_value test_value;
NODE_API_CALL(env, napi_create_string_utf8(env, "test_key", NAPI_AUTO_LENGTH,
&test_key));
NODE_API_CALL(env, napi_create_int32(env, 42, &test_value));
NODE_API_CALL(env, napi_set_property(env, obj, test_key, test_value));
napi_value retrieved_value;
NODE_API_CALL(env, napi_get_property(env, obj, test_key, &retrieved_value));
int32_t retrieved_int;
napi_status int_status =
napi_get_value_int32(env, retrieved_value, &retrieved_int);
if (int_status == napi_ok && retrieved_int == 42) {
printf("PASS: napi_get_property correctly retrieved set value: %d\n",
retrieved_int);
} else {
printf("FAIL: napi_get_property did not retrieve correct value (got %d, "
"expected 42)\n",
retrieved_int);
}
// Test 5: Use integer as property key (should be converted to string)
napi_value int_key;
napi_value int_key_value;
NODE_API_CALL(env, napi_create_int32(env, 123, &int_key));
NODE_API_CALL(env, napi_create_string_utf8(env, "integer_key_value",
NAPI_AUTO_LENGTH, &int_key_value));
// Set property using integer key
napi_status int_key_set_status =
napi_set_property(env, obj, int_key, int_key_value);
if (int_key_set_status == napi_ok) {
printf("PASS: napi_set_property with integer key succeeded\n");
// Try to get it back using the same integer key
napi_value int_key_result;
napi_status int_key_get_status =
napi_get_property(env, obj, int_key, &int_key_result);
if (int_key_get_status == napi_ok) {
// Check if we got back a string
napi_valuetype int_key_result_type;
napi_status int_key_type_status =
napi_typeof(env, int_key_result, &int_key_result_type);
if (int_key_type_status == napi_ok &&
int_key_result_type == napi_string) {
char buffer[256];
size_t copied;
napi_status str_status = napi_get_value_string_utf8(
env, int_key_result, buffer, sizeof(buffer), &copied);
if (str_status == napi_ok && strcmp(buffer, "integer_key_value") == 0) {
printf("PASS: napi_get_property with integer key retrieved correct "
"value: %s\n",
buffer);
} else {
printf("FAIL: napi_get_property with integer key retrieved wrong "
"value: %s\n",
buffer);
}
} else {
printf("FAIL: napi_get_property with integer key returned type %d "
"(expected string)\n",
int_key_result_type);
}
} else {
printf("FAIL: napi_get_property with integer key failed with status %d\n",
int_key_get_status);
}
// Also try to get it using string "123"
napi_value string_123_key;
NODE_API_CALL(env, napi_create_string_utf8(env, "123", NAPI_AUTO_LENGTH,
&string_123_key));
napi_value string_key_result;
napi_status string_key_get_status =
napi_get_property(env, obj, string_123_key, &string_key_result);
if (string_key_get_status == napi_ok) {
napi_valuetype string_key_result_type;
napi_status string_key_type_status =
napi_typeof(env, string_key_result, &string_key_result_type);
if (string_key_type_status == napi_ok &&
string_key_result_type == napi_string) {
char buffer2[256];
size_t copied2;
napi_status str_status2 = napi_get_value_string_utf8(
env, string_key_result, buffer2, sizeof(buffer2), &copied2);
if (str_status2 == napi_ok &&
strcmp(buffer2, "integer_key_value") == 0) {
printf("PASS: napi_get_property with string '123' key also retrieved "
"correct value: %s\n",
buffer2);
} else {
printf("FAIL: napi_get_property with string '123' key retrieved "
"wrong value: %s\n",
buffer2);
}
} else {
printf("FAIL: napi_get_property with string '123' key returned type %d "
"(expected string)\n",
string_key_result_type);
}
} else {
printf("FAIL: napi_get_property with string '123' key failed with status "
"%d\n",
string_key_get_status);
}
} else {
printf("FAIL: napi_set_property with integer key failed with status %d\n",
int_key_set_status);
}
return ok(env);
}
static napi_value
test_napi_numeric_string_keys(const Napi::CallbackInfo &info) {
napi_env env = info.Env();
#ifndef _WIN32
BlockingStdoutScope stdout_scope;
#endif
napi_value obj;
NODE_API_CALL(env, napi_create_object(env, &obj));
// Test setting property with numeric string key "0"
napi_value value_123;
NODE_API_CALL(env, napi_create_int32(env, 123, &value_123));
napi_status set_status = napi_set_named_property(env, obj, "0", value_123);
if (set_status == napi_ok) {
printf("PASS: napi_set_named_property with key '0' succeeded\n");
} else {
printf("FAIL: napi_set_named_property with key '0' failed: %d\n",
set_status);
}
// Test has property with numeric string key "0"
bool has_prop;
napi_status has_status = napi_has_named_property(env, obj, "0", &has_prop);
if (has_status == napi_ok && has_prop) {
printf("PASS: napi_has_named_property with key '0' returned true\n");
} else {
printf("FAIL: napi_has_named_property with key '0' failed or returned "
"false: status=%d, has=%s\n",
has_status, has_prop ? "true" : "false");
}
// Test getting property with numeric string key "0"
napi_value retrieved_value;
napi_status get_status =
napi_get_named_property(env, obj, "0", &retrieved_value);
if (get_status == napi_ok) {
int32_t retrieved_int;
napi_status int_status =
napi_get_value_int32(env, retrieved_value, &retrieved_int);
if (int_status == napi_ok && retrieved_int == 123) {
printf("PASS: napi_get_named_property with key '0' returned correct "
"value: %d\n",
retrieved_int);
} else {
printf("FAIL: napi_get_named_property with key '0' returned wrong value: "
"status=%d, value=%d\n",
int_status, retrieved_int);
}
} else {
printf("FAIL: napi_get_named_property with key '0' failed: %d\n",
get_status);
}
// Test with another numeric string key "1"
napi_value value_456;
NODE_API_CALL(env, napi_create_int32(env, 456, &value_456));
set_status = napi_set_named_property(env, obj, "1", value_456);
if (set_status == napi_ok) {
printf("PASS: napi_set_named_property with key '1' succeeded\n");
} else {
printf("FAIL: napi_set_named_property with key '1' failed: %d\n",
set_status);
}
has_status = napi_has_named_property(env, obj, "1", &has_prop);
if (has_status == napi_ok && has_prop) {
printf("PASS: napi_has_named_property with key '1' returned true\n");
} else {
printf("FAIL: napi_has_named_property with key '1' failed or returned "
"false: status=%d, has=%s\n",
has_status, has_prop ? "true" : "false");
}
get_status = napi_get_named_property(env, obj, "1", &retrieved_value);
if (get_status == napi_ok) {
int32_t retrieved_int;
napi_status int_status =
napi_get_value_int32(env, retrieved_value, &retrieved_int);
if (int_status == napi_ok && retrieved_int == 456) {
printf("PASS: napi_get_named_property with key '1' returned correct "
"value: %d\n",
retrieved_int);
} else {
printf("FAIL: napi_get_named_property with key '1' returned wrong value: "
"status=%d, value=%d\n",
int_status, retrieved_int);
}
} else {
printf("FAIL: napi_get_named_property with key '1' failed: %d\n",
get_status);
}
// Test with napi_get_property using numeric string keys
napi_value key_0, key_1;
NODE_API_CALL(env,
napi_create_string_utf8(env, "0", NAPI_AUTO_LENGTH, &key_0));
NODE_API_CALL(env,
napi_create_string_utf8(env, "1", NAPI_AUTO_LENGTH, &key_1));
napi_value prop_value;
napi_status prop_status = napi_get_property(env, obj, key_0, &prop_value);
if (prop_status == napi_ok) {
int32_t prop_int;
napi_status int_status = napi_get_value_int32(env, prop_value, &prop_int);
if (int_status == napi_ok && prop_int == 123) {
printf(
"PASS: napi_get_property with key '0' returned correct value: %d\n",
prop_int);
} else {
printf("FAIL: napi_get_property with key '0' returned wrong value: "
"status=%d, value=%d\n",
int_status, prop_int);
}
} else {
printf("FAIL: napi_get_property with key '0' failed: %d\n", prop_status);
}
// Test napi_has_property
bool has_property;
napi_status has_prop_status =
napi_has_property(env, obj, key_1, &has_property);
if (has_prop_status == napi_ok && has_property) {
printf("PASS: napi_has_property with key '1' returned true\n");
} else {
printf("FAIL: napi_has_property with key '1' failed or returned false: "
"status=%d, has=%s\n",
has_prop_status, has_property ? "true" : "false");
}
// Test napi_has_own_property
bool has_own_property;
napi_status has_own_status =
napi_has_own_property(env, obj, key_0, &has_own_property);
if (has_own_status == napi_ok && has_own_property) {
printf("PASS: napi_has_own_property with key '0' returned true\n");
} else {
printf("FAIL: napi_has_own_property with key '0' failed or returned false: "
"status=%d, has=%s\n",
has_own_status, has_own_property ? "true" : "false");
}
// Test napi_delete_property
bool delete_result;
napi_status delete_status =
napi_delete_property(env, obj, key_1, &delete_result);
if (delete_status == napi_ok) {
printf("PASS: napi_delete_property with key '1' succeeded, result=%s\n",
delete_result ? "true" : "false");
// Verify the property was actually deleted
bool still_has_property;
napi_status verify_status =
napi_has_property(env, obj, key_1, &still_has_property);
if (verify_status == napi_ok && !still_has_property) {
printf("PASS: Property '1' was successfully deleted\n");
} else {
printf(
"FAIL: Property '1' still exists after deletion: status=%d, has=%s\n",
verify_status, still_has_property ? "true" : "false");
}
} else {
printf("FAIL: napi_delete_property with key '1' failed: %d\n",
delete_status);
}
return ok(env);
}
static napi_value test_deferred_exceptions(const Napi::CallbackInfo &info) {
napi_env env = info.Env();
@@ -1466,6 +1846,8 @@ void register_standalone_tests(Napi::Env env, Napi::Object exports) {
REGISTER_FUNCTION(env, exports, bigint_to_64_null);
REGISTER_FUNCTION(env, exports, test_is_buffer);
REGISTER_FUNCTION(env, exports, test_is_typedarray);
REGISTER_FUNCTION(env, exports, test_napi_get_default_values);
REGISTER_FUNCTION(env, exports, test_napi_numeric_string_keys);
REGISTER_FUNCTION(env, exports, test_deferred_exceptions);
REGISTER_FUNCTION(env, exports, test_napi_strict_equals);
REGISTER_FUNCTION(env, exports, test_napi_call_function_recv_null);

View File

@@ -549,6 +549,14 @@ describe.concurrent("napi", () => {
await checkSameOutput("test_deferred_exceptions", []);
});
it("behaves as expected when performing operations with numeric string keys", async () => {
await checkSameOutput("test_napi_numeric_string_keys", []);
});
it("behaves as expected when performing operations with default values", async () => {
await checkSameOutput("test_napi_get_default_values", []);
});
it("NAPI finalizer iterator invalidation crash prevention", () => {
// This test verifies that the DeferGCForAWhile fix prevents iterator invalidation
// during NAPI finalizer cleanup. While we couldn't reproduce the exact crash

View File

@@ -1,3 +1,4 @@
import { test } from "bun:test";
import { basename, dirname, sep } from "node:path";
import { build, run } from "../../../harness";
@@ -6,7 +7,7 @@ test("build", async () => {
});
for (const file of Array.from(new Bun.Glob("*.js").scanSync(import.meta.dir))) {
test.todoIf(["test.js"].includes(file))(file, () => {
test(file, () => {
run(dirname(import.meta.dir), basename(import.meta.dir) + sep + file);
});
}