From 864eb7f82728060975aba9e4e4cfd5e79e90ce86 Mon Sep 17 00:00:00 2001 From: Sunli Date: Fri, 18 Sep 2026 11:54:12 +0800 Subject: [PATCH 1/2] feat(trade): add DelayedNotReported order status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gateway reports time-conditional orders that are being monitored, before they are reported to the exchange, with the order status `DelayedNotReported` (see longbridge/developers#1263). No SDK enum matched it, so `impl_serde_for_enum_string!`'s `unwrap_or_default()` turned it into `OrderStatus::Unknown` — monitoring orders were indistinguishable from a genuinely unrecognized status in `today_orders` / `history_orders` / `order_detail` and the order-changed push. Added across all six layers: Rust, C (header regenerated by cbindgen), C++ (enum plus both conversion directions), Java (JNI list plus the Java enum), Node.js (`index.d.ts` regenerated) and Python (plus the `openapi.pyi` stub). The variant is appended after `PartialWithdrawal` rather than placed next to the other `*NotReported` values: the C/C++ and Node.js enums use implicit discriminants, so inserting mid-list would renumber every following value — an ABI break for the C/C++ bindings, and a silent mismatch for TypeScript code that inlined the old `const enum` values. --- CHANGELOG.md | 6 ++++++ c/csrc/include/longbridge.h | 4 ++++ c/src/trade_context/enum_types.rs | 3 +++ cpp/include/types.hpp | 2 ++ cpp/src/convert.hpp | 4 ++++ .../src/main/java/com/longbridge/trade/OrderStatus.java | 2 ++ java/src/types/enum_types.rs | 1 + nodejs/index.d.ts | 4 +++- nodejs/src/trade/types.rs | 2 ++ python/pysrc/longbridge/openapi.pyi | 5 +++++ python/src/trade/types.rs | 2 ++ rust/src/trade/types.rs | 3 +++ 12 files changed, 37 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6b424597b..0f4825d0bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- **All SDKs:** `OrderStatus` gains a `DelayedNotReported` variant (wire value `DelayedNotReported`) — "monitoring", the state a time-conditional order sits in before it is reported to the exchange. Previously this status deserialized to `OrderStatus::Unknown`, so time-conditional orders being monitored were indistinguishable from a genuinely unrecognized status in `today_orders` / `history_orders` / `order_detail` and the order-changed push. The variant is appended **after** `PartialWithdrawal` rather than inserted next to the other `*NotReported` values on purpose: the C/C++/Node.js enums use implicit discriminants, and inserting mid-list would renumber every following value (an ABI break for the C/C++ bindings, and a silent mismatch for TypeScript code that inlined the old `const enum` values) + ## [5.0.0] - 2026-09-14 ### Changed diff --git a/c/csrc/include/longbridge.h b/c/csrc/include/longbridge.h index e3b78b5540..2e501c36ed 100644 --- a/c/csrc/include/longbridge.h +++ b/c/csrc/include/longbridge.h @@ -1261,6 +1261,10 @@ typedef enum lb_order_status_t { * Partial Withdrawal */ OrderStatusPartialWithdrawal, + /** + * Monitoring (Time-Conditional Order) + */ + OrderStatusDelayedNotReported, } lb_order_status_t; /** diff --git a/c/src/trade_context/enum_types.rs b/c/src/trade_context/enum_types.rs index dc7260af3d..e13a2ef13e 100644 --- a/c/src/trade_context/enum_types.rs +++ b/c/src/trade_context/enum_types.rs @@ -138,6 +138,9 @@ pub enum COrderStatus { /// Partial Withdrawal #[c(remote = "PartialWithdrawal")] OrderStatusPartialWithdrawal, + /// Monitoring (Time-Conditional Order) + #[c(remote = "DelayedNotReported")] + OrderStatusDelayedNotReported, } /// Order tag diff --git a/cpp/include/types.hpp b/cpp/include/types.hpp index 33ae6ffb7b..a83fcdae18 100644 --- a/cpp/include/types.hpp +++ b/cpp/include/types.hpp @@ -1480,6 +1480,8 @@ enum class OrderStatus Expired, /// Partial Withdrawal PartialWithdrawal, + /// Monitoring (Time-Conditional Order) + DelayedNotReported, }; /// Order type diff --git a/cpp/src/convert.hpp b/cpp/src/convert.hpp index f18c05ff96..482080ba35 100644 --- a/cpp/src/convert.hpp +++ b/cpp/src/convert.hpp @@ -1051,6 +1051,8 @@ convert(lb_order_status_t status) return OrderStatus::Expired; case OrderStatusPartialWithdrawal: return OrderStatus::PartialWithdrawal; + case OrderStatusDelayedNotReported: + return OrderStatus::DelayedNotReported; default: throw std::invalid_argument("unreachable"); } @@ -1096,6 +1098,8 @@ convert(OrderStatus status) return OrderStatusExpired; case OrderStatus::PartialWithdrawal: return OrderStatusPartialWithdrawal; + case OrderStatus::DelayedNotReported: + return OrderStatusDelayedNotReported; default: throw std::invalid_argument("unreachable"); } diff --git a/java/javasrc/src/main/java/com/longbridge/trade/OrderStatus.java b/java/javasrc/src/main/java/com/longbridge/trade/OrderStatus.java index 8813cc4ae4..7fbeae1f65 100644 --- a/java/javasrc/src/main/java/com/longbridge/trade/OrderStatus.java +++ b/java/javasrc/src/main/java/com/longbridge/trade/OrderStatus.java @@ -40,4 +40,6 @@ public enum OrderStatus { Expired, /** Partial withdrawal */ PartialWithdrawal, + /** Monitoring (time-conditional order) */ + DelayedNotReported, } diff --git a/java/src/types/enum_types.rs b/java/src/types/enum_types.rs index 38ceb4fcac..1d16d9428b 100644 --- a/java/src/types/enum_types.rs +++ b/java/src/types/enum_types.rs @@ -381,6 +381,7 @@ impl_java_enum!( Canceled, Expired, PartialWithdrawal, + DelayedNotReported, ] ); diff --git a/nodejs/index.d.ts b/nodejs/index.d.ts index df10f667e8..5425b6bf16 100644 --- a/nodejs/index.d.ts +++ b/nodejs/index.d.ts @@ -6227,7 +6227,9 @@ export declare const enum OrderStatus { /** Expired */ Expired = 16, /** Partial Withdrawal */ - PartialWithdrawal = 17 + PartialWithdrawal = 17, + /** Monitoring (Time-Conditional Order) */ + DelayedNotReported = 18 } /** Order tag */ diff --git a/nodejs/src/trade/types.rs b/nodejs/src/trade/types.rs index b251bc9c70..859a156b50 100644 --- a/nodejs/src/trade/types.rs +++ b/nodejs/src/trade/types.rs @@ -74,6 +74,8 @@ pub enum OrderStatus { Expired, /// Partial Withdrawal PartialWithdrawal, + /// Monitoring (Time-Conditional Order) + DelayedNotReported, } #[napi_derive::napi] diff --git a/python/pysrc/longbridge/openapi.pyi b/python/pysrc/longbridge/openapi.pyi index cdfb56adbd..407e3e1569 100644 --- a/python/pysrc/longbridge/openapi.pyi +++ b/python/pysrc/longbridge/openapi.pyi @@ -5704,6 +5704,11 @@ class OrderStatus: PartialWithdrawal """ + class DelayedNotReported(OrderStatus): + """ + Monitoring (Time-Conditional Order) + """ + class OrderTag: """ Order tag diff --git a/python/src/trade/types.rs b/python/src/trade/types.rs index 0baefd3455..cd281e2be0 100644 --- a/python/src/trade/types.rs +++ b/python/src/trade/types.rs @@ -90,6 +90,8 @@ pub(crate) enum OrderStatus { Expired, /// Partial Withdrawal PartialWithdrawal, + /// Monitoring (Time-Conditional Order) + DelayedNotReported, } #[pyclass(eq, eq_int, from_py_object)] diff --git a/rust/src/trade/types.rs b/rust/src/trade/types.rs index 46f079c175..7ea71b56aa 100644 --- a/rust/src/trade/types.rs +++ b/rust/src/trade/types.rs @@ -109,6 +109,9 @@ pub enum OrderStatus { /// Partial Withdrawal #[strum(serialize = "PartialWithdrawal")] PartialWithdrawal, + /// Monitoring (Time-Conditional Order) + #[strum(serialize = "DelayedNotReported")] + DelayedNotReported, } /// Execution From 38184a7e5fa3532294de35037c27fb90ae9093e9 Mon Sep 17 00:00:00 2001 From: Sunli Date: Fri, 18 Sep 2026 11:58:49 +0800 Subject: [PATCH 2/2] chore(nodejs): regenerate index.js with the pinned @napi-rs/cli 3.8.6 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The committed index.js was generated by an older CLI than the 3.8.6 the release workflow pins, so regenerating it alongside index.d.ts produces loader churn unrelated to the enum change. Committing it here keeps the two generated files in step with each other and with CI. What the regenerated loader changes: - `require('node:fs')` → `require('fs')` and the optional chaining in the musl / win32 probes is expanded, restoring older-Node compatibility - NAPI_RS_FORCE_WASI is now tri-state ('true' / 'error' / unset) instead of any-non-empty-string truthy, so NAPI_RS_FORCE_WASI=false or =0 no longer takes the WASI path and fails with ENOENT - adds NAPI_RS_WASI_FLAVOR for selecting one exact generated flavor - WASI candidates are resolved before being required, and the load-error chain is built without mutating the original errors; `error.cause` is assigned rather than passed to the Error options form, which Node < 16.9 ignores --- nodejs/index.js | 186 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 155 insertions(+), 31 deletions(-) diff --git a/nodejs/index.js b/nodejs/index.js index 88789ebfbe..ed780f4738 100644 --- a/nodejs/index.js +++ b/nodejs/index.js @@ -3,7 +3,7 @@ // @ts-nocheck /* auto-generated by NAPI-RS */ -const { readFileSync } = require('node:fs') +const { readFileSync } = require('fs') let nativeBinding = null const loadErrors = [] @@ -33,7 +33,7 @@ const isMuslFromFilesystem = () => { const isMuslFromReport = () => { let report = null - if (typeof process.report?.getReport === 'function') { + if (process.report && typeof process.report.getReport === 'function') { process.report.excludeNetwork = true report = process.report.getReport() } @@ -105,7 +105,7 @@ function requireNative() { } } else if (process.platform === 'win32') { if (process.arch === 'x64') { - if (process.config?.variables?.shlib_suffix === 'dll.a' || process.config?.variables?.node_target_type === 'shared_library') { + if ((process.config && process.config.variables && process.config.variables.shlib_suffix === 'dll.a') || (process.config && process.config.variables && process.config.variables.node_target_type === 'shared_library')) { try { return require('./longbridge.win32-x64-gnu.node') } catch (e) { @@ -523,54 +523,178 @@ function requireNative() { } } -nativeBinding = requireNative() +function createLoadErrorChain(errors) { + return errors.reduce((previous, current) => { + let message + try { + message = + current && typeof current.message === 'string' + ? current.message + : String(current) + } catch { + message = 'Unknown error' + } + const error = new Error(message) + error.cause = previous + return error + }, null) +} + +// NAPI_RS_FORCE_WASI is a tri-state flag: +// unset / any other value → native binding preferred, WASI is only a fallback +// 'true' → prefer WASI, but retain native as a lazy fallback +// 'error' → require WASI without initializing a native fallback +// Treating any non-empty string as truthy (the historical behavior) meant +// NAPI_RS_FORCE_WASI=false, NAPI_RS_FORCE_WASI=0, etc. inadvertently triggered +// the WASI path, causing ENOENT for packages shipped without a .wasi.cjs file. +// +// NAPI_RS_WASI_FLAVOR selects one exact generated flavor and implies strict +// WASI loading. It never crosses into another flavor or falls back to native. +const __napiWasiFlavors = ["wasm32-wasi"] +const __napiWasiFlavor = process.env.NAPI_RS_WASI_FLAVOR +const __napiWasiFlavorRequested = + typeof __napiWasiFlavor === 'string' && __napiWasiFlavor.length > 0 +if ( + __napiWasiFlavorRequested && + __napiWasiFlavors.indexOf(__napiWasiFlavor) === -1 +) { + throw new Error( + 'Unsupported WASI flavor "' + + __napiWasiFlavor + + '". Available flavors: ' + + __napiWasiFlavors.join(', '), + ) +} +const forceWasiError = process.env.NAPI_RS_FORCE_WASI === 'error' +const forceWasi = + process.env.NAPI_RS_FORCE_WASI === 'true' || + forceWasiError || + __napiWasiFlavorRequested + +if (!forceWasi) { + nativeBinding = requireNative() +} -if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) { +if (!nativeBinding || forceWasi) { let wasiBinding = null - let wasiBindingError = null - try { - wasiBinding = require('./longbridge.wasi.cjs') - nativeBinding = wasiBinding - } catch (err) { - if (process.env.NAPI_RS_FORCE_WASI) { - wasiBindingError = err + let wasiBindingLoaded = false + const wasiBindingErrors = [] + const __napiWasiResolveCandidate = (specifier, isPackage, localArtifacts) => { + try { + require.resolve(specifier) + } catch (resolveError) { + if (!resolveError || resolveError.code !== 'MODULE_NOT_FOUND') { + throw resolveError + } + if (isPackage) { + try { + require.resolve(specifier + '/package.json') + } catch (packageError) { + if (packageError && packageError.code === 'MODULE_NOT_FOUND') { + return resolveError + } + // An exports restriction proves the package exists even when its + // package.json is not public. Preserve the root resolution failure. + throw resolveError + } + // The package exists but its main/export target is broken. + throw resolveError + } + return resolveError } + if (localArtifacts) { + let artifactError = null + for (let i = 0; i < localArtifacts.length; i++) { + try { + require.resolve(localArtifacts[i]) + return null + } catch (resolveError) { + if (!resolveError || resolveError.code !== 'MODULE_NOT_FOUND') { + throw resolveError + } + artifactError = resolveError + } + } + return artifactError + } + return null } - if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) { + if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === "wasm32-wasi")) { + let candidateError = null + let candidateFailed = false try { - wasiBinding = require('longbridge-wasm32-wasi') - nativeBinding = wasiBinding + candidateError = __napiWasiResolveCandidate('./longbridge.wasi.cjs', false, ["./longbridge.wasm32-wasi.debug.wasm","./longbridge.wasm32-wasi.wasm"]) + candidateFailed = candidateError !== null + if (!candidateFailed) { + wasiBinding = require('./longbridge.wasi.cjs') + nativeBinding = wasiBinding + wasiBindingLoaded = true + } } catch (err) { - if (process.env.NAPI_RS_FORCE_WASI) { - if (!wasiBindingError) { - wasiBindingError = err - } else { - wasiBindingError.cause = err + candidateError = err + candidateFailed = true + } + if (candidateFailed) { + wasiBindingErrors.push(candidateError) + loadErrors.push(candidateError) + } + } + if (!wasiBindingLoaded && (!__napiWasiFlavorRequested || __napiWasiFlavor === "wasm32-wasi")) { + let candidateError = null + let candidateFailed = false + try { + candidateError = __napiWasiResolveCandidate('longbridge-wasm32-wasi', true, undefined) + candidateFailed = candidateError !== null + if (!candidateFailed) { + if (process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + const bindingPackageVersion = require('longbridge-wasm32-wasi/package.json').version + if (bindingPackageVersion !== '0.0.0') { + throw new Error(`WASI binding package version mismatch, expected 0.0.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + } } - loadErrors.push(err) + wasiBinding = require('longbridge-wasm32-wasi') + nativeBinding = wasiBinding + wasiBindingLoaded = true } + } catch (err) { + candidateError = err + candidateFailed = true + } + if (candidateFailed) { + wasiBindingErrors.push(candidateError) + loadErrors.push(candidateError) } } - if (process.env.NAPI_RS_FORCE_WASI === 'error' && !wasiBinding) { - const error = new Error('WASI binding not found and NAPI_RS_FORCE_WASI is set to error') - error.cause = wasiBindingError + if ( + !wasiBindingLoaded && + forceWasi && + !forceWasiError && + !__napiWasiFlavorRequested + ) { + nativeBinding = requireNative() + } + if ((forceWasiError || __napiWasiFlavorRequested) && !wasiBindingLoaded) { + const error = new Error( + __napiWasiFlavorRequested + ? 'WASI binding for flavor "' + __napiWasiFlavor + '" not found' + : 'WASI binding not found and NAPI_RS_FORCE_WASI is set to error', + ) + error.cause = createLoadErrorChain(wasiBindingErrors) throw error } } if (!nativeBinding) { if (loadErrors.length > 0) { - throw new Error( + const error = new Error( `Cannot find native binding. ` + `npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). ` + 'Please try `npm i` again after removing both package-lock.json and node_modules directory.', - { - cause: loadErrors.reduce((err, cur) => { - cur.cause = err - return cur - }), - }, ) + // assign instead of the `new Error(message, { cause })` options form, + // which Node < 16.9 silently ignores + error.cause = createLoadErrorChain(loadErrors) + throw error } throw new Error(`Failed to load native binding`) }