Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
60 changes: 40 additions & 20 deletions crates/perry-api-manifest/src/entries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,19 @@ const ZLIB_OPTIONS_PARAM: ParamSpec = ParamSpec::Named {
};
const fn zlib_stream_factory(name: &'static str) -> ApiEntry {
method_sig("zlib", name, false, None, ZLIB_STREAM_OPTS, TypeSpec::Any)
.stub_note("options (level/chunkSize/dictionary/...) accepted but ignored (#4917)")
}
/// Deflate-family compressor factory: `level` is honored (#4917);
/// `strategy`/`memLevel` are validated but not applied, and a supplied
/// `dictionary` warns once instead of silently mis-compressing.
const fn zlib_compressor_factory(name: &'static str) -> ApiEntry {
zlib_stream_factory(name)
.stub_note("level honored; strategy/memLevel validated but not applied (#4917)")
}
/// Brotli/zstd factory: their `params` option shape is not wired up; a
/// passed options object warns once (#4917).
const fn zlib_params_factory(name: &'static str) -> ApiEntry {
zlib_stream_factory(name)
.stub_note("params/quality options accepted but ignored, warns once (#4917)")
}

/// Source-of-truth manifest. See module-level docs for what feeds it.
Expand Down Expand Up @@ -590,8 +602,9 @@ pub static API_MANIFEST: &[ApiEntry] = &[
method("mongodb", "insertOne", true, None),
method("mongodb", "insertMany", true, None),
method("mongodb", "find", true, None),
method("mongodb", "findOne", true, None)
.stub_note("resolves a JSON string, not a document object (#4917)"),
// #4917 — resolves a parsed document object (BSON-specific types in
// relaxed extended-JSON shape, e.g. `_id.$oid`), or null.
method("mongodb", "findOne", true, None),
method("mongodb", "updateOne", true, None),
method("mongodb", "updateMany", true, None),
method("mongodb", "deleteOne", true, None),
Expand Down Expand Up @@ -1632,15 +1645,17 @@ pub static API_MANIFEST: &[ApiEntry] = &[
}],
TypeSpec::Bool,
),
// #4917 — real retry semantics: options (numOfAttempts/startingDelay/
// timeMultiple/maxDelay/delayFirstAttempt/jitter/retry) honored;
// Promise-returning tasks retry on rejection via promise reactions.
method_sig(
"exponential-backoff",
"backOff",
false,
None,
&[p_any("p0"), p_any("p1")],
TypeSpec::Any,
)
.stub_note("retry options ignored; hardcoded 3 attempts / 100ms / x2 (#4917)"),
),
method_sig(
"argon2",
"hash",
Expand Down Expand Up @@ -2223,19 +2238,22 @@ pub static API_MANIFEST: &[ApiEntry] = &[
),
// #1843 — Transform-stream factories. Each returns a stream handle
// supporting `.write`/`.end`/`.on('data'|'end'|'error')`/`.pipe`.
zlib_stream_factory("createGzip"),
// #4917 — deflate-family factories honor `options.level`; a supplied
// `dictionary` warns once (decompressors fail loudly without it, so
// the plain factories are no longer flagged).
zlib_compressor_factory("createGzip"),
zlib_stream_factory("createGunzip"),
zlib_stream_factory("createDeflate"),
zlib_compressor_factory("createDeflate"),
zlib_stream_factory("createInflate"),
zlib_stream_factory("createDeflateRaw"),
zlib_compressor_factory("createDeflateRaw"),
zlib_stream_factory("createInflateRaw"),
zlib_stream_factory("createUnzip"),
zlib_stream_factory("createBrotliCompress"),
zlib_params_factory("createBrotliCompress"),
// `zlib.createBrotliDecompress(options?)` — now a real Transform stream
// (still passes axios's `typeof === 'function'` module-init gate).
zlib_stream_factory("createBrotliDecompress"),
zlib_stream_factory("createZstdCompress"),
zlib_stream_factory("createZstdDecompress"),
zlib_params_factory("createBrotliDecompress"),
zlib_params_factory("createZstdCompress"),
zlib_params_factory("createZstdDecompress"),
method_sig(
"cron",
"validate",
Expand Down Expand Up @@ -2797,10 +2815,11 @@ pub static API_MANIFEST: &[ApiEntry] = &[
method("worker_threads", "once", true, Some("Worker")),
method("worker_threads", "off", true, Some("Worker")),
method("worker_threads", "terminate", true, Some("Worker")),
method("worker_threads", "ref", true, Some("Worker"))
.stub_note("no-op; does not affect process event-loop ref-count (#4917)"),
method("worker_threads", "unref", true, Some("Worker"))
.stub_note("no-op; does not affect process event-loop ref-count (#4917)"),
// #4917 — real: `ref()`/`unref()` flip `WorkerRecord.refed`, which
// `js_worker_threads_has_pending` checks to keep the event loop alive
// (a live refed worker holds the process; `unref()` releases it).
method("worker_threads", "ref", true, Some("Worker")),
method("worker_threads", "unref", true, Some("Worker")),
method("worker_threads", "getHeapStatistics", true, Some("Worker")),
method("worker_threads", "cpuUsage", true, Some("Worker")),
method("worker_threads", "getHeapSnapshot", true, Some("Worker")),
Expand Down Expand Up @@ -4670,13 +4689,14 @@ pub static API_MANIFEST: &[ApiEntry] = &[
class("http", "Agent"),
method("http", "Agent", false, None),
method("http", "getName", true, Some("Agent")),
method("http", "destroy", true, Some("Agent"))
.stub_note("real Agent object, but Perry does not pool sockets; this is a no-op (#4917)"),
// #4917 — `destroy()` really drops the per-agent reqwest client (=
// releases its keep-alive pool) and flips `destroyed`; not a stub.
method("http", "destroy", true, Some("Agent")),
method("http", "close", true, Some("Agent")),
method("http", "keepSocketAlive", true, Some("Agent"))
.stub_note("real Agent object, but Perry does not pool sockets; this is a no-op (#4917)"),
.stub_note("reqwest owns the keep-alive pool; per-socket hooks are no-ops, warns once (#4917)"),
method("http", "reuseSocket", true, Some("Agent"))
.stub_note("real Agent object, but Perry does not pool sockets; this is a no-op (#4917)"),
.stub_note("reqwest owns the keep-alive pool; per-socket hooks are no-ops, warns once (#4917)"),
// Synthetic `__get_<name>` / `__set_<name>` accessor methods (HIR
// rewrites bare `agent.maxSockets` reads to `__get_maxSockets()`
// when the receiver is class-tagged) + their bare-name twins for
Expand Down
30 changes: 26 additions & 4 deletions crates/perry-api-manifest/tests/stub_inventory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,17 @@ fn stub_inventory_matches_known_clusters() {
// open/url/waitForDebugger/Session.post(4) + repl
// start/REPLServer no-eval-loop(2).
("#4916", 10),
("#4917", 18), // stdlib adapters: zlib(11) + http.Agent(3) + worker ref/unref(2) + mongodb(1) + backoff(1)
// #4917 (stdlib-adapter no-ops) — what remains after the real
// semantics landed: zlib deflate-family compressor factories
// honor `level` but still drop strategy/memLevel (3) +
// Brotli/zstd factories ignore `params`, warn once (4) +
// http.Agent per-socket keepSocketAlive/reuseSocket hooks (2).
// Intentionally absent now: zlib decompressor factories (level
// honored; a missing dictionary fails loudly), Agent.destroy
// (drops the per-agent reqwest pool), worker ref/unref (real
// event-loop refcount), mongodb.findOne (parsed document),
// exponential-backoff options (honored, incl. retry predicate).
("#4917", 9),
];
let expected_map: BTreeMap<String, usize> =
expected.iter().map(|(k, v)| (k.to_string(), *v)).collect();
Expand Down Expand Up @@ -127,8 +137,10 @@ fn keystone_apis_are_flagged() {
let must_be_stub: &[(&str, &str)] = &[
("repl", "start"),
("inspector", "post"),
// still partial after #4917: level honored, strategy/memLevel dropped
("zlib", "createGzip"),
("mongodb", "findOne"),
("zlib", "createBrotliCompress"),
("http", "keepSocketAlive"),
];
for (module, name) in must_be_stub {
let found = iter_entries().any(|e| e.module == *module && e.name == *name && e.stub);
Expand All @@ -137,8 +149,18 @@ fn keystone_apis_are_flagged() {

// The inverse: APIs implemented for real must NOT stay flagged.
// v8 heap snapshots emit a real GC-walk object graph since #4916.
let must_not_be_stub: &[(&str, &str)] =
&[("v8", "getHeapSnapshot"), ("v8", "writeHeapSnapshot")];
// mongodb.findOne resolves a parsed document, backOff honors its
// options, and worker ref/unref drive the event-loop refcount since
// #4917.
let must_not_be_stub: &[(&str, &str)] = &[
("v8", "getHeapSnapshot"),
("v8", "writeHeapSnapshot"),
("mongodb", "findOne"),
("exponential-backoff", "backOff"),
("worker_threads", "ref"),
("worker_threads", "unref"),
("http", "destroy"),
];
for (module, name) in must_not_be_stub {
let flagged = iter_entries().any(|e| e.module == *module && e.name == *name && e.stub);
assert!(
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/lower_call/native_table/media.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ pub(super) const MEDIA_ROWS: &[NativeModSig] = &[
method: "deflateRawSync",
class_filter: None,
runtime: "js_zlib_deflate_raw_sync",
args: &[NA_F64],
args: &[NA_F64, NA_F64],
ret: NR_PTR,
},
NativeModSig {
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,7 @@ pub fn declare_stdlib_ffi(module: &mut LlModule) {
module.declare_function("js_zlib_gzip", VOID, &[DOUBLE, DOUBLE]);
module.declare_function("js_zlib_inflate_sync", I64, &[I64]);
module.declare_function("js_zlib_inflate", VOID, &[DOUBLE, DOUBLE]);
module.declare_function("js_zlib_deflate_raw_sync", I64, &[DOUBLE]);
module.declare_function("js_zlib_deflate_raw_sync", I64, &[DOUBLE, DOUBLE]);
module.declare_function("js_zlib_deflate_raw", VOID, &[DOUBLE, DOUBLE]);
module.declare_function("js_zlib_inflate_raw_sync", I64, &[DOUBLE]);
module.declare_function("js_zlib_inflate_raw", VOID, &[DOUBLE, DOUBLE]);
Expand Down
9 changes: 8 additions & 1 deletion crates/perry-ext-http/src/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -851,11 +851,18 @@ fn json_value_to_string(v: &serde_json::Value) -> String {
}

// ------------------------------------------------------------------
// destroy / keepSocketAlive / reuseSocket — chainable no-ops
// keepSocketAlive / reuseSocket — chainable no-ops (reqwest owns the
// keep-alive pool, so there is no per-socket hook to forward to);
// destroy is real (drops the cached client below).
// ------------------------------------------------------------------

#[no_mangle]
pub extern "C" fn js_http_agent_noop_self(handle: Handle) -> Handle {
perry_runtime::stub_diag::perry_stub_warn(
"http.Agent keepSocketAlive/reuseSocket",
"reqwest owns the keep-alive pool; per-socket hooks are no-ops",
Some("#4917"),
);
handle
}

Expand Down
50 changes: 45 additions & 5 deletions crates/perry-ext-mysql2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,16 +320,56 @@ fn raw_row_to_js_array(row: &RawRowData) -> *mut ArrayHeader {
arr
}

/// Map sqlx's MySQL type *name* back to the wire-protocol numeric type ID
/// (`enum_field_types`, what Node's mysql2 puts in `field.type`/`columnType`).
/// Twin of `perry_stdlib::mysql2::types::mysql_type_id_from_name` (#4917) —
/// this crate cannot depend on perry-stdlib, keep the two in sync.
fn mysql_type_id_from_name(name: &str) -> f64 {
let base = name.strip_suffix(" UNSIGNED").unwrap_or(name);
let id: u8 = match base {
"BOOLEAN" | "TINYINT" => 1,
"SMALLINT" => 2,
"INT" => 3,
"FLOAT" => 4,
"DOUBLE" => 5,
"NULL" => 6,
"TIMESTAMP" => 7,
"BIGINT" => 8,
"MEDIUMINT" => 9,
"DATE" => 10,
"TIME" => 11,
"DATETIME" => 12,
"YEAR" => 13,
"BIT" => 16,
"JSON" => 245,
"DECIMAL" => 246,
"ENUM" => 247,
"SET" => 248,
"TINYBLOB" | "TINYTEXT" => 249,
"MEDIUMBLOB" | "MEDIUMTEXT" => 250,
"LONGBLOB" | "LONGTEXT" => 251,
"BLOB" | "TEXT" => 252,
"VARCHAR" | "VARBINARY" => 253,
"CHAR" | "BINARY" => 254,
"GEOMETRY" => 255,
_ => 0,
};
id as f64
}

fn raw_column_to_field_packet(col: &RawColumnInfo) -> *mut ObjectHeader {
let (packed, shape_id) = build_object_shape(&["name", "type", "length"]);
let (packed, shape_id) = build_object_shape(&["name", "type", "columnType", "length"]);
let obj =
unsafe { js_object_alloc_with_shape(shape_id, 3, packed.as_ptr(), packed.len() as u32) };
unsafe { js_object_alloc_with_shape(shape_id, 4, packed.as_ptr(), packed.len() as u32) };
let name_str = alloc_string(&col.name);
let type_str = alloc_string(&col.type_name);
// #4917: `type`/`columnType` carry the numeric wire ID mysql2 exposes;
// `length` stays 0 (sqlx 0.8 keeps the wire `max_size` pub(crate)).
let type_id = mysql_type_id_from_name(&col.type_name);
unsafe {
js_object_set_field(obj, 0, JsValue::from_string_ptr(name_str.as_raw()));
js_object_set_field(obj, 1, JsValue::from_string_ptr(type_str.as_raw()));
js_object_set_field(obj, 2, JsValue::from_number(0.0));
js_object_set_field(obj, 1, JsValue::from_number(type_id));
js_object_set_field(obj, 2, JsValue::from_number(type_id));
js_object_set_field(obj, 3, JsValue::from_number(0.0));
}
obj
}
Expand Down
35 changes: 29 additions & 6 deletions crates/perry-ext-pg/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,17 +162,40 @@ fn row_to_js_object(row: &PgRow) -> *mut ObjectHeader {
obj
}

/// Build a `FieldDef`-shaped object: `{ name, dataTypeID, tableID }`.
/// Build a `FieldDef`-shaped object matching node-pg's `result.fields[i]`
/// (#4917): `dataTypeID` is the numeric type OID, `tableID`/`columnID` come
/// from the RowDescription (0 for expression columns, like Node).
/// `dataTypeSize`/`dataTypeModifier` are not exposed by sqlx 0.8 and report
/// the "unknown/variable" sentinel -1. Twin of
/// `perry_stdlib::pg::types::column_to_field_def` — keep in sync.
fn column_to_field_def(col: &PgColumn) -> *mut ObjectHeader {
let (packed, shape_id) = build_object_shape(&["name", "dataTypeID", "tableID"]);
let (packed, shape_id) = build_object_shape(&[
"name",
"tableID",
"columnID",
"dataTypeID",
"dataTypeSize",
"dataTypeModifier",
"format",
]);
let obj =
unsafe { js_object_alloc_with_shape(shape_id, 3, packed.as_ptr(), packed.len() as u32) };
unsafe { js_object_alloc_with_shape(shape_id, 7, packed.as_ptr(), packed.len() as u32) };
let name_str = alloc_string(col.name());
let type_str = alloc_string(col.type_info().name());
let table_id = col.relation_id().map(|oid| oid.0 as f64).unwrap_or(0.0);
let column_id = col
.relation_attribute_no()
.map(|attno| attno as f64)
.unwrap_or(0.0);
let data_type_id = col.type_info().oid().map(|oid| oid.0 as f64).unwrap_or(0.0);
let format_str = alloc_string("text");
unsafe {
js_object_set_field(obj, 0, JsValue::from_string_ptr(name_str.as_raw()));
js_object_set_field(obj, 1, JsValue::from_string_ptr(type_str.as_raw()));
js_object_set_field(obj, 2, JsValue::from_number(0.0));
js_object_set_field(obj, 1, JsValue::from_number(table_id));
js_object_set_field(obj, 2, JsValue::from_number(column_id));
js_object_set_field(obj, 3, JsValue::from_number(data_type_id));
js_object_set_field(obj, 4, JsValue::from_number(-1.0));
js_object_set_field(obj, 5, JsValue::from_number(-1.0));
js_object_set_field(obj, 6, JsValue::from_string_ptr(format_str.as_raw()));
}
obj
}
Expand Down
1 change: 1 addition & 0 deletions crates/perry-runtime/src/array/sort.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ unsafe fn stable_merge_sort_raw(
l += 1;
k += 1;
}
// GC_STORE_AUDIT(STACK): tail copies target caller-rooted scratch buffers.
while r < right {
*dst.add(k) = *src.add(r);
r += 1;
Expand Down
14 changes: 14 additions & 0 deletions crates/perry-runtime/src/node_submodules/zlib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,20 @@ pub extern "C" fn js_zlib_validate_options(opts: f64, min_window_bits: i32) {
validate_option_range(ptr, b"strategy", "options.strategy", 0, 4);
validate_option_chunk_size(ptr);
validate_option_range(ptr, b"flush", "options.flush", 0, 5);

// #4917: `level` is honored and the ranged options above are validated,
// but a preset `dictionary` is not threaded through flate2. Silently
// dropping it would mis-compress against peers that expect it to apply,
// so warn once instead.
let dict = read_option_field(ptr, b"dictionary");
let dv = crate::value::JSValue::from_bits(dict.to_bits());
if !dv.is_undefined() && !dv.is_null() {
crate::stub_diag::perry_stub_warn(
"zlib options.dictionary",
"the preset dictionary option is accepted but not applied",
Some("#4917"),
);
}
}

/// Validate the `buffer` argument to a one-shot zlib codec (`gzipSync`,
Expand Down
Loading
Loading