This is an automated email from the ASF dual-hosted git repository.
xuanwo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opendal.git
The following commit(s) were added to refs/heads/main by this push:
new fc5725a2a feat(binding/nodejs): add TimeoutLayer, LoggingLayer and
ThrottleLayer in nodejs binding (#6772)
fc5725a2a is described below
commit fc5725a2a007fbf8e322d05596e72f5a787c7ce4
Author: Kilerd Chan <[email protected]>
AuthorDate: Fri Nov 14 18:03:56 2025 +0800
feat(binding/nodejs): add TimeoutLayer, LoggingLayer and ThrottleLayer in
nodejs binding (#6772)
* feat: add TimeoutLayer, LoggingLayer and ThrottleLayer in nodejs binding
* refactor: remove redundant param check in binding
---
bindings/nodejs/Cargo.toml | 1 +
bindings/nodejs/README.md | 155 ++++++++++++++++++++
bindings/nodejs/generated.d.ts | 136 ++++++++++++++++++
bindings/nodejs/generated.js | 3 +
bindings/nodejs/src/layer.rs | 207 +++++++++++++++++++++++++++
bindings/nodejs/tests/suites/layer.suite.mjs | 59 +++++++-
6 files changed, 560 insertions(+), 1 deletion(-)
diff --git a/bindings/nodejs/Cargo.toml b/bindings/nodejs/Cargo.toml
index bdce8f7a2..f35f9f0f0 100644
--- a/bindings/nodejs/Cargo.toml
+++ b/bindings/nodejs/Cargo.toml
@@ -161,6 +161,7 @@ napi-derive = "3.2.2"
# this crate won't be published, we always use the local version
opendal = { version = ">=0", path = "../../core", features = [
"blocking",
+ "layers-throttle",
] }
tokio = "1"
diff --git a/bindings/nodejs/README.md b/bindings/nodejs/README.md
index 2f2545c8c..fcbd5c51e 100644
--- a/bindings/nodejs/README.md
+++ b/bindings/nodejs/README.md
@@ -65,6 +65,161 @@ async function main() {
main();
```
+## Layers
+
+OpenDAL provides layers to add additional functionality to operators. You can
chain multiple layers together to build powerful data access pipelines.
+
+### Available Layers
+
+- **RetryLayer** - Retry failed operations automatically
+- **ConcurrentLimitLayer** - Limit concurrent requests
+- **TimeoutLayer** - Add timeout for operations
+- **LoggingLayer** - Log all operations
+- **ThrottleLayer** - Limit bandwidth usage
+
+### TimeoutLayer
+
+Prevents operations from hanging indefinitely:
+
+```javascript
+import { Operator, TimeoutLayer } from "opendal";
+
+const op = new Operator("fs", { root: "/tmp" });
+
+const timeout = new TimeoutLayer();
+timeout.timeout = 10000; // 10 seconds for non-IO operations (ms)
+timeout.ioTimeout = 3000; // 3 seconds for IO operations (ms)
+op.layer(timeout.build());
+```
+
+**Default values:** timeout: 60s, ioTimeout: 10s
+
+### LoggingLayer
+
+Add structured logging for debugging and monitoring:
+
+```javascript
+import { Operator, LoggingLayer } from "opendal";
+
+const op = new Operator("fs", { root: "/tmp" });
+
+const logging = new LoggingLayer();
+op.layer(logging.build());
+
+// All operations will be logged
+await op.write("test.txt", "Hello World");
+```
+
+Enable logging output:
+
+```bash
+# Show debug logs
+RUST_LOG=debug node app.js
+
+# Show only OpenDAL logs
+RUST_LOG=opendal::services=debug node app.js
+```
+
+### ThrottleLayer
+
+Control bandwidth usage with rate limiting:
+
+```javascript
+import { Operator, ThrottleLayer } from "opendal";
+
+const op = new Operator("s3", {
+ bucket: "my-bucket",
+ region: "us-east-1",
+});
+
+// Limit to 10 KiB/s with 10 MiB burst
+const throttle = new ThrottleLayer(10 * 1024, 10 * 1024 * 1024);
+op.layer(throttle.build());
+```
+
+**Parameters:**
+
+- `bandwidth`: Maximum bytes per second
+- `burst`: Maximum burst size (must be larger than any operation size)
+
+### RetryLayer
+
+Automatically retry temporary failed operations:
+
+```javascript
+import { Operator, RetryLayer } from "opendal";
+
+const op = new Operator("s3", {
+ bucket: "my-bucket",
+ region: "us-east-1",
+});
+
+const retry = new RetryLayer();
+retry.maxTimes = 3;
+retry.jitter = true;
+op.layer(retry.build());
+```
+
+### ConcurrentLimitLayer
+
+Limit concurrent requests to storage services:
+
+```javascript
+import { Operator, ConcurrentLimitLayer } from "opendal";
+
+const op = new Operator("s3", {
+ bucket: "my-bucket",
+ region: "us-east-1",
+});
+
+// Allow max 1024 concurrent operations
+const limit = new ConcurrentLimitLayer(1024);
+limit.httpPermits = 512; // Limit HTTP requests separately
+op.layer(limit.build());
+```
+
+### Combining Multiple Layers
+
+Stack multiple layers for comprehensive control:
+
+```javascript
+import {
+ Operator,
+ LoggingLayer,
+ TimeoutLayer,
+ RetryLayer,
+ ThrottleLayer,
+} from "opendal";
+
+const op = new Operator("s3", {
+ bucket: "my-bucket",
+ region: "us-east-1",
+});
+
+// Layer 1: Logging for observability
+const logging = new LoggingLayer();
+op.layer(logging.build());
+
+// Layer 2: Timeout protection
+const timeout = new TimeoutLayer();
+timeout.timeout = 30000;
+timeout.ioTimeout = 10000;
+op.layer(timeout.build());
+
+// Layer 3: Retry on failures
+const retry = new RetryLayer();
+retry.maxTimes = 3;
+retry.jitter = true;
+op.layer(retry.build());
+
+// Layer 4: Bandwidth throttling
+const throttle = new ThrottleLayer(100 * 1024, 10 * 1024 * 1024);
+op.layer(throttle.build());
+
+// Now the operator has full production-ready protection
+await op.write("data.json", JSON.stringify(data));
+```
+
## Usage with Next.js
Config automatically be bundled by
[Next.js](https://nextjs.org/docs/app/api-reference/config/next-config-js/serverExternalPackages).
diff --git a/bindings/nodejs/generated.d.ts b/bindings/nodejs/generated.d.ts
index 3473095c4..68ba74179 100644
--- a/bindings/nodejs/generated.d.ts
+++ b/bindings/nodejs/generated.d.ts
@@ -293,6 +293,44 @@ export declare class Lister {
next(): Promise<Entry | null>
}
+/**
+ * Logging layer
+ *
+ * Add log for every operation.
+ *
+ * # Logging
+ *
+ * - OpenDAL will log in structural way.
+ * - Every operation will start with a `started` log entry.
+ * - Every operation will finish with the following status:
+ * - `succeeded`: the operation is successful, but might have more to take.
+ * - `finished`: the whole operation is finished.
+ * - `failed`: the operation returns an unexpected error.
+ * - The default log level while expected error happened is `Warn`.
+ * - The default log level while unexpected failure happened is `Error`.
+ *
+ * # Examples
+ *
+ * ```javascript
+ * const op = new Operator("fs", { root: "/tmp" })
+ *
+ * const logging = new LoggingLayer();
+ * op.layer(logging.build());
+ * ```
+ *
+ * # Output
+ *
+ * To enable logging output, set the `RUST_LOG` environment variable:
+ *
+ * ```shell
+ * RUST_LOG=debug node app.js
+ * ```
+ */
+export declare class LoggingLayer {
+ constructor()
+ build(): ExternalObject<Layer>
+}
+
/** Metadata carries all metadata associated with a path. */
export declare class Metadata {
/** Returns true if the <op.stat> object describes a file system directory.
*/
@@ -876,6 +914,104 @@ export declare class RetryLayer {
build(): ExternalObject<Layer>
}
+/**
+ * Throttle layer
+ *
+ * Add a bandwidth rate limiter to the underlying services.
+ *
+ * # Throttle
+ *
+ * There are several algorithms when it come to rate limiting techniques.
+ * This throttle layer uses Generic Cell Rate Algorithm (GCRA) provided by
Governor.
+ * By setting the `bandwidth` and `burst`, we can control the byte flow rate
of underlying services.
+ *
+ * # Note
+ *
+ * When setting the ThrottleLayer, always consider the largest possible
operation size as the burst size,
+ * as **the burst size should be larger than any possible byte length to allow
it to pass through**.
+ *
+ * # Examples
+ *
+ * This example limits bandwidth to 10 KiB/s and burst size to 10 MiB.
+ *
+ * ```javascript
+ * const op = new Operator("fs", { root: "/tmp" })
+ *
+ * const throttle = new ThrottleLayer(10 * 1024, 10000 * 1024);
+ * op.layer(throttle.build());
+ * ```
+ */
+export declare class ThrottleLayer {
+ /**
+ * Create a new `ThrottleLayer` with given bandwidth and burst.
+ *
+ * # Arguments
+ *
+ * - `bandwidth`: the maximum number of bytes allowed to pass through per
second.
+ * - `burst`: the maximum number of bytes allowed to pass through at once.
+ *
+ * # Notes
+ *
+ * Validation (bandwidth and burst must be greater than 0) is handled by the
Rust core layer.
+ */
+ constructor(bandwidth: number, burst: number)
+ build(): ExternalObject<Layer>
+}
+
+/**
+ * Timeout layer
+ *
+ * Add timeout for every operation to avoid slow or unexpected hang operations.
+ *
+ * # Notes
+ *
+ * `TimeoutLayer` treats all operations in two kinds:
+ *
+ * - Non IO Operation like `stat`, `delete` they operate on a single file. We
control
+ * them by setting `timeout`.
+ * - IO Operation like `read`, `Reader::read` and `Writer::write`, they
operate on data directly, we
+ * control them by setting `io_timeout`.
+ *
+ * # Default
+ *
+ * - timeout: 60 seconds
+ * - io_timeout: 10 seconds
+ *
+ * # Examples
+ *
+ * ```javascript
+ * const op = new Operator("fs", { root: "/tmp" })
+ *
+ * const timeout = new TimeoutLayer();
+ * timeout.timeout = 10000; // 10 seconds for non-IO ops (in milliseconds)
+ * timeout.ioTimeout = 3000; // 3 seconds for IO ops (in milliseconds)
+ *
+ * op.layer(timeout.build());
+ * ```
+ */
+export declare class TimeoutLayer {
+ constructor()
+ /**
+ * Set timeout for non-IO operations (stat, delete, etc.)
+ *
+ * # Notes
+ *
+ * - The unit is millisecond.
+ * - Default is 60000ms (60 seconds).
+ */
+ set timeout(v: number)
+ /**
+ * Set timeout for IO operations (read, write, etc.)
+ *
+ * # Notes
+ *
+ * - The unit is millisecond.
+ * - Default is 10000ms (10 seconds).
+ */
+ set ioTimeout(v: number)
+ build(): ExternalObject<Layer>
+}
+
/**
* Writer is designed to write data into a given path in an asynchronous
* manner.
diff --git a/bindings/nodejs/generated.js b/bindings/nodejs/generated.js
index 5f1b7e986..68f81d9ff 100644
--- a/bindings/nodejs/generated.js
+++ b/bindings/nodejs/generated.js
@@ -535,9 +535,12 @@ module.exports.ConcurrentLimitLayer =
nativeBinding.ConcurrentLimitLayer
module.exports.Entry = nativeBinding.Entry
module.exports.Layer = nativeBinding.Layer
module.exports.Lister = nativeBinding.Lister
+module.exports.LoggingLayer = nativeBinding.LoggingLayer
module.exports.Metadata = nativeBinding.Metadata
module.exports.Operator = nativeBinding.Operator
module.exports.Reader = nativeBinding.Reader
module.exports.RetryLayer = nativeBinding.RetryLayer
+module.exports.ThrottleLayer = nativeBinding.ThrottleLayer
+module.exports.TimeoutLayer = nativeBinding.TimeoutLayer
module.exports.Writer = nativeBinding.Writer
module.exports.EntryMode = nativeBinding.EntryMode
diff --git a/bindings/nodejs/src/layer.rs b/bindings/nodejs/src/layer.rs
index 80b2c65d7..ff6e9dd2b 100644
--- a/bindings/nodejs/src/layer.rs
+++ b/bindings/nodejs/src/layer.rs
@@ -233,3 +233,210 @@ impl ConcurrentLimitLayer {
External::new(Layer { inner: Box::new(l) })
}
}
+
+impl NodeLayer for opendal::layers::TimeoutLayer {
+ fn layer(&self, op: opendal::Operator) -> opendal::Operator {
+ op.layer(self.clone())
+ }
+}
+
+/// Timeout layer
+///
+/// Add timeout for every operation to avoid slow or unexpected hang
operations.
+///
+/// # Notes
+///
+/// `TimeoutLayer` treats all operations in two kinds:
+///
+/// - Non IO Operation like `stat`, `delete` they operate on a single file. We
control
+/// them by setting `timeout`.
+/// - IO Operation like `read`, `Reader::read` and `Writer::write`, they
operate on data directly, we
+/// control them by setting `io_timeout`.
+///
+/// # Default
+///
+/// - timeout: 60 seconds
+/// - io_timeout: 10 seconds
+///
+/// # Examples
+///
+/// ```javascript
+/// const op = new Operator("fs", { root: "/tmp" })
+///
+/// const timeout = new TimeoutLayer();
+/// timeout.timeout = 10000; // 10 seconds for non-IO ops (in
milliseconds)
+/// timeout.ioTimeout = 3000; // 3 seconds for IO ops (in milliseconds)
+///
+/// op.layer(timeout.build());
+/// ```
+#[derive(Default)]
+#[napi]
+pub struct TimeoutLayer {
+ timeout: Option<f64>,
+ io_timeout: Option<f64>,
+}
+
+#[napi]
+impl TimeoutLayer {
+ #[napi(constructor)]
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ /// Set timeout for non-IO operations (stat, delete, etc.)
+ ///
+ /// # Notes
+ ///
+ /// - The unit is millisecond.
+ /// - Default is 60000ms (60 seconds).
+ #[napi(setter)]
+ pub fn timeout(&mut self, v: f64) {
+ self.timeout = Some(v);
+ }
+
+ /// Set timeout for IO operations (read, write, etc.)
+ ///
+ /// # Notes
+ ///
+ /// - The unit is millisecond.
+ /// - Default is 10000ms (10 seconds).
+ #[napi(setter)]
+ pub fn io_timeout(&mut self, v: f64) {
+ self.io_timeout = Some(v);
+ }
+
+ #[napi]
+ pub fn build(&self) -> External<Layer> {
+ let mut l = opendal::layers::TimeoutLayer::default();
+
+ if let Some(timeout) = self.timeout {
+ l = l.with_timeout(Duration::from_millis(timeout as u64));
+ }
+ if let Some(io_timeout) = self.io_timeout {
+ l = l.with_io_timeout(Duration::from_millis(io_timeout as u64));
+ }
+
+ External::new(Layer { inner: Box::new(l) })
+ }
+}
+
+impl NodeLayer for opendal::layers::LoggingLayer {
+ fn layer(&self, op: opendal::Operator) -> opendal::Operator {
+ op.layer(opendal::layers::LoggingLayer::default())
+ }
+}
+
+/// Logging layer
+///
+/// Add log for every operation.
+///
+/// # Logging
+///
+/// - OpenDAL will log in structural way.
+/// - Every operation will start with a `started` log entry.
+/// - Every operation will finish with the following status:
+/// - `succeeded`: the operation is successful, but might have more to take.
+/// - `finished`: the whole operation is finished.
+/// - `failed`: the operation returns an unexpected error.
+/// - The default log level while expected error happened is `Warn`.
+/// - The default log level while unexpected failure happened is `Error`.
+///
+/// # Examples
+///
+/// ```javascript
+/// const op = new Operator("fs", { root: "/tmp" })
+///
+/// const logging = new LoggingLayer();
+/// op.layer(logging.build());
+/// ```
+///
+/// # Output
+///
+/// To enable logging output, set the `RUST_LOG` environment variable:
+///
+/// ```shell
+/// RUST_LOG=debug node app.js
+/// ```
+#[napi]
+pub struct LoggingLayer;
+
+impl Default for LoggingLayer {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+#[napi]
+impl LoggingLayer {
+ #[napi(constructor)]
+ pub fn new() -> Self {
+ Self
+ }
+
+ #[napi]
+ pub fn build(&self) -> External<Layer> {
+ let l = opendal::layers::LoggingLayer::default();
+ External::new(Layer { inner: Box::new(l) })
+ }
+}
+
+impl NodeLayer for opendal::layers::ThrottleLayer {
+ fn layer(&self, op: opendal::Operator) -> opendal::Operator {
+ op.layer(self.clone())
+ }
+}
+
+/// Throttle layer
+///
+/// Add a bandwidth rate limiter to the underlying services.
+///
+/// # Throttle
+///
+/// There are several algorithms when it come to rate limiting techniques.
+/// This throttle layer uses Generic Cell Rate Algorithm (GCRA) provided by
Governor.
+/// By setting the `bandwidth` and `burst`, we can control the byte flow rate
of underlying services.
+///
+/// # Note
+///
+/// When setting the ThrottleLayer, always consider the largest possible
operation size as the burst size,
+/// as **the burst size should be larger than any possible byte length to
allow it to pass through**.
+///
+/// # Examples
+///
+/// This example limits bandwidth to 10 KiB/s and burst size to 10 MiB.
+///
+/// ```javascript
+/// const op = new Operator("fs", { root: "/tmp" })
+///
+/// const throttle = new ThrottleLayer(10 * 1024, 10000 * 1024);
+/// op.layer(throttle.build());
+/// ```
+#[napi]
+pub struct ThrottleLayer {
+ bandwidth: u32,
+ burst: u32,
+}
+
+#[napi]
+impl ThrottleLayer {
+ /// Create a new `ThrottleLayer` with given bandwidth and burst.
+ ///
+ /// # Arguments
+ ///
+ /// - `bandwidth`: the maximum number of bytes allowed to pass through per
second.
+ /// - `burst`: the maximum number of bytes allowed to pass through at once.
+ ///
+ /// # Notes
+ ///
+ /// Validation (bandwidth and burst must be greater than 0) is handled by
the Rust core layer.
+ #[napi(constructor)]
+ pub fn new(bandwidth: u32, burst: u32) -> Self {
+ Self { bandwidth, burst }
+ }
+
+ #[napi]
+ pub fn build(&self) -> External<Layer> {
+ let l = opendal::layers::ThrottleLayer::new(self.bandwidth,
self.burst);
+ External::new(Layer { inner: Box::new(l) })
+ }
+}
diff --git a/bindings/nodejs/tests/suites/layer.suite.mjs
b/bindings/nodejs/tests/suites/layer.suite.mjs
index dc9f0307c..9eb6c68b9 100644
--- a/bindings/nodejs/tests/suites/layer.suite.mjs
+++ b/bindings/nodejs/tests/suites/layer.suite.mjs
@@ -19,7 +19,7 @@
import { test, assert } from 'vitest'
-import { RetryLayer, ConcurrentLimitLayer } from '../../index.mjs'
+import { RetryLayer, ConcurrentLimitLayer, TimeoutLayer, LoggingLayer,
ThrottleLayer } from '../../index.mjs'
/**
* @param {import("../../index").Operator} op
@@ -52,4 +52,61 @@ export function run(op) {
assert.ok(layerOp)
assert.ok(layerOp.capability())
})
+
+ test('test operator with timeout layer', () => {
+ const timeoutLayer = new TimeoutLayer()
+ timeoutLayer.timeout = 10000
+ timeoutLayer.ioTimeout = 5000
+
+ const layerOp = op.layer(timeoutLayer.build())
+
+ assert.ok(layerOp)
+ assert.ok(layerOp.capability())
+ })
+
+ test('test operator with timeout layer using default values', () => {
+ const timeoutLayer = new TimeoutLayer()
+
+ const layerOp = op.layer(timeoutLayer.build())
+
+ assert.ok(layerOp)
+ assert.ok(layerOp.capability())
+ })
+
+ test('test operator with logging layer', () => {
+ const loggingLayer = new LoggingLayer()
+
+ const layerOp = op.layer(loggingLayer.build())
+
+ assert.ok(layerOp)
+ assert.ok(layerOp.capability())
+ })
+
+ test('test operator with throttle layer', () => {
+ const throttleLayer = new ThrottleLayer(10 * 1024, 1024 * 1024)
+
+ const layerOp = op.layer(throttleLayer.build())
+
+ assert.ok(layerOp)
+ assert.ok(layerOp.capability())
+ })
+
+ test('test operator with multiple layers', () => {
+ const loggingLayer = new LoggingLayer()
+ const timeoutLayer = new TimeoutLayer()
+ timeoutLayer.timeout = 30000
+ timeoutLayer.ioTimeout = 10000
+ const retryLayer = new RetryLayer()
+ retryLayer.maxTimes = 3
+ const throttleLayer = new ThrottleLayer(100 * 1024, 10 * 1024 * 1024)
+
+ const layerOp = op
+ .layer(loggingLayer.build())
+ .layer(timeoutLayer.build())
+ .layer(retryLayer.build())
+ .layer(throttleLayer.build())
+
+ assert.ok(layerOp)
+ assert.ok(layerOp.capability())
+ })
}