xrootd 0.1.11 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +131 -10
- package/dist/index.cjs +170 -22
- package/dist/index.d.cts +433 -24
- package/dist/index.d.mts +433 -24
- package/dist/index.mjs +170 -23
- package/package.json +3 -4
- package/prebuilds/darwin-arm64/xrootd.node +0 -0
- package/prebuilds/darwin-x64/xrootd.node +0 -0
- package/prebuilds/linux-arm64/xrootd.node +0 -0
- package/prebuilds/linux-x64/xrootd.node +0 -0
package/README.md
CHANGED
|
@@ -1,23 +1,144 @@
|
|
|
1
|
-
# XRootD
|
|
1
|
+
# 🚀 XRootD TypeScript Support (`xrootd`)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
> **Working in process**. This package is currently under active development and not ready for production use. Stay tuned!
|
|
3
|
+
A high-performance, enterprise-grade Node.js binding for the [XRootD](https://github.com/xrootd/xrootd) client library. Designed to bring seamless, extreme-throughput data access to the TypeScript ecosystem.
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
> [!NOTE]
|
|
6
|
+
> **Work in Progress**: This package is currently under active development. While core features are highly functional, the API may undergo minor refinements.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
## Supported Capabilities
|
|
10
|
+
|
|
11
|
+
* **`FileSystem`**: Cluster-level operations (`stat`, `dirList`, `rm`, `mkdir`, `locate`).
|
|
12
|
+
* **`File`**: High-performance I/O (`open`, `read`, `write`, `vectorRead`, server-side `clone`).
|
|
13
|
+
* **`CopyProcess`**: Asynchronous, highly-parallel data transfers with real-time progress callbacks.
|
|
14
|
+
* **`Env`**: Type-safe configuration management and OS-level auth protocol routing.
|
|
15
|
+
* **`Url`**: XRootD-specific URI scheme parsing and validation.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
## Prerequisites
|
|
19
|
+
|
|
20
|
+
To ensure compatibility with the pre-compiled native bindings, your environment must meet the following requirements:
|
|
21
|
+
|
|
22
|
+
* **Node.js**: Version 20.x or higher.
|
|
23
|
+
* **System Dependencies (Linux)**: Requires `libstdc++` providing `GLIBCXX_3.4.26` or newer (typically GCC 9+, e.g., CentOS 9 / Ubuntu 20.04+).
|
|
24
|
+
* **System Dependencies (Macos)**: Requires macos 14 or later for arm64 architecture, macos 15 or later for intel architecture.
|
|
25
|
+
* **Authentication**: Ensure your host machine has valid tickets (e.g., `kinit` for Kerberos) if accessing a secured cluster.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install xrootd
|
|
32
|
+
# or
|
|
33
|
+
yarn add xrootd
|
|
34
|
+
|
|
35
|
+
```
|
|
8
36
|
|
|
9
37
|
---
|
|
10
38
|
|
|
11
|
-
|
|
39
|
+
## Usage Examples
|
|
40
|
+
|
|
41
|
+
### 1. Configuration & Basic FileSystem
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { Env, FileSystem } from 'xrootd';
|
|
45
|
+
|
|
46
|
+
// Safely configure underlying XRootD environment
|
|
47
|
+
Env.configure({
|
|
48
|
+
RequestTimeout: 30, // Prevent 30-min infinite hangs (Node.js friendly)
|
|
49
|
+
WorkerThreads: 4, // Boost underlying multiplexing
|
|
50
|
+
SecProtocol: 'krb5,unix' // Force Kerberos or Unix socket auth
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const fs = new FileSystem('root://eos01.ihep.ac.cn/');
|
|
54
|
+
|
|
55
|
+
async function checkCluster() {
|
|
56
|
+
try {
|
|
57
|
+
const stat = await fs.stat('/eos/lhaaso/data');
|
|
58
|
+
console.log(`Directory size: ${stat.size} bytes`);
|
|
59
|
+
console.log(`Modified at: ${stat.modTimeAsString}`);
|
|
60
|
+
} catch (err: any) {
|
|
61
|
+
if (err.code === 'ENOENT') {
|
|
62
|
+
console.error('File not found in cluster.');
|
|
63
|
+
} else {
|
|
64
|
+
console.error(err.message);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 2. Zero-Copy File Reading
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { File, OpenFlags } from 'xrootd';
|
|
75
|
+
|
|
76
|
+
async function readHeader() {
|
|
77
|
+
const file = new File();
|
|
78
|
+
|
|
79
|
+
// Auto-translates and handles C++ lifecycle
|
|
80
|
+
await file.open('root://eos01.ihep.ac.cn//eos/test.dat', OpenFlags.Read);
|
|
81
|
+
|
|
82
|
+
// The returned buffer is a zero-copy mapping of C++ allocated memory
|
|
83
|
+
const buffer = await file.read(0n, 1024);
|
|
84
|
+
console.log(buffer.toString('utf-8'));
|
|
85
|
+
|
|
86
|
+
await file.close();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 3. High-Performance CopyProcess with Progress
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { CopyProcess } from 'xrootd';
|
|
95
|
+
|
|
96
|
+
async function syncData() {
|
|
97
|
+
const cp = new CopyProcess();
|
|
98
|
+
|
|
99
|
+
cp.addJob({
|
|
100
|
+
source: 'root://eos01.ihep.ac.cn//eos/data.raw',
|
|
101
|
+
target: '/local/disk/data.raw',
|
|
102
|
+
force: true,
|
|
103
|
+
parallelChunks: 4
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await cp.prepare();
|
|
107
|
+
|
|
108
|
+
const results = await cp.run((jobNum, processed, total) => {
|
|
109
|
+
const percent = ((processed / total) * 100).toFixed(2);
|
|
110
|
+
console.log(`Job ${jobNum} Progress: ${percent}%`);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
console.log('Copy completed:', results);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## ✨ Why `xrootd`? (Core Architecture)
|
|
119
|
+
|
|
120
|
+
Unlike traditional Node.js C++ addons, `xrootd` is architected for **High Energy Physics (HEP)** and **Big Data** workloads:
|
|
121
|
+
|
|
122
|
+
* **True Native Async**: Bypasses the notoriously limited Node.js `libuv` thread pool (default 4 threads). It hooks directly into XRootD's underlying C++ event loop via N-API `ThreadSafeFunction`, allowing thousands of concurrent requests without blocking the V8 engine.
|
|
123
|
+
* **Absolute Zero-Copy I/O**: Implements direct memory handoffs. Data read from the EOS cluster is mounted directly as V8 `Buffer` objects without a single byte of internal memory copying, completely eliminating GC (Garbage Collection) pauses during heavy I/O.
|
|
124
|
+
* **Idiomatic Node.js Experience**: "Thin C++, Thick TS". Complex XRootD protocol errors (e.g., `3011`) are smartly translated into standard Node.js exceptions (e.g., `ENOENT`, `EACCES`), complete with system call contexts and retryable flags.
|
|
125
|
+
* **Zero-Config Authentication**: Bundles essential security plugins (Kerberos, SSS, Unix, Token) natively. Path injection is handled automatically under the hood—no more `[FATAL] Auth failed` or missing `.so` nightmares.
|
|
12
126
|
|
|
13
127
|
---
|
|
14
128
|
|
|
15
|
-
|
|
129
|
+
## Licensing
|
|
130
|
+
|
|
131
|
+
This project is released under a **Dual License** strategy to balance open-source compatibility and developer freedom:
|
|
16
132
|
|
|
17
|
-
|
|
133
|
+
1. **[GNU GPLv3](https://www.google.com/search?q=https://www.gnu.org/licenses/gpl-3.0.en.html)**: The native binding codebase adheres to the GPLv3 license to remain fully compatible with the upstream C++ XRootD project.
|
|
134
|
+
2. **[MIT](https://www.google.com/search?q=https://opensource.org/licenses/MIT)**: The core TypeScript APIs, interface definitions, and glue layers are provided under the MIT license, allowing you to integrate the TS components into your own software architectures without viral restrictive requirements.
|
|
18
135
|
|
|
19
|
-
|
|
136
|
+
*Disclaimer: This project is a third-party community initiative built for modern web ecosystems and is not affiliated with, officially endorsed by, or sponsored by the core XRootD project.*
|
|
137
|
+
|
|
138
|
+
---
|
|
20
139
|
|
|
140
|
+
## Contributing
|
|
21
141
|
|
|
142
|
+
We welcome contributions from the high-energy physics, astrophysics, and Node.js communities! Whether it's reporting bugs, improving documentation, or adding support for advanced XRootD features.
|
|
22
143
|
|
|
23
|
-
|
|
144
|
+
*Contribution guidelines coming soon.*
|
package/dist/index.cjs
CHANGED
|
@@ -144,13 +144,20 @@ var File = class {
|
|
|
144
144
|
async read(offset, size) {
|
|
145
145
|
return this._internal.Read(BigInt(offset), size);
|
|
146
146
|
}
|
|
147
|
+
async write(offset, arg1, arg2, arg3) {
|
|
148
|
+
if (Buffer.isBuffer(arg1)) return this._internal.Write(BigInt(offset), arg1);
|
|
149
|
+
else if (typeof arg1 === "number" && typeof arg2 === "number") return this._internal.WriteFd(BigInt(offset), arg1, arg2, arg3 !== void 0 ? BigInt(arg3) : void 0);
|
|
150
|
+
else throw new TypeError("Invalid arguments for write");
|
|
151
|
+
}
|
|
147
152
|
/**
|
|
148
|
-
* 写入文件块
|
|
149
|
-
* @param offset
|
|
150
|
-
* @param
|
|
153
|
+
* 从本地 fd 写入文件块 (直接映射)
|
|
154
|
+
* @param offset 写入的起始字节偏移量。
|
|
155
|
+
* @param size 写入大小。
|
|
156
|
+
* @param fd 本地文件描述符 (fd)。
|
|
157
|
+
* @param fdoff 可选,从本地 fd 中读取的起始偏移。
|
|
151
158
|
*/
|
|
152
|
-
async
|
|
153
|
-
return this._internal.
|
|
159
|
+
async writeFd(offset, size, fd, fdoff) {
|
|
160
|
+
return this._internal.WriteFd(BigInt(offset), size, fd, fdoff !== void 0 ? BigInt(fdoff) : void 0);
|
|
154
161
|
}
|
|
155
162
|
/**
|
|
156
163
|
* 同步文件缓冲区到磁盘
|
|
@@ -172,7 +179,9 @@ var File = class {
|
|
|
172
179
|
return this._internal.IsOpen();
|
|
173
180
|
}
|
|
174
181
|
async getProperty(name) {
|
|
175
|
-
|
|
182
|
+
const ret = this._internal.GetProperty(name);
|
|
183
|
+
if (ret.success) return ret.value;
|
|
184
|
+
throw new Error("TODO");
|
|
176
185
|
}
|
|
177
186
|
async setProperty(name, value) {
|
|
178
187
|
return this._internal.SetProperty(name, value);
|
|
@@ -204,25 +213,37 @@ var File = class {
|
|
|
204
213
|
/**
|
|
205
214
|
* 设置文件的扩展属性
|
|
206
215
|
*/
|
|
207
|
-
async
|
|
208
|
-
return this._internal.SetXAttr(
|
|
216
|
+
async setXAttrs(attrs) {
|
|
217
|
+
return this._internal.SetXAttr(attrs);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* 设置文件的扩展属性
|
|
221
|
+
*/
|
|
222
|
+
async setXAttr(key, value) {
|
|
223
|
+
return (await this._internal.SetXAttr({ [key]: value }))[0].ok;
|
|
209
224
|
}
|
|
210
225
|
/**
|
|
211
226
|
* 获取文件的扩展属性
|
|
212
227
|
*/
|
|
213
|
-
async
|
|
214
|
-
return this._internal.GetXAttr(
|
|
228
|
+
async getXAttrs(keys) {
|
|
229
|
+
return this._internal.GetXAttr(keys);
|
|
230
|
+
}
|
|
231
|
+
async getXAttr(key) {
|
|
232
|
+
return (await this._internal.GetXAttr([key]))[key];
|
|
215
233
|
}
|
|
216
234
|
/**
|
|
217
235
|
* 删除文件的扩展属性
|
|
218
236
|
*/
|
|
219
|
-
async
|
|
220
|
-
return this._internal.DelXAttr(
|
|
237
|
+
async delXAttrs(keys) {
|
|
238
|
+
return this._internal.DelXAttr(keys);
|
|
239
|
+
}
|
|
240
|
+
async delXAttr(key) {
|
|
241
|
+
return (await this._internal.DelXAttr([key]))[0].ok;
|
|
221
242
|
}
|
|
222
243
|
/**
|
|
223
|
-
*
|
|
244
|
+
* 列出文件所有的扩展属性和内容
|
|
224
245
|
*/
|
|
225
|
-
async
|
|
246
|
+
async listXAttrs() {
|
|
226
247
|
return this._internal.ListXAttr();
|
|
227
248
|
}
|
|
228
249
|
/**
|
|
@@ -528,25 +549,34 @@ var FileSystem = class {
|
|
|
528
549
|
* @param targetPath 目标路径
|
|
529
550
|
* @param attrs 扩展属性键值对记录
|
|
530
551
|
*/
|
|
531
|
-
async
|
|
552
|
+
async setXAttrs(targetPath, attrs) {
|
|
532
553
|
return this._internal.SetXAttr(this._normalize(targetPath), attrs);
|
|
533
554
|
}
|
|
555
|
+
async setXAttr(targetPath, key, value) {
|
|
556
|
+
return (await this.setXAttrs(targetPath, { [key]: value }))[0].ok;
|
|
557
|
+
}
|
|
534
558
|
/**
|
|
535
559
|
* 获取扩展属性
|
|
536
560
|
* @param targetPath 目标路径
|
|
537
561
|
* @param keys 需要获取的属性名数组
|
|
538
562
|
*/
|
|
539
|
-
async
|
|
563
|
+
async getXAttrs(targetPath, keys) {
|
|
540
564
|
return this._internal.GetXAttr(this._normalize(targetPath), keys);
|
|
541
565
|
}
|
|
566
|
+
async getXAttr(targetPath, key) {
|
|
567
|
+
return (await this.getXAttrs(targetPath, [key]))[key];
|
|
568
|
+
}
|
|
542
569
|
/**
|
|
543
570
|
* 删除指定的扩展属性
|
|
544
571
|
* @param targetPath 目标路径
|
|
545
572
|
* @param keys 需要删除的属性名数组
|
|
546
573
|
*/
|
|
547
|
-
async
|
|
574
|
+
async delXAttrs(targetPath, keys) {
|
|
548
575
|
return this._internal.DelXAttr(this._normalize(targetPath), keys);
|
|
549
576
|
}
|
|
577
|
+
async delXAttr(targetPath, key) {
|
|
578
|
+
return (await this.delXAttrs(targetPath, [key]))[0].ok;
|
|
579
|
+
}
|
|
550
580
|
/**
|
|
551
581
|
* 列出目标文件或目录的所有扩展属性
|
|
552
582
|
*/
|
|
@@ -555,6 +585,47 @@ var FileSystem = class {
|
|
|
555
585
|
}
|
|
556
586
|
};
|
|
557
587
|
//#endregion
|
|
588
|
+
//#region lib/copy.ts
|
|
589
|
+
var CopyProcess = class {
|
|
590
|
+
nativeCp;
|
|
591
|
+
constructor() {
|
|
592
|
+
this.nativeCp = new nativeAddon.CopyProcess();
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Add a job to the copy process synchronously
|
|
596
|
+
*/
|
|
597
|
+
addJob(config) {
|
|
598
|
+
this.nativeCp.AddJob(config);
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Asynchronously prepare the jobs (resolve DNS, check endpoints, etc.)
|
|
602
|
+
*/
|
|
603
|
+
async prepare() {
|
|
604
|
+
return this.nativeCp.Prepare();
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Run the copy process.
|
|
608
|
+
* @param onProgress Optional progress callback
|
|
609
|
+
* @returns Array of results corresponding to the jobs added
|
|
610
|
+
*/
|
|
611
|
+
async run(onProgress) {
|
|
612
|
+
if (onProgress) this.nativeCp.SetEventListener("progress", onProgress);
|
|
613
|
+
return this.nativeCp.Run();
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Abort the running copy process.
|
|
617
|
+
*/
|
|
618
|
+
abort() {
|
|
619
|
+
this.nativeCp.CancelJob();
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Abort the running copy process. (alias for abort)
|
|
623
|
+
*/
|
|
624
|
+
cancelJob() {
|
|
625
|
+
this.nativeCp.CancelJob();
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
//#endregion
|
|
558
629
|
//#region lib/url.ts
|
|
559
630
|
/**
|
|
560
631
|
* 纯 TypeScript 实现的 XRootD URL 解析器
|
|
@@ -656,14 +727,91 @@ var XRootDUrl = class XRootDUrl {
|
|
|
656
727
|
};
|
|
657
728
|
//#endregion
|
|
658
729
|
//#region lib/env.ts
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
730
|
+
function checkIntRange(key, value) {
|
|
731
|
+
const num = Number(value);
|
|
732
|
+
if (num < -2147483648 || num > 2147483647) console.warn(`[xrootd] Warning: value ${value} for key "${key}" is out of 32-bit signed integer range [-2147483648, 2147483647]. It will be truncated.`);
|
|
733
|
+
}
|
|
734
|
+
var XRootDEnvironment = class {
|
|
735
|
+
constructor() {}
|
|
736
|
+
/**
|
|
737
|
+
* 设置整数配置项,返回是否设置成功。
|
|
738
|
+
*/
|
|
739
|
+
putInt(key, intVal) {
|
|
740
|
+
checkIntRange(key, intVal);
|
|
741
|
+
const success = nativeAddon.Env.PutInt(key, Number(intVal));
|
|
742
|
+
if (!success) console.warn(`[xrootd] Warning: Failed to set integer configuration "${key}"=${intVal}. It might have been overridden by a system environment variable.`);
|
|
743
|
+
return success;
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* 设置字符串配置项,返回是否设置成功。
|
|
747
|
+
*/
|
|
748
|
+
putString(key, strVal) {
|
|
749
|
+
if (key === "SecProtocol") {
|
|
750
|
+
process.env.XrdSecPROTOCOL = strVal;
|
|
751
|
+
process.env.XRD_SECPROTOCOL = strVal;
|
|
752
|
+
return true;
|
|
753
|
+
}
|
|
754
|
+
const success = nativeAddon.Env.PutString(key, strVal);
|
|
755
|
+
if (!success) console.warn(`[xrootd] Warning: Failed to set string configuration "${key}"="${strVal}". It might have been overridden by a system environment variable.`);
|
|
756
|
+
return success;
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* 设置布尔配置项,返回是否设置成功。
|
|
760
|
+
*/
|
|
761
|
+
putBoolean(key, boolVal) {
|
|
762
|
+
if (typeof boolVal === "boolean") boolVal = boolVal ? "true" : "false";
|
|
763
|
+
return this.putString(key, boolVal);
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* 获取整数配置项。
|
|
767
|
+
*/
|
|
768
|
+
getInt(key) {
|
|
769
|
+
return nativeAddon.Env.GetInt(key) ?? void 0;
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* 获取字符串配置项。
|
|
773
|
+
*/
|
|
774
|
+
getString(key) {
|
|
775
|
+
if (key === "SecProtocol") return process.env.XrdSecPROTOCOL ?? process.env.XRD_SECPROTOCOL ?? void 0;
|
|
776
|
+
return nativeAddon.Env.GetString(key) ?? void 0;
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* 批量安全地设置 XRootD 底层参数
|
|
780
|
+
*/
|
|
781
|
+
configure(config) {
|
|
782
|
+
for (const [key, value] of Object.entries(config)) {
|
|
783
|
+
if (value === void 0 || value === null) continue;
|
|
784
|
+
let success = true;
|
|
785
|
+
if (key === "SecProtocol") {
|
|
786
|
+
process.env.XrdSecPROTOCOL = String(value);
|
|
787
|
+
process.env.XRD_SECPROTOCOL = String(value);
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
if (typeof value === "number" || typeof value === "bigint") {
|
|
791
|
+
checkIntRange(key, value);
|
|
792
|
+
success = nativeAddon.Env.PutInt(key, Number(value));
|
|
793
|
+
} else if (typeof value === "string") success = nativeAddon.Env.PutString(key, value);
|
|
794
|
+
else if (typeof value === "boolean") success = nativeAddon.Env.PutString(key, value ? "true" : "false");
|
|
795
|
+
else {
|
|
796
|
+
console.warn(`[xrootd] Unhandled type ${typeof value} for key ${key}.`);
|
|
797
|
+
success = nativeAddon.Env.PutString(key, value.toString());
|
|
798
|
+
}
|
|
799
|
+
if (!success) console.warn(`[xrootd] Warning: Failed to set configuration "${key}"=${value}. It might have been overridden by a system environment variable.`);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
get(key) {
|
|
803
|
+
if (key === "SecProtocol") return process.env.XrdSecPROTOCOL ?? process.env.XRD_SECPROTOCOL ?? void 0;
|
|
804
|
+
return nativeAddon.Env.GetInt(key) ?? nativeAddon.Env.GetString(key) ?? void 0;
|
|
805
|
+
}
|
|
664
806
|
};
|
|
807
|
+
const Env = new XRootDEnvironment();
|
|
808
|
+
Env.configure({
|
|
809
|
+
RequestTimeout: 30,
|
|
810
|
+
WorkerThreads: 4
|
|
811
|
+
});
|
|
665
812
|
//#endregion
|
|
666
813
|
exports.AccessMode = AccessMode;
|
|
814
|
+
exports.CopyProcess = CopyProcess;
|
|
667
815
|
exports.Env = Env;
|
|
668
816
|
exports.File = File;
|
|
669
817
|
exports.FileSystem = FileSystem;
|