Skip to content
Open
117 changes: 108 additions & 9 deletions src/jsc/ConsoleObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,42 @@ fn message_with_type_and_level_(
Ok(())
}

/// Steps the formatters give an iterable that has no element count of its own, such as a generator.
const UNSIZED_ITERABLE_BUDGET: u32 = 1000;

/// A bounded [`JSValue::for_each`] for formatters. True when elements were left. `Bun__ConsoleObject__forEachLimited` has the rules.
pub(crate) fn for_each_limited(
iterable: JSValue,
global: &JSGlobalObject,
budget: u32,
size_already_read: Option<i32>,
ctx: *mut c_void,
callback: jsc::ForEachCallback,
) -> JsResult<bool> {
unsafe extern "C" {
// safe: `ctx` is an opaque round-trip pointer that C++ only forwards to `callback`.
safe fn Bun__ConsoleObject__forEachLimited(
iterable: JSValue,
global: &JSGlobalObject,
budget: u32,
size_already_read: i32,
ctx: *mut c_void,
callback: jsc::ForEachCallback,
) -> bool;
}
let size_already_read = size_already_read.filter(|size| *size >= 0).unwrap_or(-1);
jsc::host_fn::from_js_host_call_generic(global, || {
Bun__ConsoleObject__forEachLimited(
iterable,
global,
budget,
size_already_read,
ctx,
callback,
)
})
}

// ───────────────────────────────────────────────────────────────────────────
// TablePrinter
// ───────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -898,6 +934,7 @@ impl<'a> TablePrinter<'a> {
// ranges, so no property is re-read and no value is re-formatted.
let mut cell_text: Vec<u8> = Vec::new();
let mut rows: Vec<CollectedRow> = Vec::new();
let mut rows_truncated = false;
{
if self.is_iterable {
struct Ctx<'c, 'a> {
Expand Down Expand Up @@ -943,8 +980,11 @@ impl<'a> TablePrinter<'a> {
}
ctx.idx += 1;
}
tabular_data.for_each_with_context(
rows_truncated = for_each_limited(
tabular_data,
global_object,
UNSIZED_ITERABLE_BUDGET,
None,
(&raw mut ctx).cast::<c_void>(),
callback::<ENABLE_ANSI_COLORS>,
)?;
Expand Down Expand Up @@ -1063,6 +1103,11 @@ impl<'a> TablePrinter<'a> {
let _ = writer.write_all("┘\n".as_bytes());
}

if rows_truncated {
let _ =
writer.write_all(pfmt!("<r><d>... more rows<r>\n", ENABLE_ANSI_COLORS).as_bytes());
}

Ok(())
}
}
Expand Down Expand Up @@ -2745,6 +2790,24 @@ pub mod formatter {
self.estimated_line_length += 1;
Ok(())
}

/// Ends a preview whose iterator had more to give than the walk allowed.
fn print_more_entries<const C: bool>(
&mut self,
writer: &mut dyn bun_io::Write,
wrote_entry: bool,
) {
if !self.single_line {
let _ = self.write_indent(writer);
} else if wrote_entry {
let _ = self.print_comma::<C>(writer);
let _ = writer.write_all(b" ");
}
let _ = writer.write_all(pfmt!("<r><d>... more items<r>", C).as_bytes());
if !self.single_line {
let _ = writer.write_all(b"\n");
}
}
}

// ───────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -4601,16 +4664,22 @@ pub mod formatter {
writer: writer_,
count: 0,
};
value.for_each(
let truncated = for_each_limited(
value,
global_this,
UNSIZED_ITERABLE_BUDGET,
Some(length),
(&raw mut iter).cast::<c_void>(),
MapIteratorCtx::<C, false, true>::for_each,
)?;
let count = iter.count;
if iter.formatter.failed {
return Ok(());
}
if count > 0 {
if truncated {
self.print_more_entries::<C>(writer_, count > 0);
}
if count > 0 || truncated {
let _ = writer_.write_all(b" ");
}
} else {
Expand All @@ -4619,14 +4688,20 @@ pub mod formatter {
writer: writer_,
count: 0,
};
value.for_each(
let truncated = for_each_limited(
value,
global_this,
UNSIZED_ITERABLE_BUDGET,
Some(length),
(&raw mut iter).cast::<c_void>(),
MapIteratorCtx::<C, false, false>::for_each,
)?;
if iter.formatter.failed {
return Ok(());
}
if truncated {
self.print_more_entries::<C>(writer_, true);
}
}
}
if !self.single_line {
Expand Down Expand Up @@ -4660,15 +4735,21 @@ pub mod formatter {
writer: writer_,
count: 0,
};
value.for_each(
let truncated = for_each_limited(
value,
global_this,
UNSIZED_ITERABLE_BUDGET,
None,
(&raw mut iter).cast::<c_void>(),
MapIteratorCtx::<C, true, true>::for_each,
)?;
let count = iter.count;
if iter.formatter.failed {
return Ok(());
}
if truncated {
self.print_more_entries::<C>(writer_, count > 0);
}
// Only the MapIterator case writes a trailing space.
if count > 0 && label == "MapIterator" {
let _ = writer_.write_all(b" ");
Expand All @@ -4679,8 +4760,11 @@ pub mod formatter {
writer: writer_,
count: 0,
};
value.for_each(
let truncated = for_each_limited(
value,
global_this,
UNSIZED_ITERABLE_BUDGET,
None,
(&raw mut iter).cast::<c_void>(),
MapIteratorCtx::<C, true, false>::for_each,
)?;
Expand All @@ -4691,6 +4775,9 @@ pub mod formatter {
if count > 0 {
let _ = writer_.write_all(b"\n");
}
if truncated {
self.print_more_entries::<C>(writer_, count > 0);
}
}
}
if !self.single_line {
Expand Down Expand Up @@ -4743,16 +4830,22 @@ pub mod formatter {
writer: writer_,
is_first: true,
};
value.for_each(
let truncated = for_each_limited(
value,
global_this,
UNSIZED_ITERABLE_BUDGET,
Some(length),
(&raw mut iter).cast::<c_void>(),
SetIteratorCtx::<C, true>::for_each,
)?;
let is_first = iter.is_first;
if iter.formatter.failed {
return Ok(());
}
if !is_first {
if truncated {
self.print_more_entries::<C>(writer_, !is_first);
}
if !is_first || truncated {
let _ = writer_.write_all(b" ");
}
} else {
Expand All @@ -4761,14 +4854,20 @@ pub mod formatter {
writer: writer_,
is_first: true,
};
value.for_each(
let truncated = for_each_limited(
value,
global_this,
UNSIZED_ITERABLE_BUDGET,
Some(length),
(&raw mut iter).cast::<c_void>(),
SetIteratorCtx::<C, false>::for_each,
)?;
if iter.formatter.failed {
return Ok(());
}
if truncated {
self.print_more_entries::<C>(writer_, true);
}
}
}
if !self.single_line {
Expand Down
13 changes: 1 addition & 12 deletions src/jsc/JSValue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2210,7 +2210,7 @@ struct SerializedScriptValueExternal {
handle: *mut c_void,
}

/// Callback signature for [`JSValue::for_each`] / [`JSValue::for_each_with_context`].
/// Callback signature for [`JSValue::for_each`].
pub type ForEachCallback =
extern "C" fn(vm: *mut crate::VM, global: &JSGlobalObject, ctx: *mut c_void, next: JSValue);

Expand Down Expand Up @@ -2443,17 +2443,6 @@ impl JSValue {
JSC__JSValue__forEach(self, global, ctx, callback)
})
}
/// `JSValue.forEachWithContext` — typed-ctx wrapper (callers pass
/// `*mut c_void` directly).
#[inline]
pub(crate) fn for_each_with_context(
self,
global: &JSGlobalObject,
ctx: *mut c_void,
callback: ForEachCallback,
) -> JsResult<()> {
self.for_each(global, ctx, callback)
}
/// `JSValue.forEachProperty` — enumerate own props,
/// invoking `callback` per (key, value, is_symbol, is_private_symbol).
///
Expand Down
25 changes: 20 additions & 5 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5439,11 +5439,26 @@ impl VirtualMachine {
allow_side_effects,
printed_member: false,
};
if errors
.for_each(global_ref, (&raw mut ctx).cast(), agg_iter)
.is_err()
{
global_ref.clear_exception();
// `errors` is user-assigned, so its iterator can be endless.
const UNSIZED_ERRORS_BUDGET: u32 = 100;
match crate::console_object::for_each_limited(
errors,
global_ref,
UNSIZED_ERRORS_BUDGET,
None,
(&raw mut ctx).cast(),
agg_iter,
) {
Ok(true) => {
let marker = if allow_ansi_color {
bun_core::pretty_fmt!("<r><d>... more errors<r>\n", true)
} else {
bun_core::pretty_fmt!("<r><d>... more errors<r>\n", false)
};
let _ = writer.write_all(marker.as_bytes());
}
Ok(false) => {}
Err(_) => global_ref.clear_exception(),
}
if ctx.printed_member {
return;
Expand Down
Loading
Loading