Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a4b3695510 | |||
| 8f50039a68 | |||
| 99f1b2e720 | |||
| 54ecc811f7 | |||
| 0b7a506fde | |||
| 61f016f08c | |||
| 6cd0ea45d6 | |||
| 1322e4a0d3 | |||
| db377ead3c | |||
| 3fcd27f41a | |||
| c896af234d | |||
| d1fc0dba87 | |||
| e697ab060c | |||
| cf59b4c8fb | |||
| feadfc3456 | |||
| 2c2f8f41a1 | |||
| a2380282ff | |||
| 19773fddc9 | |||
| 6e2fccd04e | |||
| 3970b8e0a8 | |||
| 89a72cf8a9 | |||
| 0ef8dd61a8 | |||
| dad098ea3b | |||
| f534248bec | |||
| 05fa7f52dd | |||
| 96535147fb | |||
| f0b088f6f8 | |||
| 1d177f5438 | |||
| cefc56a6bb | |||
| 7205cc1ea3 | |||
| aa7cdbd36f | |||
| 1b86a9252d | |||
| e9fd148235 | |||
| 34ea1ed8ec | |||
| aa92fb6d0d | |||
| fbbea7b42b | |||
| b2859710cd | |||
| bc0ce7159c | |||
| 4614f99358 | |||
| 1ecc55f8aa | |||
| ae0f24ccb2 | |||
| 060c68cd05 | |||
| e85eba4cea | |||
| 206467e1fa | |||
| a98394b9b9 | |||
| c448811aa9 | |||
| c3225a90c7 | |||
| 89acf780bf | |||
| e5f4793370 | |||
| 95fe697501 | |||
| ee2d2c7238 | |||
| 1dfa277279 | |||
| 78a8952383 | |||
| fcc50847e4 | |||
| f8d93991f5 | |||
| bee9f783d9 | |||
| 3e1c8d563e | |||
| 1299febcdc | |||
| be94c62760 | |||
| 6a862ef243 | |||
| ae2de5fc62 | |||
| df0bbc7327 | |||
| d94761c866 | |||
| f8235e1a59 | |||
| 647cadf497 | |||
| 8c793a81b6 | |||
| 6a42ba7e43 | |||
| 14b3790251 | |||
| 61d81bed62 | |||
| 1a10bc1a5f | |||
| 7f68d08134 | |||
| ab20cd896f | |||
| 5a9e93d6e7 | |||
| b51641dc7e | |||
| 45f1257896 | |||
| 3e2b8b1e3a | |||
| 90d81617ef | |||
| 64c62e616b | |||
| 2c340e37c7 | |||
| 7853e94d2e | |||
| 99bf57b154 | |||
| 0fa6eaf95b | |||
| 76f42be740 | |||
| d99dc41be9 | |||
| 263508b8f7 | |||
| 0c2cca30ed | |||
| 46fdf668c6 | |||
| f8a92a45a0 | |||
| cec70e6036 | |||
| f9e08ba628 | |||
| c12a078149 | |||
| dedd803dc3 | |||
| e8e927a491 | |||
| d950bbac23 | |||
| fc8da2ebf5 | |||
| f6e50c405f |
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
package.json
|
||||||
|
package-lock.json
|
||||||
@@ -13,3 +13,91 @@ Role: Principal Systems Architect & Lead Software Engineer.Objective: Implement
|
|||||||
|
|
||||||
|
|
||||||
Create a walkthrough for Julia service-A service sending a mix-content chat message to Julia service-B. the chat message must includes
|
Create a walkthrough for Julia service-A service sending a mix-content chat message to Julia service-B. the chat message must includes
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
I updated the following:
|
||||||
|
- NATSBridge.jl. Essentially I add NATS_connection keyword and new publish_message function to support the keyword.
|
||||||
|
Use them and ONLY them as ground truth.
|
||||||
|
Then update the following files accordingly:
|
||||||
|
- architecture.md
|
||||||
|
- implementation.md
|
||||||
|
|
||||||
|
All API should be semantically consistent and naming should be consistent across the board.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Task: Update NATSBridge.js to reflect recent changes in NATSBridge.jl and docs
|
||||||
|
Context: NATSBridge.jl and docs has been updated.
|
||||||
|
Requirements:
|
||||||
|
Source of Truth: Treat the updated NATSBridge.jl and docs as the definitive source.
|
||||||
|
API Consistency: Ensure the Main Package API (e.g., smartsend(), publish_message()) uses consistent naming across all three supported languages.
|
||||||
|
Ecosystem Variance: Low-level native functions (e.g., NATS.connect(), JSON.read()) should follow the conventions of the specific language ecosystem and do not require cross-language consistency.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
I'm expanding this Julia package (NATSBridge) into a cross-platform project by adding a JavaScript and Python/MicroPython implementation. To ensure accuracy, the Julia src directory will serve as the ground truth, as the documentation may be outdated.
|
||||||
|
|
||||||
|
My goal is to maintain interface parity at the high-level API for a consistent user experience, while ensuring the low-level implementation adheres strictly to the idiomatic conventions of each respective language (e.g., multiple dispatch in Julia vs. asynchronous, prototype, or class-based patterns in JS and Python/MicroPython)
|
||||||
|
|
||||||
|
Now, help me do the following:
|
||||||
|
1) check architecture.md for any mistake.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Help me expands this Julia package (NATSBridge) into a cross-platform project by adding a JavaScript and Python/MicroPython implementation. To ensure accuracy, NATSBridge.jl will serve as the ground truth, as the documentation may be outdated.
|
||||||
|
|
||||||
|
My goal is to maintain interface parity at the high-level API for a consistent user experience, while ensuring the low-level implementation adheres strictly to the idiomatic conventions of each respective language (e.g., multiple dispatch in Julia vs. asynchronous, prototype, or class-based patterns in JS and Python/MicroPython)
|
||||||
|
|
||||||
|
Now do the following:
|
||||||
|
1) check docs to see if there is any mistake.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
I'm expanding this Julia package (NATSBridge) into a cross-platform project by adding
|
||||||
|
a JavaScript, Python and MicroPython implementation.
|
||||||
|
The following will serve as the ground truth:
|
||||||
|
- test_julia_mix_payloads_sender.jl
|
||||||
|
- NATSBridge.jl
|
||||||
|
- test_julia_mix_payloads_receiver.jl
|
||||||
|
- architecture.md
|
||||||
|
|
||||||
|
My goal is to maintain interface parity at the high-level API for a consistent user experience,
|
||||||
|
while ensuring the low-level implementation adheres strictly to the idiomatic conventions of each
|
||||||
|
respective language (e.g., multiple dispatch in Julia vs. asynchronous, prototype, or class-based
|
||||||
|
patterns in JS, Python and MicroPython)
|
||||||
|
|
||||||
|
Now, help me do the following:
|
||||||
|
1) Check whether natsbridge.js needs update or it already up to date.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name = "NATSBridge"
|
name = "NATSBridge"
|
||||||
uuid = "f2724d33-f338-4a57-b9f8-1be882570d10"
|
uuid = "f2724d33-f338-4a57-b9f8-1be882570d10"
|
||||||
version = "0.4.2"
|
version = "0.5.4"
|
||||||
authors = ["narawat <narawat@gmail.com>"]
|
authors = ["narawat <narawat@gmail.com>"]
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|||||||
672
README.md
Normal file
672
README.md
Normal file
@@ -0,0 +1,672 @@
|
|||||||
|
# NATSBridge - Cross-Platform Bi-Directional Data Bridge
|
||||||
|
|
||||||
|
A high-performance, bi-directional data bridge for **Julia, JavaScript, Python, and MicroPython** applications using NATS (Core & JetStream), implementing the Claim-Check pattern for large payloads.
|
||||||
|
|
||||||
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
[](https://nats.io)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Overview](#overview)
|
||||||
|
- [Cross-Platform Support](#cross-platform-support)
|
||||||
|
- [Features](#features)
|
||||||
|
- [Quick Start](#quick-start)
|
||||||
|
- [API Reference](#api-reference)
|
||||||
|
- [Payload Types](#payload-types)
|
||||||
|
- [Cross-Platform Examples](#cross-platform-examples)
|
||||||
|
- [Testing](#testing)
|
||||||
|
- [Documentation](#documentation)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
NATSBridge enables seamless communication across multiple platforms through NATS, with intelligent transport selection based on payload size:
|
||||||
|
|
||||||
|
| Transport | Payload Size | Method |
|
||||||
|
|-----------|--------------|--------|
|
||||||
|
| **Direct** | < 1MB | Sent directly via NATS (Base64 encoded) |
|
||||||
|
| **Link** | >= 1MB | Uploaded to HTTP file server, URL sent via NATS |
|
||||||
|
|
||||||
|
### Use Cases
|
||||||
|
|
||||||
|
- **Chat Applications**: Text, images, audio, video in a single message
|
||||||
|
- **File Transfer**: Efficient transfer of large files using claim-check pattern
|
||||||
|
- **IoT/Embedded**: Sensor data, telemetry, and analytics pipelines (MicroPython)
|
||||||
|
- **Cross-Platform Communication**: Interoperability between Julia, JavaScript, Python, and MicroPython systems
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-Platform Support
|
||||||
|
|
||||||
|
| Platform | Implementation | Features |
|
||||||
|
|----------|----------------|----------|
|
||||||
|
| **Julia** | [`src/NATSBridge.jl`](src/NATSBridge.jl) | Full feature set, Arrow IPC, multiple dispatch |
|
||||||
|
| **JavaScript** | [`src/natsbridge.js`](src/natsbridge.js) | Node.js, async/await |
|
||||||
|
| **JavaScript (Browser)** | [`src/natsbridge_csr.js`](src/natsbridge_csr.js) | Browser, WebSocket NATS, async/await |
|
||||||
|
| **Python** | [`src/natsbridge.py`](src/natsbridge.py) | Desktop Python, asyncio, type hints |
|
||||||
|
| **MicroPython** | [`src/natsbridge_mpy.py`](src/natsbridge_mpy.py) | Memory-constrained, synchronous API |
|
||||||
|
|
||||||
|
### Platform Comparison
|
||||||
|
|
||||||
|
| Feature | Julia | JavaScript | JavaScript (Browser) | Python | MicroPython |
|
||||||
|
|---------|-------|------------|----------------------|--------|-------------|
|
||||||
|
| Multiple Dispatch | ✅ Native | ❌ | ❌ | ❌ | ❌ |
|
||||||
|
| Async/Await | ❌ | ✅ Native | ✅ Native | ✅ Native | ⚠️ (uasyncio) |
|
||||||
|
| Type Safety | ✅ Strong | ⚠️ (TypeScript) | ⚠️ (TypeScript) | ✅ (Type hints) | ❌ |
|
||||||
|
| Arrow IPC | ✅ Native | ✅ | ✅ | ✅ | ❌ |
|
||||||
|
| Direct Transport | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Link Transport | ✅ | ✅ | ✅ | ✅ | ⚠️ (Limited) |
|
||||||
|
| Handler Functions | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Cross-Platform API | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| WebSocket NATS | ❌ | ❌ | ✅ | ❌ | ❌ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- ✅ **Cross-platform messaging** for Julia, JavaScript, Python, and MicroPython applications
|
||||||
|
- ✅ **Bi-directional messaging** with request-reply patterns
|
||||||
|
- ✅ **Multi-payload support** - send multiple payloads with different types in one message
|
||||||
|
- ✅ **Automatic transport selection** - direct vs link based on payload size
|
||||||
|
- ✅ **Claim-Check pattern** for payloads > 1MB
|
||||||
|
- ✅ **Apache Arrow IPC** support for tabular data (zero-copy reading)
|
||||||
|
- ✅ **Exponential backoff** for reliable file server downloads
|
||||||
|
- ✅ **Correlation ID tracking** for message tracing
|
||||||
|
- ✅ **Reply-to support** for request-response patterns
|
||||||
|
- ✅ **Handler function abstraction** - pluggable file server implementations (Plik, AWS S3, custom)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Step 1: Start NATS Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -p 4222:4222 nats:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Start HTTP File Server (Optional)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a directory for file uploads
|
||||||
|
mkdir -p /tmp/fileserver
|
||||||
|
|
||||||
|
# Start HTTP file server
|
||||||
|
python3 -m http.server 8080 --directory /tmp/fileserver
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Send Your First Message
|
||||||
|
|
||||||
|
#### Julia
|
||||||
|
|
||||||
|
```julia
|
||||||
|
using NATSBridge
|
||||||
|
|
||||||
|
data = [("message", "Hello World", "text")]
|
||||||
|
env, env_json_str = smartsend("/chat/room1", data, broker_url="nats://localhost:4222")
|
||||||
|
println("Message sent!")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JavaScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const NATSBridge = require('./src/natsbridge.js');
|
||||||
|
|
||||||
|
const data = [["message", "Hello World", "text"]];
|
||||||
|
const [env, env_json_str] = await NATSBridge.smartsend(
|
||||||
|
"/chat/room1",
|
||||||
|
data,
|
||||||
|
{ broker_url: "nats://localhost:4222" }
|
||||||
|
);
|
||||||
|
console.log("Message sent!");
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
from natsbridge import smartsend
|
||||||
|
|
||||||
|
data = [("message", "Hello World", "text")]
|
||||||
|
env, env_json_str = await smartsend(
|
||||||
|
"/chat/room1",
|
||||||
|
data,
|
||||||
|
broker_url="nats://localhost:4222"
|
||||||
|
)
|
||||||
|
print("Message sent!")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Unified API Standard
|
||||||
|
|
||||||
|
All platforms use the same input/output format for payloads:
|
||||||
|
|
||||||
|
**Input format for smartsend:**
|
||||||
|
```
|
||||||
|
[(dataname1, data1, type1), (dataname2, data2, type2), ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output format for smartreceive:**
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"correlation_id": "...",
|
||||||
|
"msg_id": "...",
|
||||||
|
"timestamp": "...",
|
||||||
|
"send_to": "...",
|
||||||
|
"msg_purpose": "...",
|
||||||
|
"sender_name": "...",
|
||||||
|
"sender_id": "...",
|
||||||
|
"receiver_name": "...",
|
||||||
|
"receiver_id": "...",
|
||||||
|
"reply_to": "...",
|
||||||
|
"reply_to_msg_id": "...",
|
||||||
|
"broker_url": "...",
|
||||||
|
"metadata": {...},
|
||||||
|
"payloads": [(dataname1, data1, type1), (dataname2, data2, type2), ...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### smartsend
|
||||||
|
|
||||||
|
Sends data either directly via NATS or via a fileserver URL, depending on payload size.
|
||||||
|
|
||||||
|
#### Julia
|
||||||
|
|
||||||
|
```julia
|
||||||
|
using NATSBridge
|
||||||
|
|
||||||
|
env, env_json_str = NATSBridge.smartsend(
|
||||||
|
subject::String,
|
||||||
|
data::AbstractArray{Tuple{String, Any, String}};
|
||||||
|
broker_url::String = "nats://localhost:4222",
|
||||||
|
fileserver_url = "http://localhost:8080",
|
||||||
|
fileserver_upload_handler::Function = plik_oneshot_upload,
|
||||||
|
size_threshold::Int = 1_000_000,
|
||||||
|
correlation_id::String = string(uuid4()),
|
||||||
|
msg_purpose::String = "chat",
|
||||||
|
sender_name::String = "NATSBridge",
|
||||||
|
receiver_name::String = "",
|
||||||
|
receiver_id::String = "",
|
||||||
|
reply_to::String = "",
|
||||||
|
reply_to_msg_id::String = "",
|
||||||
|
is_publish::Bool = true,
|
||||||
|
NATS_connection::Union{NATS.Connection, Nothing} = nothing,
|
||||||
|
msg_id::String = string(uuid4()),
|
||||||
|
sender_id::String = string(uuid4())
|
||||||
|
)
|
||||||
|
# Returns: ::Tuple{msg_envelope_v1, String}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JavaScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const NATSBridge = require('natsbridge');
|
||||||
|
|
||||||
|
const [env, env_json_str] = await NATSBridge.smartsend(
|
||||||
|
subject,
|
||||||
|
data, // Array of [dataname, data, type] tuples
|
||||||
|
{
|
||||||
|
broker_url: 'nats://localhost:4222',
|
||||||
|
fileserver_url: 'http://localhost:8080',
|
||||||
|
fileserver_upload_handler: NATSBridge.plikOneshotUpload,
|
||||||
|
size_threshold: 1_000_000,
|
||||||
|
correlation_id: uuidv4(),
|
||||||
|
msg_purpose: 'chat',
|
||||||
|
sender_name: 'NATSBridge',
|
||||||
|
receiver_name: '',
|
||||||
|
receiver_id: '',
|
||||||
|
reply_to: '',
|
||||||
|
reply_to_msg_id: '',
|
||||||
|
is_publish: true,
|
||||||
|
nats_connection: null,
|
||||||
|
msg_id: uuidv4(),
|
||||||
|
sender_id: uuidv4()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// Returns: Promise<[env, env_json_str]>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
from natsbridge import NATSBridge
|
||||||
|
|
||||||
|
env, env_json_str = await NATSBridge.smartsend(
|
||||||
|
subject: str,
|
||||||
|
data: List[Tuple[str, Any, str]],
|
||||||
|
broker_url: str = "nats://localhost:4222",
|
||||||
|
fileserver_url: str = "http://localhost:8080",
|
||||||
|
fileserver_upload_handler: Callable = plik_oneshot_upload,
|
||||||
|
size_threshold: int = 1_000_000,
|
||||||
|
correlation_id: str = None,
|
||||||
|
msg_purpose: str = "chat",
|
||||||
|
sender_name: str = "NATSBridge",
|
||||||
|
receiver_name: str = "",
|
||||||
|
receiver_id: str = "",
|
||||||
|
reply_to: str = "",
|
||||||
|
reply_to_msg_id: str = "",
|
||||||
|
is_publish: bool = True,
|
||||||
|
nats_connection: Any = None,
|
||||||
|
msg_id: str = None,
|
||||||
|
sender_id: str = None
|
||||||
|
)
|
||||||
|
# Returns: Tuple[Dict, str]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### MicroPython
|
||||||
|
|
||||||
|
```python
|
||||||
|
from natsbridge import NATSBridge
|
||||||
|
|
||||||
|
# Limited to direct transport (< 100KB threshold)
|
||||||
|
env, env_json_str = NATSBridge.smartsend(
|
||||||
|
subject,
|
||||||
|
data, # List of (dataname, data, type) tuples
|
||||||
|
broker_url="nats://localhost:4222",
|
||||||
|
size_threshold=100000 # Lower threshold for memory constraints
|
||||||
|
)
|
||||||
|
# Returns: Tuple[Dict, str]
|
||||||
|
```
|
||||||
|
|
||||||
|
### smartreceive
|
||||||
|
|
||||||
|
Receives and processes messages from NATS, handling both direct and link transport.
|
||||||
|
|
||||||
|
#### Julia
|
||||||
|
|
||||||
|
```julia
|
||||||
|
using NATSBridge
|
||||||
|
|
||||||
|
env = NATSBridge.smartreceive(
|
||||||
|
msg::NATS.Msg;
|
||||||
|
fileserver_download_handler::Function = _fetch_with_backoff,
|
||||||
|
max_retries::Int = 5,
|
||||||
|
base_delay::Int = 100,
|
||||||
|
max_delay::Int = 5000
|
||||||
|
)
|
||||||
|
# Returns: ::JSON.Object{String, Any}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JavaScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const env = await NATSBridge.smartreceive(
|
||||||
|
msg,
|
||||||
|
{
|
||||||
|
fileserver_download_handler: NATSBridge.fetchWithBackoff,
|
||||||
|
max_retries: 5,
|
||||||
|
base_delay: 100,
|
||||||
|
max_delay: 5000
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// Returns: Promise<env_object>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
env = await NATSBridge.smartreceive(
|
||||||
|
msg,
|
||||||
|
fileserver_download_handler=fetch_with_backoff,
|
||||||
|
max_retries=5,
|
||||||
|
base_delay=100,
|
||||||
|
max_delay=5000
|
||||||
|
)
|
||||||
|
# Returns: Dict with "payloads" key
|
||||||
|
```
|
||||||
|
|
||||||
|
#### MicroPython
|
||||||
|
|
||||||
|
```python
|
||||||
|
env = NATSBridge.smartreceive(
|
||||||
|
msg,
|
||||||
|
fileserver_download_handler=_sync_fileserver_download,
|
||||||
|
max_retries=3,
|
||||||
|
base_delay=100,
|
||||||
|
max_delay=1000
|
||||||
|
)
|
||||||
|
# Returns: Dict with "payloads" key
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Payload Types
|
||||||
|
|
||||||
|
| Type | Julia | JavaScript | Python | MicroPython | Description |
|
||||||
|
|------|-------|------------|--------|-------------|-------------|
|
||||||
|
| `text` | `String` | `string` | `str` | `str` | Plain text strings |
|
||||||
|
| `dictionary` | `Dict`, `NamedTuple` | `Object`, `Array` | `dict`, `list` | `dict` | JSON-serializable dictionaries |
|
||||||
|
| `arrowtable` | `DataFrame`, `Arrow.Table` | `Array<Object>` | `pandas.DataFrame` | ❌ | Tabular data (Arrow IPC) |
|
||||||
|
| `jsontable` | `Vector{NamedTuple}` | `Array<Object>` | `list[dict]` | ❌ | Tabular data (JSON) |
|
||||||
|
| `image` | `Vector{UInt8}` | `Uint8Array`, `Buffer` | `bytes` | `bytearray` | Image data (PNG, JPG) |
|
||||||
|
| `audio` | `Vector{UInt8}` | `Uint8Array`, `Buffer` | `bytes` | `bytearray` | Audio data (WAV, MP3) |
|
||||||
|
| `video` | `Vector{UInt8}` | `Uint8Array`, `Buffer` | `bytes` | `bytearray` | Video data (MP4, AVI) |
|
||||||
|
| `binary` | `Vector{UInt8}`, `IOBuffer` | `Uint8Array`, `Buffer` | `bytes`, `bytearray` | `bytearray` | Generic binary data |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-Platform Examples
|
||||||
|
|
||||||
|
### Example 1: Chat with Mixed Content
|
||||||
|
|
||||||
|
Send text, image, and large file in one message.
|
||||||
|
|
||||||
|
#### Julia
|
||||||
|
|
||||||
|
```julia
|
||||||
|
using NATSBridge
|
||||||
|
|
||||||
|
data = [
|
||||||
|
("message_text", "Hello!", "text"),
|
||||||
|
("user_avatar", image_data, "image"),
|
||||||
|
("large_document", large_file_data, "binary")
|
||||||
|
]
|
||||||
|
|
||||||
|
env, env_json_str = NATSBridge.smartsend("/chat/room1", data; fileserver_url="http://localhost:8080")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JavaScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const NATSBridge = require('natsbridge');
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
["message_text", "Hello!", "text"],
|
||||||
|
["user_avatar", imageData, "image"],
|
||||||
|
["large_document", largeFileData, "binary"]
|
||||||
|
];
|
||||||
|
|
||||||
|
const [env, env_json_str] = await NATSBridge.smartsend(
|
||||||
|
"/chat/room1",
|
||||||
|
data,
|
||||||
|
{ fileserver_url: 'http://localhost:8080' }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
from natsbridge import NATSBridge
|
||||||
|
|
||||||
|
data = [
|
||||||
|
("message_text", "Hello!", "text"),
|
||||||
|
("user_avatar", image_data, "image"),
|
||||||
|
("large_document", large_file_data, "binary")
|
||||||
|
]
|
||||||
|
|
||||||
|
env, env_json_str = await NATSBridge.smartsend(
|
||||||
|
"/chat/room1",
|
||||||
|
data,
|
||||||
|
fileserver_url="http://localhost:8080"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Dictionary Exchange
|
||||||
|
|
||||||
|
Send configuration data between platforms.
|
||||||
|
|
||||||
|
#### Julia
|
||||||
|
|
||||||
|
```julia
|
||||||
|
using NATSBridge
|
||||||
|
|
||||||
|
config = Dict(
|
||||||
|
"wifi_ssid" => "MyNetwork",
|
||||||
|
"wifi_password" => "password123",
|
||||||
|
"update_interval" => 60
|
||||||
|
)
|
||||||
|
|
||||||
|
data = [("config", config, "dictionary")]
|
||||||
|
env, env_json_str = NATSBridge.smartsend("/device/config", data)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JavaScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const NATSBridge = require('natsbridge');
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
wifi_ssid: "MyNetwork",
|
||||||
|
wifi_password: "password123",
|
||||||
|
update_interval: 60
|
||||||
|
};
|
||||||
|
|
||||||
|
const [env, env_json_str] = await NATSBridge.smartsend(
|
||||||
|
"/device/config",
|
||||||
|
[["config", config, "dictionary"]]
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
from natsbridge import NATSBridge
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"wifi_ssid": "MyNetwork",
|
||||||
|
"wifi_password": "password123",
|
||||||
|
"update_interval": 60
|
||||||
|
}
|
||||||
|
|
||||||
|
data = [("config", config, "dictionary")]
|
||||||
|
env, env_json_str = await NATSBridge.smartsend("/device/config", data)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: Table Data (Arrow IPC)
|
||||||
|
|
||||||
|
Send tabular data using Apache Arrow IPC format.
|
||||||
|
|
||||||
|
#### Julia
|
||||||
|
|
||||||
|
```julia
|
||||||
|
using NATSBridge
|
||||||
|
using DataFrames
|
||||||
|
|
||||||
|
df = DataFrame(
|
||||||
|
id = [1, 2, 3],
|
||||||
|
name = ["Alice", "Bob", "Charlie"],
|
||||||
|
score = [95, 88, 92]
|
||||||
|
)
|
||||||
|
|
||||||
|
data = [("students", df, "arrowtable")]
|
||||||
|
env, env_json_str = NATSBridge.smartsend("/data/analysis", data)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JavaScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const NATSBridge = require('natsbridge');
|
||||||
|
|
||||||
|
const df = [
|
||||||
|
{ id: 1, name: "Alice", score: 95 },
|
||||||
|
{ id: 2, name: "Bob", score: 88 },
|
||||||
|
{ id: 3, name: "Charlie", score: 92 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const [env, env_json_str] = await NATSBridge.smartsend(
|
||||||
|
"/data/analysis",
|
||||||
|
[["students", df, "arrowtable"]]
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
from natsbridge import NATSBridge
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
df = pd.DataFrame({
|
||||||
|
"id": [1, 2, 3],
|
||||||
|
"name": ["Alice", "Bob", "Charlie"],
|
||||||
|
"score": [95, 88, 92]
|
||||||
|
})
|
||||||
|
|
||||||
|
data = [("students", df, "arrowtable")]
|
||||||
|
env, env_json_str = await NATSBridge.smartsend("/data/analysis", data)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 4: Request-Response Pattern
|
||||||
|
|
||||||
|
Bi-directional communication with reply-to support.
|
||||||
|
|
||||||
|
#### Julia
|
||||||
|
|
||||||
|
```julia
|
||||||
|
using NATSBridge
|
||||||
|
|
||||||
|
# Requester
|
||||||
|
env, env_json_str = NATSBridge.smartsend(
|
||||||
|
"/device/command",
|
||||||
|
[("command", Dict("action" => "read_sensor"), "dictionary")];
|
||||||
|
broker_url="nats://localhost:4222",
|
||||||
|
reply_to="/device/response"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JavaScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const NATSBridge = require('natsbridge');
|
||||||
|
|
||||||
|
// Requester
|
||||||
|
const [env, env_json_str] = await NATSBridge.smartsend(
|
||||||
|
"/device/command",
|
||||||
|
[["command", { action: "read_sensor" }, "dictionary"]],
|
||||||
|
{ broker_url: 'nats://localhost:4222', reply_to: '/device/response' }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
from natsbridge import NATSBridge
|
||||||
|
|
||||||
|
# Requester
|
||||||
|
env, env_json_str = await NATSBridge.smartsend(
|
||||||
|
"/device/command",
|
||||||
|
[("command", {"action": "read_sensor"}, "dictionary")],
|
||||||
|
broker_url="nats://localhost:4222",
|
||||||
|
reply_to="/device/response"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Test File Organization
|
||||||
|
|
||||||
|
| Platform | Sender Tests | Receiver Tests |
|
||||||
|
|----------|--------------|----------------|
|
||||||
|
| **Julia** | `test/test_julia_*_sender.jl` | `test/test_julia_*_receiver.jl` |
|
||||||
|
| **JavaScript** | `test/test_js_*_sender.js` | `test/test_js_*_receiver.js` |
|
||||||
|
| **Python** | `test/test_py_*_sender.py` | `test/test_py_*_receiver.py` |
|
||||||
|
|
||||||
|
### Run Tests
|
||||||
|
|
||||||
|
#### Julia
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Text message exchange
|
||||||
|
julia test/test_julia_text_sender.jl
|
||||||
|
julia test/test_julia_text_receiver.jl
|
||||||
|
|
||||||
|
# Dictionary exchange
|
||||||
|
julia test/test_julia_dict_sender.jl
|
||||||
|
julia test/test_julia_dict_receiver.jl
|
||||||
|
|
||||||
|
# File transfer
|
||||||
|
julia test/test_julia_file_sender.jl
|
||||||
|
julia test/test_julia_file_receiver.jl
|
||||||
|
|
||||||
|
# Mixed payload types
|
||||||
|
julia test/test_julia_mix_payloads_sender.jl
|
||||||
|
julia test/test_julia_mix_payloads_receiver.jl
|
||||||
|
|
||||||
|
# Table exchange
|
||||||
|
julia test/test_julia_table_sender.jl
|
||||||
|
julia test/test_julia_table_receiver.jl
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JavaScript (Node.js)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Text message exchange
|
||||||
|
node test/test_js_text_sender.js
|
||||||
|
node test/test_js_text_receiver.js
|
||||||
|
|
||||||
|
# Dictionary exchange
|
||||||
|
node test/test_js_dictionary_sender.js
|
||||||
|
node test/test_js_dictionary_receiver.js
|
||||||
|
|
||||||
|
# Binary transfer
|
||||||
|
node test/test_js_binary_sender.js
|
||||||
|
node test/test_js_binary_receiver.js
|
||||||
|
|
||||||
|
# Table exchange
|
||||||
|
node test/test_js_table_sender.js
|
||||||
|
node test/test_js_table_receiver.js
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Python
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Text message exchange
|
||||||
|
python3 test/test_py_text_sender.py
|
||||||
|
python3 test/test_py_text_receiver.py
|
||||||
|
|
||||||
|
# Dictionary exchange
|
||||||
|
python3 test/test_py_dictionary_sender.py
|
||||||
|
python3 test/test_py_dictionary_receiver.py
|
||||||
|
|
||||||
|
# Binary transfer
|
||||||
|
python3 test/test_py_binary_sender.py
|
||||||
|
python3 test/test_py_binary_receiver.py
|
||||||
|
|
||||||
|
# Table exchange
|
||||||
|
python3 test/test_py_table_sender.py
|
||||||
|
python3 test/test_py_table_receiver.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
For detailed architecture and implementation information, see:
|
||||||
|
|
||||||
|
- [Architecture Documentation](docs/architecture_updated.md) - Cross-platform architecture, API parity, platform-specific patterns
|
||||||
|
- [Implementation Guide](docs/implementation_updated.md) - Detailed implementation for each platform, handler functions, testing
|
||||||
|
- [Tutorial](docs/tutorial_updated.md) - Step-by-step getting started guide
|
||||||
|
- [Walkthrough](docs/walkthrough_updated.md) - Real-world application building guides
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 NATSBridge Contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
741
docs/tutorial.md
Normal file
741
docs/tutorial.md
Normal file
@@ -0,0 +1,741 @@
|
|||||||
|
# Cross-Platform NATSBridge Tutorial
|
||||||
|
|
||||||
|
A step-by-step guide to get started with NATSBridge across **Julia**, **JavaScript**, and **Python/MicroPython**.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Overview](#overview)
|
||||||
|
2. [Prerequisites](#prerequisites)
|
||||||
|
3. [Installation](#installation)
|
||||||
|
4. [Quick Start](#quick-start)
|
||||||
|
5. [Basic Examples](#basic-examples)
|
||||||
|
6. [Advanced Usage](#advanced-usage)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
NATSBridge enables seamless communication across platforms through NATS, with automatic transport selection based on payload size:
|
||||||
|
|
||||||
|
- **Direct Transport**: Payloads < 1MB are sent directly via NATS (Base64 encoded)
|
||||||
|
- **Link Transport**: Payloads >= 1MB are uploaded to an HTTP file server and referenced via URL
|
||||||
|
|
||||||
|
### Cross-Platform API Parity
|
||||||
|
|
||||||
|
All three platforms use the same high-level API:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Input format
|
||||||
|
smartsend(subject, [(dataname, data, type), ...], options)
|
||||||
|
|
||||||
|
# Output format
|
||||||
|
(env, env_json_str) = smartsend(...)
|
||||||
|
env = smartreceive(msg, options)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important Platform Differences:**
|
||||||
|
|
||||||
|
1. **Encoding field:** Julia and JavaScript preserve the original serialization format in the encoding field (`"base64"`, `"json"`, or `"arrow-ipc"`), while Python and MicroPython always use `"base64"` for all direct transport payloads.
|
||||||
|
|
||||||
|
2. **Async vs Sync:** JavaScript and Python desktop use async/await, while MicroPython uses synchronous API.
|
||||||
|
|
||||||
|
### Supported Payload Types
|
||||||
|
|
||||||
|
| Type | Julia | JavaScript | Python | MicroPython |
|
||||||
|
|------|-------|------------|--------|-------------|
|
||||||
|
| `text` | `String` | `string` | `str` | `str` |
|
||||||
|
| `dictionary` | `Dict` | `Object` | `dict` | `dict` |
|
||||||
|
| `arrowtable` | `DataFrame` | `Array<Object>` | `pandas.DataFrame` | ❌ |
|
||||||
|
| `jsontable` | `Vector{NamedTuple}` | `Array<Object>` | `list[dict]` | ❌ |
|
||||||
|
| `table` | ❌ | ❌ | `pandas.DataFrame` | ❌ |
|
||||||
|
| `image` | `Vector{UInt8}` | `Uint8Array` | `bytes` | `bytearray` |
|
||||||
|
| `audio` | `Vector{UInt8}` | `Uint8Array` | `bytes` | `bytearray` |
|
||||||
|
| `video` | `Vector{UInt8}` | `Uint8Array` | `bytes` | `bytearray` |
|
||||||
|
| `binary` | `Vector{UInt8}` | `Uint8Array` | `bytes` | `bytearray` |
|
||||||
|
|
||||||
|
**Note on MicroPython:** MicroPython does not support table types (`arrowtable`, `jsontable`, or `table`) due to memory constraints. Use `dictionary` or `binary` instead.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before you begin, ensure you have:
|
||||||
|
|
||||||
|
1. **NATS Server** running (or accessible)
|
||||||
|
2. **HTTP File Server** (optional, for large payloads > 1MB)
|
||||||
|
3. **Platform-specific packages** installed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Julia
|
||||||
|
|
||||||
|
```julia
|
||||||
|
using Pkg
|
||||||
|
Pkg.add("NATS")
|
||||||
|
Pkg.add("Arrow")
|
||||||
|
Pkg.add("JSON3")
|
||||||
|
Pkg.add("HTTP")
|
||||||
|
Pkg.add("UUIDs")
|
||||||
|
Pkg.add("Dates")
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript (Node.js)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install nats uuid apache-arrow node-fetch
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript (Browser)
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script src="https://unpkg.com/nats-js/dist/bundle/nats.min.js"></script>
|
||||||
|
<script src="https://unpkg.com/apache-arrow/arrow.min.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python (Desktop)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install nats-py aiohttp pyarrow pandas
|
||||||
|
```
|
||||||
|
|
||||||
|
### MicroPython
|
||||||
|
|
||||||
|
Uses built-in modules: `network`, `socket`, `time`, `json`, `base64`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Step 1: Start NATS Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -p 4222:4222 nats:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Start HTTP File Server (Optional)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /tmp/fileserver
|
||||||
|
python3 -m http.server 8080 --directory /tmp/fileserver
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Send Your First Message
|
||||||
|
|
||||||
|
#### Julia
|
||||||
|
|
||||||
|
```julia
|
||||||
|
using NATSBridge
|
||||||
|
|
||||||
|
# Send a text message
|
||||||
|
data = [("message", "Hello World", "text")]
|
||||||
|
env, env_json_str = smartsend("/chat/room1", data, broker_url="nats://localhost:4222")
|
||||||
|
# env: msg_envelope_v1 struct with all metadata and payloads
|
||||||
|
# env_json_str: JSON string representation of the envelope for publishing
|
||||||
|
println("Message sent!")
|
||||||
|
|
||||||
|
# Or use is_publish=false to get envelope and JSON without publishing
|
||||||
|
env, env_json_str = smartsend("/chat/room1", data, broker_url="nats://localhost:4222", is_publish=false)
|
||||||
|
# env: msg_envelope_v1 struct
|
||||||
|
# env_json_str: JSON string for publishing to NATS
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JavaScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const NATSBridge = require('./src/natsbridge.js');
|
||||||
|
|
||||||
|
// Send a text message
|
||||||
|
const data = [["message", "Hello World", "text"]];
|
||||||
|
const [env, env_json_str] = await NATSBridge.smartsend(
|
||||||
|
"/chat/room1",
|
||||||
|
data,
|
||||||
|
{ broker_url: "nats://localhost:4222" }
|
||||||
|
);
|
||||||
|
// env: Object with all metadata and payloads
|
||||||
|
// env_json_str: JSON string for publishing
|
||||||
|
console.log("Message sent!");
|
||||||
|
|
||||||
|
// Or use is_publish=false
|
||||||
|
const [env, env_json_str] = await NATSBridge.smartsend(
|
||||||
|
"/chat/room1",
|
||||||
|
data,
|
||||||
|
{ broker_url: "nats://localhost:4222", is_publish: false }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
from natsbridge import smartsend
|
||||||
|
|
||||||
|
# Send a text message
|
||||||
|
data = [("message", "Hello World", "text")]
|
||||||
|
env, env_json_str = await smartsend(
|
||||||
|
"/chat/room1",
|
||||||
|
data,
|
||||||
|
broker_url="nats://localhost:4222"
|
||||||
|
)
|
||||||
|
# env: Dict with all metadata and payloads
|
||||||
|
# env_json_str: JSON string for publishing
|
||||||
|
print("Message sent!")
|
||||||
|
|
||||||
|
# Or use is_publish=False
|
||||||
|
env, env_json_str = await smartsend(
|
||||||
|
"/chat/room1",
|
||||||
|
data,
|
||||||
|
broker_url="nats://localhost:4222",
|
||||||
|
is_publish=False
|
||||||
|
)
|
||||||
|
# env: Dict with all metadata and payloads
|
||||||
|
# env_json_str: JSON string for publishing to NATS
|
||||||
|
```
|
||||||
|
|
||||||
|
#### MicroPython
|
||||||
|
|
||||||
|
```python
|
||||||
|
from natsbridge_mpy import NATSBridge
|
||||||
|
|
||||||
|
bridge = NATSBridge()
|
||||||
|
|
||||||
|
# Send a text message (limited to small payloads)
|
||||||
|
data = [("message", "Hello World", "text")]
|
||||||
|
env, env_json_str = bridge.smartsend(
|
||||||
|
"/chat/room1",
|
||||||
|
data,
|
||||||
|
size_threshold=100000 # Lower threshold for MicroPython
|
||||||
|
)
|
||||||
|
print("Message sent!")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Receive Messages
|
||||||
|
|
||||||
|
#### Julia
|
||||||
|
|
||||||
|
```julia
|
||||||
|
using NATSBridge
|
||||||
|
|
||||||
|
# Receive and process message
|
||||||
|
env = smartreceive(msg; fileserver_download_handler=_fetch_with_backoff)
|
||||||
|
# Returns: ::JSON.Object{String, Any} with "payloads" field containing Vector{Tuple{String, Any, String}}
|
||||||
|
# Access payloads: for (dataname, data, type) in env["payloads"]
|
||||||
|
for (dataname, data, type) in env["payloads"]
|
||||||
|
println("Received $dataname: $data")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JavaScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const NATSBridge = require('./src/natsbridge.js');
|
||||||
|
|
||||||
|
// Receive and process message
|
||||||
|
const env = await NATSBridge.smartreceive(msg, {
|
||||||
|
fileserver_download_handler: NATSBridge.fetchWithBackoff
|
||||||
|
});
|
||||||
|
// env.payloads = [[dataname, data, type], ...]
|
||||||
|
for (const [dataname, data, type] of env.payloads) {
|
||||||
|
console.log(`Received ${dataname}:`, data);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
from natsbridge import smartreceive, fetch_with_backoff
|
||||||
|
|
||||||
|
# Receive and process message
|
||||||
|
env = await smartreceive(
|
||||||
|
msg,
|
||||||
|
fileserver_download_handler=fetch_with_backoff
|
||||||
|
)
|
||||||
|
# env["payloads"] = [(dataname, data, type), ...]
|
||||||
|
for dataname, data, type_ in env["payloads"]:
|
||||||
|
print(f"Received {dataname}: {data}")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Basic Examples
|
||||||
|
|
||||||
|
### Example 1: Sending a Dictionary
|
||||||
|
|
||||||
|
#### Julia
|
||||||
|
|
||||||
|
```julia
|
||||||
|
using NATSBridge
|
||||||
|
|
||||||
|
config = Dict(
|
||||||
|
"wifi_ssid" => "MyNetwork",
|
||||||
|
"wifi_password" => "password123",
|
||||||
|
"update_interval" => 60
|
||||||
|
)
|
||||||
|
|
||||||
|
data = [("config", config, "dictionary")]
|
||||||
|
env, env_json_str = smartsend("/device/config", data, broker_url="nats://localhost:4222")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JavaScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const NATSBridge = require('./src/natsbridge.js');
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
wifi_ssid: "MyNetwork",
|
||||||
|
wifi_password: "password123",
|
||||||
|
update_interval: 60
|
||||||
|
};
|
||||||
|
|
||||||
|
const data = [["config", config, "dictionary"]];
|
||||||
|
const [env, env_json_str] = await NATSBridge.smartsend(
|
||||||
|
"/device/config",
|
||||||
|
data,
|
||||||
|
{ broker_url: "nats://localhost:4222" }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
from natsbridge import smartsend
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"wifi_ssid": "MyNetwork",
|
||||||
|
"wifi_password": "password123",
|
||||||
|
"update_interval": 60
|
||||||
|
}
|
||||||
|
|
||||||
|
data = [("config", config, "dictionary")]
|
||||||
|
env, env_json_str = await smartsend(
|
||||||
|
"/device/config",
|
||||||
|
data,
|
||||||
|
broker_url="nats://localhost:4222"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### MicroPython
|
||||||
|
|
||||||
|
```python
|
||||||
|
from natsbridge_mpy import NATSBridge
|
||||||
|
|
||||||
|
bridge = NATSBridge()
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"wifi_ssid": "MyNetwork",
|
||||||
|
"wifi_password": "password123",
|
||||||
|
"update_interval": 60
|
||||||
|
}
|
||||||
|
|
||||||
|
data = [("config", config, "dictionary")]
|
||||||
|
env, env_json_str = bridge.smartsend(
|
||||||
|
"/device/config",
|
||||||
|
data,
|
||||||
|
size_threshold=100000
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Sending Binary Data (Image)
|
||||||
|
|
||||||
|
#### Julia
|
||||||
|
|
||||||
|
```julia
|
||||||
|
using NATSBridge
|
||||||
|
|
||||||
|
# Read image file
|
||||||
|
image_data = read("image.png")
|
||||||
|
|
||||||
|
data = [("user_image", image_data, "binary")]
|
||||||
|
env, env_json_str = smartsend("/chat/image", data, broker_url="nats://localhost:4222")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JavaScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const NATSBridge = require('./src/natsbridge.js');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
// Read image file
|
||||||
|
const image_data = fs.readFileSync('image.png');
|
||||||
|
|
||||||
|
const data = [["user_image", image_data, "binary"]];
|
||||||
|
const [env, env_json_str] = await NATSBridge.smartsend(
|
||||||
|
"/chat/image",
|
||||||
|
data,
|
||||||
|
{ broker_url: "nats://localhost:4222" }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
from natsbridge import smartsend
|
||||||
|
|
||||||
|
# Read image file
|
||||||
|
with open("image.png", "rb") as f:
|
||||||
|
image_data = f.read()
|
||||||
|
|
||||||
|
data = [("user_image", image_data, "binary")]
|
||||||
|
env, env_json_str = await smartsend(
|
||||||
|
"/chat/image",
|
||||||
|
data,
|
||||||
|
broker_url="nats://localhost:4222"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### MicroPython
|
||||||
|
|
||||||
|
```python
|
||||||
|
from natsbridge_mpy import NATSBridge
|
||||||
|
|
||||||
|
bridge = NATSBridge()
|
||||||
|
|
||||||
|
# Read image file
|
||||||
|
with open("image.png", "rb") as f:
|
||||||
|
image_data = f.read()
|
||||||
|
|
||||||
|
data = [("user_image", image_data, "binary")]
|
||||||
|
env, env_json_str = bridge.smartsend(
|
||||||
|
"/chat/image",
|
||||||
|
data,
|
||||||
|
size_threshold=100000
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: Request-Response Pattern
|
||||||
|
|
||||||
|
#### Julia (Requester)
|
||||||
|
|
||||||
|
```julia
|
||||||
|
using NATSBridge
|
||||||
|
|
||||||
|
# Send command with reply-to
|
||||||
|
data = [("command", Dict("action" => "read_sensor"), "dictionary")]
|
||||||
|
env, env_json_str = smartsend(
|
||||||
|
"/device/command",
|
||||||
|
data,
|
||||||
|
broker_url="nats://localhost:4222",
|
||||||
|
reply_to="/device/response",
|
||||||
|
reply_to_msg_id="cmd-001"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JavaScript (Requester)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const NATSBridge = require('./src/natsbridge.js');
|
||||||
|
|
||||||
|
// Send command with reply-to
|
||||||
|
const data = [["command", { action: "read_sensor" }, "dictionary"]];
|
||||||
|
const [env, env_json_str] = await NATSBridge.smartsend(
|
||||||
|
"/device/command",
|
||||||
|
data,
|
||||||
|
{
|
||||||
|
broker_url: "nats://localhost:4222",
|
||||||
|
reply_to: "/device/response",
|
||||||
|
reply_to_msg_id: "cmd-001"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Python (Requester)
|
||||||
|
|
||||||
|
```python
|
||||||
|
from natsbridge import smartsend
|
||||||
|
|
||||||
|
# Send command with reply-to
|
||||||
|
data = [("command", {"action": "read_sensor"}, "dictionary")]
|
||||||
|
env, env_json_str = await smartsend(
|
||||||
|
"/device/command",
|
||||||
|
data,
|
||||||
|
broker_url="nats://localhost:4222",
|
||||||
|
reply_to="/device/response",
|
||||||
|
reply_to_msg_id="cmd-001"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Julia (Responder)
|
||||||
|
|
||||||
|
```julia
|
||||||
|
using NATSBridge, NATS
|
||||||
|
|
||||||
|
const SUBJECT = "/device/command"
|
||||||
|
const NATS_URL = "nats://localhost:4222"
|
||||||
|
|
||||||
|
function test_responder()
|
||||||
|
conn = NATS.connect(NATS_URL)
|
||||||
|
NATS.subscribe(conn, SUBJECT) do msg
|
||||||
|
env = smartreceive(msg, fileserver_download_handler=_fetch_with_backoff)
|
||||||
|
|
||||||
|
reply_to = env["reply_to"]
|
||||||
|
|
||||||
|
for (dataname, data, type) in env["payloads"]
|
||||||
|
if dataname == "command" && data["action"] == "read_sensor"
|
||||||
|
response = Dict("sensor_id" => "sensor-001", "value" => 42.5)
|
||||||
|
if !isempty(reply_to)
|
||||||
|
smartsend(reply_to, [("data", response, "dictionary")])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
sleep(120)
|
||||||
|
NATS.drain(conn)
|
||||||
|
end
|
||||||
|
|
||||||
|
test_responder()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
### Example 4: Large Payloads (File Server)
|
||||||
|
|
||||||
|
For payloads larger than 1MB, NATSBridge automatically uses the file server:
|
||||||
|
|
||||||
|
#### Julia
|
||||||
|
|
||||||
|
```julia
|
||||||
|
using NATSBridge
|
||||||
|
|
||||||
|
# Create large data (> 1MB)
|
||||||
|
large_data = rand(UInt8, 2_000_000)
|
||||||
|
|
||||||
|
env, env_json_str = smartsend(
|
||||||
|
"/data/large",
|
||||||
|
[("large_file", large_data, "binary")],
|
||||||
|
broker_url="nats://localhost:4222",
|
||||||
|
fileserver_url="http://localhost:8080"
|
||||||
|
)
|
||||||
|
|
||||||
|
println("File uploaded to: $(env.payloads[1].data)")
|
||||||
|
# Note: For link transport, data field contains the URL string
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JavaScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const NATSBridge = require('./src/natsbridge.js');
|
||||||
|
|
||||||
|
// Create large data (> 1MB)
|
||||||
|
const large_data = Buffer.alloc(2_000_000);
|
||||||
|
for (let i = 0; i < large_data.length; i++) {
|
||||||
|
large_data[i] = Math.floor(Math.random() * 256);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [env, env_json_str] = await NATSBridge.smartsend(
|
||||||
|
"/data/large",
|
||||||
|
[["large_file", large_data, "binary"]],
|
||||||
|
{
|
||||||
|
broker_url: "nats://localhost:4222",
|
||||||
|
fileserver_url: "http://localhost:8080"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("File uploaded to:", env.payloads[0].data);
|
||||||
|
// Note: For link transport, data field contains the URL string
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
from natsbridge import smartsend
|
||||||
|
|
||||||
|
# Create large data (> 1MB)
|
||||||
|
import os
|
||||||
|
large_data = os.urandom(2_000_000)
|
||||||
|
|
||||||
|
env, env_json_str = await smartsend(
|
||||||
|
"/data/large",
|
||||||
|
[("large_file", large_data, "binary")],
|
||||||
|
broker_url="nats://localhost:4222",
|
||||||
|
fileserver_url="http://localhost:8080"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"File uploaded to: {env['payloads'][0]['data']}")
|
||||||
|
# Note: For link transport, data field contains the URL string
|
||||||
|
```
|
||||||
|
|
||||||
|
#### MicroPython
|
||||||
|
|
||||||
|
MicroPython enforces a hard limit of 50KB per payload:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from natsbridge_mpy import NATSBridge
|
||||||
|
|
||||||
|
bridge = NATSBridge()
|
||||||
|
|
||||||
|
# MicroPython has a hard limit of 50KB per payload
|
||||||
|
# Use streaming or chunking for larger data
|
||||||
|
small_data = bytes(1000) # 1KB
|
||||||
|
|
||||||
|
data = [("small_file", small_data, "binary")]
|
||||||
|
env, env_json_str = bridge.smartsend(
|
||||||
|
"/data/small",
|
||||||
|
data,
|
||||||
|
size_threshold=100000 # Enforced max: 50000 bytes
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 5: Mixed Content (Chat with Text + Image)
|
||||||
|
|
||||||
|
NATSBridge supports sending multiple payloads with different types in a single message:
|
||||||
|
|
||||||
|
#### Julia
|
||||||
|
|
||||||
|
```julia
|
||||||
|
using NATSBridge
|
||||||
|
|
||||||
|
image_data = read("avatar.png")
|
||||||
|
|
||||||
|
data = [
|
||||||
|
("message_text", "Hello with image!", "text"),
|
||||||
|
("user_avatar", image_data, "image")
|
||||||
|
]
|
||||||
|
|
||||||
|
env, env_json_str = smartsend("/chat/mixed", data, broker_url="nats://localhost:4222")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JavaScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const NATSBridge = require('./src/natsbridge.js');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const image_data = fs.readFileSync('avatar.png');
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
["message_text", "Hello with image!", "text"],
|
||||||
|
["user_avatar", image_data, "image"]
|
||||||
|
];
|
||||||
|
|
||||||
|
const [env, env_json_str] = await NATSBridge.smartsend(
|
||||||
|
"/chat/mixed",
|
||||||
|
data,
|
||||||
|
{ broker_url: "nats://localhost:4222" }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
from natsbridge import smartsend
|
||||||
|
|
||||||
|
with open("avatar.png", "rb") as f:
|
||||||
|
image_data = f.read()
|
||||||
|
|
||||||
|
data = [
|
||||||
|
("message_text", "Hello with image!", "text"),
|
||||||
|
("user_avatar", image_data, "image")
|
||||||
|
]
|
||||||
|
|
||||||
|
env, env_json_str = await smartsend(
|
||||||
|
"/chat/mixed",
|
||||||
|
data,
|
||||||
|
broker_url="nats://localhost:4222"
|
||||||
|
)
|
||||||
|
# env: Dict with all metadata and payloads
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 6: Table Data (Arrow IPC)
|
||||||
|
|
||||||
|
For tabular data, NATSBridge uses Apache Arrow IPC format:
|
||||||
|
|
||||||
|
#### Julia
|
||||||
|
|
||||||
|
```julia
|
||||||
|
using NATSBridge
|
||||||
|
using DataFrames
|
||||||
|
|
||||||
|
# Create DataFrame
|
||||||
|
df = DataFrame(
|
||||||
|
id = [1, 2, 3],
|
||||||
|
name = ["Alice", "Bob", "Charlie"],
|
||||||
|
score = [95, 88, 92]
|
||||||
|
)
|
||||||
|
|
||||||
|
data = [("students", df, "arrowtable")]
|
||||||
|
env, env_json_str = smartsend("/data/students", data, broker_url="nats://localhost:4222")
|
||||||
|
```
|
||||||
|
|
||||||
|
#### JavaScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const NATSBridge = require('./src/natsbridge.js');
|
||||||
|
|
||||||
|
// Create table data (array of objects)
|
||||||
|
const table_data = [
|
||||||
|
{ id: 1, name: "Alice", score: 95 },
|
||||||
|
{ id: 2, name: "Bob", score: 88 },
|
||||||
|
{ id: 3, name: "Charlie", score: 92 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const data = [["students", table_data, "arrowtable"]];
|
||||||
|
const [env, env_json_str] = await NATSBridge.smartsend(
|
||||||
|
"/data/students",
|
||||||
|
data,
|
||||||
|
{ broker_url: "nats://localhost:4222" }
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
from natsbridge import smartsend
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
# Create DataFrame
|
||||||
|
df = pd.DataFrame({
|
||||||
|
'id': [1, 2, 3],
|
||||||
|
'name': ['Alice', 'Bob', 'Charlie'],
|
||||||
|
'score': [95, 88, 92]
|
||||||
|
})
|
||||||
|
|
||||||
|
data = [("students", df, "table")]
|
||||||
|
env, env_json_str = await smartsend(
|
||||||
|
"/data/students",
|
||||||
|
data,
|
||||||
|
broker_url="nats://localhost:4222"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### MicroPython
|
||||||
|
|
||||||
|
MicroPython does not support table type due to memory constraints. Use dictionary or binary instead.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Explore the test directory** for more examples
|
||||||
|
2. **Check the documentation** for advanced configuration options
|
||||||
|
3. **Read the walkthrough** for building real-world applications
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Connection Issues
|
||||||
|
|
||||||
|
- Ensure NATS server is running: `docker ps | grep nats`
|
||||||
|
- Check firewall settings
|
||||||
|
- Verify NATS URL configuration
|
||||||
|
|
||||||
|
### File Server Issues
|
||||||
|
|
||||||
|
- Ensure file server is running and accessible
|
||||||
|
- Check upload permissions
|
||||||
|
- Verify file server URL configuration
|
||||||
|
|
||||||
|
### Serialization Errors
|
||||||
|
|
||||||
|
- Verify data type matches the specified type
|
||||||
|
- Check that binary data is in the correct format
|
||||||
|
- MicroPython: Ensure payload size < 50KB
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
1378
docs/walkthrough.md
Normal file
1378
docs/walkthrough.md
Normal file
File diff suppressed because it is too large
Load Diff
310
etc.txt
Normal file
310
etc.txt
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
#!/usr/bin/env julia
|
||||||
|
# Test script for mixed-content message testing
|
||||||
|
# Tests receiving a mix of text, json, table, image, audio, video, and binary data
|
||||||
|
# from Julia serviceA to Julia serviceB using NATSBridge.jl smartreceive
|
||||||
|
#
|
||||||
|
# This test demonstrates that any combination and any number of mixed content
|
||||||
|
# can be sent and received correctly.
|
||||||
|
|
||||||
|
using NATS, JSON, UUIDs, Dates, PrettyPrinting, DataFrames, Arrow, HTTP, Base64
|
||||||
|
|
||||||
|
# Include the bridge module
|
||||||
|
include("./src/NATSBridge.jl")
|
||||||
|
using .NATSBridge
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
const SUBJECT = "/test/mix"
|
||||||
|
const NATS_URL = "nats.yiem.cc"
|
||||||
|
const FILESERVER_URL = "http://192.168.88.104:8080"
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------------------------ #
|
||||||
|
# test mixed content transfer #
|
||||||
|
# ------------------------------------------------------------------------------------------------ #
|
||||||
|
|
||||||
|
|
||||||
|
# Helper: Log with correlation ID
|
||||||
|
function log_trace(message)
|
||||||
|
timestamp = Dates.now()
|
||||||
|
println("[$timestamp] $message")
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Receiver: Listen for messages and verify mixed content handling
|
||||||
|
function test_mix_receive()
|
||||||
|
conn = NATS.connect(NATS_URL)
|
||||||
|
incoming_msg = nothing
|
||||||
|
NATS.subscribe(conn, SUBJECT) do msg
|
||||||
|
log_trace("Received message on $(msg.subject)")
|
||||||
|
incoming_msg = msg
|
||||||
|
|
||||||
|
# # Use NATSBridge.smartreceive to handle the data
|
||||||
|
# # API: smartreceive(msg, download_handler; max_retries, base_delay, max_delay)
|
||||||
|
# result = NATSBridge.smartreceive(
|
||||||
|
# msg;
|
||||||
|
# max_retries = 5,
|
||||||
|
# base_delay = 100,
|
||||||
|
# max_delay = 5000
|
||||||
|
# )
|
||||||
|
|
||||||
|
# log_trace("Received $(length(result["payloads"])) payloads")
|
||||||
|
|
||||||
|
|
||||||
|
# # Result is an envelope dictionary with payloads field containing list of (dataname, data, data_type) tuples
|
||||||
|
# for (dataname, data, data_type) in result["payloads"]
|
||||||
|
# log_trace("\n=== Payload: $dataname (type: $data_type) ===")
|
||||||
|
|
||||||
|
# # Handle different data types
|
||||||
|
# if data_type == "text"
|
||||||
|
# # Text data - should be a String
|
||||||
|
# if isa(data, String)
|
||||||
|
# log_trace(" Type: String")
|
||||||
|
# log_trace(" Length: $(length(data)) characters")
|
||||||
|
|
||||||
|
# # Display first 200 characters
|
||||||
|
# if length(data) > 200
|
||||||
|
# log_trace(" First 200 chars: $(data[1:200])...")
|
||||||
|
# else
|
||||||
|
# log_trace(" Content: $data")
|
||||||
|
# end
|
||||||
|
|
||||||
|
# # Save to file
|
||||||
|
# output_path = "./received_$dataname.txt"
|
||||||
|
# write(output_path, data)
|
||||||
|
# log_trace(" Saved to: $output_path")
|
||||||
|
# else
|
||||||
|
# log_trace(" ERROR: Expected String, got $(typeof(data))")
|
||||||
|
# end
|
||||||
|
|
||||||
|
# elseif data_type == "dictionary"
|
||||||
|
# # Dictionary data - should be JSON object
|
||||||
|
# if isa(data, JSON.Object{String, Any})
|
||||||
|
# log_trace(" Type: Dict")
|
||||||
|
# log_trace(" Keys: $(keys(data))")
|
||||||
|
|
||||||
|
# # Display nested content
|
||||||
|
# for (key, value) in data
|
||||||
|
# log_trace(" $key => $value")
|
||||||
|
# end
|
||||||
|
|
||||||
|
# # Save to JSON file
|
||||||
|
# output_path = "./received_$dataname.json"
|
||||||
|
# json_str = JSON.json(data, 2)
|
||||||
|
# write(output_path, json_str)
|
||||||
|
# log_trace(" Saved to: $output_path")
|
||||||
|
# else
|
||||||
|
# log_trace(" ERROR: Expected Dict, got $(typeof(data))")
|
||||||
|
# end
|
||||||
|
|
||||||
|
# elseif data_type == "table"
|
||||||
|
# # Table data - should be a DataFrame
|
||||||
|
# tabledata = deepcopy(data)
|
||||||
|
# println("found table data")
|
||||||
|
# break
|
||||||
|
# # return data
|
||||||
|
# # if isa(data, DataFrame)
|
||||||
|
# # log_trace(" Type: DataFrame")
|
||||||
|
# # log_trace(" Dimensions: $(size(data, 1)) rows x $(size(data, 2)) columns")
|
||||||
|
# # log_trace(" Columns: $(names(data))")
|
||||||
|
|
||||||
|
# # # Display first few rows
|
||||||
|
# # log_trace(" First 5 rows:")
|
||||||
|
# # display(data[1:min(5, size(data, 1)), :])
|
||||||
|
|
||||||
|
# # # Save to Arrow file
|
||||||
|
# # output_path = "./received_$dataname.arrow"
|
||||||
|
# # io = IOBuffer()
|
||||||
|
# # Arrow.write(io, data)
|
||||||
|
# # write(output_path, take!(io))
|
||||||
|
# # log_trace(" Saved to: $output_path")
|
||||||
|
# # else
|
||||||
|
# # log_trace(" ERROR: Expected DataFrame, got $(typeof(data))")
|
||||||
|
# # end
|
||||||
|
|
||||||
|
# elseif data_type == "image"
|
||||||
|
# # Image data - should be Vector{UInt8}
|
||||||
|
# if isa(data, Vector{UInt8})
|
||||||
|
# log_trace(" Type: Vector{UInt8} (binary)")
|
||||||
|
# log_trace(" Size: $(length(data)) bytes")
|
||||||
|
|
||||||
|
# # Save to file
|
||||||
|
# output_path = "./received_$dataname.bin"
|
||||||
|
# write(output_path, data)
|
||||||
|
# log_trace(" Saved to: $output_path")
|
||||||
|
# else
|
||||||
|
# log_trace(" ERROR: Expected Vector{UInt8}, got $(typeof(data))")
|
||||||
|
# end
|
||||||
|
|
||||||
|
# elseif data_type == "audio"
|
||||||
|
# # Audio data - should be Vector{UInt8}
|
||||||
|
# if isa(data, Vector{UInt8})
|
||||||
|
# log_trace(" Type: Vector{UInt8} (binary)")
|
||||||
|
# log_trace(" Size: $(length(data)) bytes")
|
||||||
|
|
||||||
|
# # Save to file
|
||||||
|
# output_path = "./received_$dataname.bin"
|
||||||
|
# write(output_path, data)
|
||||||
|
# log_trace(" Saved to: $output_path")
|
||||||
|
# else
|
||||||
|
# log_trace(" ERROR: Expected Vector{UInt8}, got $(typeof(data))")
|
||||||
|
# end
|
||||||
|
|
||||||
|
# elseif data_type == "video"
|
||||||
|
# # Video data - should be Vector{UInt8}
|
||||||
|
# if isa(data, Vector{UInt8})
|
||||||
|
# log_trace(" Type: Vector{UInt8} (binary)")
|
||||||
|
# log_trace(" Size: $(length(data)) bytes")
|
||||||
|
|
||||||
|
# # Save to file
|
||||||
|
# output_path = "./received_$dataname.bin"
|
||||||
|
# write(output_path, data)
|
||||||
|
# log_trace(" Saved to: $output_path")
|
||||||
|
# else
|
||||||
|
# log_trace(" ERROR: Expected Vector{UInt8}, got $(typeof(data))")
|
||||||
|
# end
|
||||||
|
|
||||||
|
# elseif data_type == "binary"
|
||||||
|
# # Binary data - should be Vector{UInt8}
|
||||||
|
# if isa(data, Vector{UInt8})
|
||||||
|
# log_trace(" Type: Vector{UInt8} (binary)")
|
||||||
|
# log_trace(" Size: $(length(data)) bytes")
|
||||||
|
|
||||||
|
# # Save to file
|
||||||
|
# output_path = "./received_$dataname.bin"
|
||||||
|
# write(output_path, data)
|
||||||
|
# log_trace(" Saved to: $output_path")
|
||||||
|
# else
|
||||||
|
# log_trace(" ERROR: Expected Vector{UInt8}, got $(typeof(data))")
|
||||||
|
# end
|
||||||
|
|
||||||
|
# else
|
||||||
|
# log_trace(" ERROR: Unknown data type '$data_type'")
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
# println("\n=== Verification Summary ===")
|
||||||
|
# text_count = count(x -> x[3] == "text", result["payloads"])
|
||||||
|
# dict_count = count(x -> x[3] == "dictionary", result["payloads"])
|
||||||
|
# table_count = count(x -> x[3] == "table", result["payloads"])
|
||||||
|
# image_count = count(x -> x[3] == "image", result["payloads"])
|
||||||
|
# audio_count = count(x -> x[3] == "audio", result["payloads"])
|
||||||
|
# video_count = count(x -> x[3] == "video", result["payloads"])
|
||||||
|
# binary_count = count(x -> x[3] == "binary", result["payloads"])
|
||||||
|
|
||||||
|
# log_trace("Text payloads: $text_count")
|
||||||
|
# log_trace("Dictionary payloads: $dict_count")
|
||||||
|
# log_trace("Table payloads: $table_count")
|
||||||
|
# log_trace("Image payloads: $image_count")
|
||||||
|
# log_trace("Audio payloads: $audio_count")
|
||||||
|
# log_trace("Video payloads: $video_count")
|
||||||
|
# log_trace("Binary payloads: $binary_count")
|
||||||
|
|
||||||
|
# # Print transport type info for each payload if available
|
||||||
|
# println("\n=== Payload Details ===")
|
||||||
|
# for (dataname, data, data_type) in result["payloads"]
|
||||||
|
# if data_type in ["image", "audio", "video", "binary"]
|
||||||
|
# log_trace("$dataname: $(length(data)) bytes (binary)")
|
||||||
|
# elseif data_type == "table"
|
||||||
|
# data = DataFrame(data)
|
||||||
|
# log_trace("$dataname: $(size(data, 1)) rows x $(size(data, 2)) columns (DataFrame)")
|
||||||
|
# elseif data_type == "dictionary"
|
||||||
|
# log_trace("$dataname: $(length(JSON.json(data))) bytes (Dict)")
|
||||||
|
# elseif data_type == "text"
|
||||||
|
# log_trace("$dataname: $(length(data)) characters (String)")
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Keep listening for 2 minutes
|
||||||
|
sleep(20)
|
||||||
|
NATS.drain(conn)
|
||||||
|
return incoming_msg
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Run the test
|
||||||
|
println("Starting mixed-content transport test...")
|
||||||
|
println("Note: This receiver will wait for messages from the sender.")
|
||||||
|
println("Run test_julia_to_julia_mix_sender.jl first to send test data.")
|
||||||
|
|
||||||
|
# Run receiver
|
||||||
|
println("\ntesting smartreceive for mixed content")
|
||||||
|
incoming_msg = test_mix_receive()
|
||||||
|
|
||||||
|
println("\nTest completed.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Check architecture.md. For sending table I want to add JSON in addition to Apache Arrow.
|
||||||
|
Currently I use "table" datatype when sending table data using Arrow. Now table that I want to send using JSON
|
||||||
|
I will use "jsontable" as datatype while sending table using Arrow I will use "arrowtable" as datatype.
|
||||||
|
This will select how smartsend and smartreceive serialize/deserialize the table.
|
||||||
|
|
||||||
|
Can you help me do this? Save the updated architecture.md into updated_architecture.md file. I will deal with source code later.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Now update implementation.md and save into updated_implementation.md
|
||||||
|
Keep in mind that Julia DataFrame and Python Pandas rely on columnar-oriented dictionary to create as the following example:
|
||||||
|
julia> dict = Dict("customer age" => [15, 20, 25],
|
||||||
|
"first name" => ["Rohit", "Rahul", "Akshat"])
|
||||||
|
julia> DataFrame(dict)
|
||||||
|
|
||||||
|
python> data = {
|
||||||
|
"Name": ["Alice", "Bob", "Charlie"],
|
||||||
|
"Age": [25, 30, 35],
|
||||||
|
"Score": [88.5, 92.0, 79.5]
|
||||||
|
}
|
||||||
|
|
||||||
|
python> df = pd.DataFrame(data)
|
||||||
|
|
||||||
|
|
||||||
|
But JS use Array of Objects while MicroPython use list of lists. Both are row-oriented structure.
|
||||||
|
So use row-oriented JSON to send across these languages. For Julia and Python, only convert
|
||||||
|
row-oriented JSON to columnar-oriented dictionary for "going-into" and vise versa for "coming-out"
|
||||||
|
a dataframe while JS and MicroPython won't require such process.
|
||||||
|
You may add these info into architecture.md if you see fit.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,604 +0,0 @@
|
|||||||
# NATSBridge Tutorial
|
|
||||||
|
|
||||||
A step-by-step guide to get started with NATSBridge - a high-performance, bi-directional data bridge for **Julia**, **JavaScript**, and **Python/Micropython**.
|
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
1. [Overview](#overview)
|
|
||||||
2. [Prerequisites](#prerequisites)
|
|
||||||
3. [Installation](#installation)
|
|
||||||
4. [Quick Start](#quick-start)
|
|
||||||
5. [Basic Examples](#basic-examples)
|
|
||||||
6. [Advanced Usage](#advanced-usage)
|
|
||||||
7. [Cross-Platform Communication](#cross-platform-communication)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
NATSBridge enables seamless communication between Julia, JavaScript, and Python/Micropython applications through NATS, with automatic transport selection based on payload size:
|
|
||||||
|
|
||||||
- **Direct Transport**: Payloads < 1MB are sent directly via NATS (Base64 encoded)
|
|
||||||
- **Link Transport**: Payloads >= 1MB are uploaded to an HTTP file server and referenced via URL
|
|
||||||
|
|
||||||
### Supported Payload Types
|
|
||||||
|
|
||||||
| Type | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `text` | Plain text strings |
|
|
||||||
| `dictionary` | JSON-serializable dictionaries |
|
|
||||||
| `table` | Tabular data (Arrow IPC format) |
|
|
||||||
| `image` | Image data (PNG, JPG bytes) |
|
|
||||||
| `audio` | Audio data (WAV, MP3 bytes) |
|
|
||||||
| `video` | Video data (MP4, AVI bytes) |
|
|
||||||
| `binary` | Generic binary data |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
Before you begin, ensure you have:
|
|
||||||
|
|
||||||
1. **NATS Server** running (or accessible)
|
|
||||||
2. **HTTP File Server** (optional, for large payloads > 1MB)
|
|
||||||
3. **One of the supported platforms**: Julia, JavaScript (Node.js), or Python/Micropython
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Julia
|
|
||||||
|
|
||||||
```julia
|
|
||||||
using Pkg
|
|
||||||
Pkg.add("NATS")
|
|
||||||
Pkg.add("Arrow")
|
|
||||||
Pkg.add("JSON3")
|
|
||||||
Pkg.add("HTTP")
|
|
||||||
Pkg.add("UUIDs")
|
|
||||||
Pkg.add("Dates")
|
|
||||||
```
|
|
||||||
|
|
||||||
### JavaScript
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install nats.js apache-arrow uuid base64-url
|
|
||||||
```
|
|
||||||
|
|
||||||
### Python/Micropython
|
|
||||||
|
|
||||||
1. Copy `src/nats_bridge.py` to your device
|
|
||||||
2. Install dependencies:
|
|
||||||
|
|
||||||
**For Python (desktop):**
|
|
||||||
```bash
|
|
||||||
pip install nats-py
|
|
||||||
```
|
|
||||||
|
|
||||||
**For Micropython:**
|
|
||||||
- `urequests` for HTTP requests
|
|
||||||
- `base64` for base64 encoding (built-in)
|
|
||||||
- `json` for JSON handling (built-in)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
### Step 1: Start NATS Server
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -p 4222:4222 nats:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Start HTTP File Server (Optional)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Create a directory for file uploads
|
|
||||||
mkdir -p /tmp/fileserver
|
|
||||||
|
|
||||||
# Use Python's built-in server
|
|
||||||
python3 -m http.server 8080 --directory /tmp/fileserver
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Send Your First Message
|
|
||||||
|
|
||||||
#### Python/Micropython
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nats_bridge import smartsend
|
|
||||||
|
|
||||||
# Send a text message
|
|
||||||
data = [("message", "Hello World", "text")]
|
|
||||||
env = smartsend("/chat/room1", data, nats_url="nats://localhost:4222")
|
|
||||||
print("Message sent!")
|
|
||||||
```
|
|
||||||
|
|
||||||
#### JavaScript
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const { smartsend } = require('./src/NATSBridge');
|
|
||||||
|
|
||||||
// Send a text message
|
|
||||||
await smartsend("/chat/room1", [
|
|
||||||
{ dataname: "message", data: "Hello World", type: "text" }
|
|
||||||
], { natsUrl: "nats://localhost:4222" });
|
|
||||||
|
|
||||||
console.log("Message sent!");
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Julia
|
|
||||||
|
|
||||||
```julia
|
|
||||||
using NATSBridge
|
|
||||||
|
|
||||||
# Send a text message
|
|
||||||
data = [("message", "Hello World", "text")]
|
|
||||||
env = smartsend("/chat/room1", data, nats_url="nats://localhost:4222")
|
|
||||||
println("Message sent!")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4: Receive Messages
|
|
||||||
|
|
||||||
#### Python/Micropython
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nats_bridge import smartreceive
|
|
||||||
|
|
||||||
# Receive and process message
|
|
||||||
envelope = smartreceive(msg)
|
|
||||||
for dataname, data, type in envelope["payloads"]:
|
|
||||||
print(f"Received {dataname}: {data}")
|
|
||||||
```
|
|
||||||
|
|
||||||
#### JavaScript
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const { smartreceive } = require('./src/NATSBridge');
|
|
||||||
|
|
||||||
// Receive and process message
|
|
||||||
const envelope = await smartreceive(msg);
|
|
||||||
for (const payload of envelope.payloads) {
|
|
||||||
console.log(`Received ${payload.dataname}: ${payload.data}`);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Julia
|
|
||||||
|
|
||||||
```julia
|
|
||||||
using NATSBridge
|
|
||||||
|
|
||||||
# Receive and process message
|
|
||||||
envelope = smartreceive(msg, fileserverDownloadHandler)
|
|
||||||
for (dataname, data, type) in envelope["payloads"]
|
|
||||||
println("Received $dataname: $data")
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Basic Examples
|
|
||||||
|
|
||||||
### Example 1: Sending a Dictionary
|
|
||||||
|
|
||||||
#### Python/Micropython
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nats_bridge import smartsend
|
|
||||||
|
|
||||||
# Create configuration dictionary
|
|
||||||
config = {
|
|
||||||
"wifi_ssid": "MyNetwork",
|
|
||||||
"wifi_password": "password123",
|
|
||||||
"update_interval": 60
|
|
||||||
}
|
|
||||||
|
|
||||||
# Send as dictionary type
|
|
||||||
data = [("config", config, "dictionary")]
|
|
||||||
env = smartsend("/device/config", data, nats_url="nats://localhost:4222")
|
|
||||||
```
|
|
||||||
|
|
||||||
#### JavaScript
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const { smartsend } = require('./src/NATSBridge');
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
wifi_ssid: "MyNetwork",
|
|
||||||
wifi_password: "password123",
|
|
||||||
update_interval: 60
|
|
||||||
};
|
|
||||||
|
|
||||||
await smartsend("/device/config", [
|
|
||||||
{ dataname: "config", data: config, type: "dictionary" }
|
|
||||||
]);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Julia
|
|
||||||
|
|
||||||
```julia
|
|
||||||
using NATSBridge
|
|
||||||
|
|
||||||
config = Dict(
|
|
||||||
"wifi_ssid" => "MyNetwork",
|
|
||||||
"wifi_password" => "password123",
|
|
||||||
"update_interval" => 60
|
|
||||||
)
|
|
||||||
|
|
||||||
data = [("config", config, "dictionary")]
|
|
||||||
smartsend("/device/config", data)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example 2: Sending Binary Data (Image)
|
|
||||||
|
|
||||||
#### Python/Micropython
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nats_bridge import smartsend
|
|
||||||
|
|
||||||
# Read image file
|
|
||||||
with open("image.png", "rb") as f:
|
|
||||||
image_data = f.read()
|
|
||||||
|
|
||||||
# Send as binary type
|
|
||||||
data = [("user_image", image_data, "binary")]
|
|
||||||
env = smartsend("/chat/image", data, nats_url="nats://localhost:4222")
|
|
||||||
```
|
|
||||||
|
|
||||||
#### JavaScript
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const { smartsend } = require('./src/NATSBridge');
|
|
||||||
|
|
||||||
// Read image file (Node.js)
|
|
||||||
const fs = require('fs');
|
|
||||||
const image_data = fs.readFileSync('image.png');
|
|
||||||
|
|
||||||
await smartsend("/chat/image", [
|
|
||||||
{ dataname: "user_image", data: image_data, type: "binary" }
|
|
||||||
]);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Julia
|
|
||||||
|
|
||||||
```julia
|
|
||||||
using NATSBridge
|
|
||||||
|
|
||||||
# Read image file
|
|
||||||
image_data = read("image.png")
|
|
||||||
|
|
||||||
data = [("user_image", image_data, "binary")]
|
|
||||||
smartsend("/chat/image", data)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example 3: Request-Response Pattern
|
|
||||||
|
|
||||||
#### Python/Micropython (Requester)
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nats_bridge import smartsend
|
|
||||||
|
|
||||||
# Send command with reply-to
|
|
||||||
data = [("command", {"action": "read_sensor"}, "dictionary")]
|
|
||||||
env = smartsend(
|
|
||||||
"/device/command",
|
|
||||||
data,
|
|
||||||
nats_url="nats://localhost:4222",
|
|
||||||
reply_to="/device/response",
|
|
||||||
reply_to_msg_id="cmd-001"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### JavaScript (Responder)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const { smartreceive, smartsend } = require('./src/NATSBridge');
|
|
||||||
|
|
||||||
// Subscribe to command topic
|
|
||||||
const sub = nc.subscribe("/device/command");
|
|
||||||
|
|
||||||
for await (const msg of sub) {
|
|
||||||
const envelope = await smartreceive(msg);
|
|
||||||
|
|
||||||
// Process command
|
|
||||||
for (const payload of envelope.payloads) {
|
|
||||||
if (payload.dataname === "command") {
|
|
||||||
const command = payload.data;
|
|
||||||
|
|
||||||
if (command.action === "read_sensor") {
|
|
||||||
// Read sensor and send response
|
|
||||||
const response = {
|
|
||||||
sensor_id: "sensor-001",
|
|
||||||
value: 42.5,
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
await smartsend("/device/response", [
|
|
||||||
{ dataname: "sensor_data", data: response, type: "dictionary" }
|
|
||||||
], {
|
|
||||||
reply_to: envelope.replyTo,
|
|
||||||
reply_to_msg_id: envelope.msgId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Advanced Usage
|
|
||||||
|
|
||||||
### Example 4: Large Payloads (File Server)
|
|
||||||
|
|
||||||
For payloads larger than 1MB, NATSBridge automatically uses the file server:
|
|
||||||
|
|
||||||
#### Python/Micropython
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nats_bridge import smartsend
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Create large data (> 1MB)
|
|
||||||
large_data = os.urandom(2_000_000) # 2MB of random data
|
|
||||||
|
|
||||||
# Send with file server URL
|
|
||||||
env = smartsend(
|
|
||||||
"/data/large",
|
|
||||||
[("large_file", large_data, "binary")],
|
|
||||||
nats_url="nats://localhost:4222",
|
|
||||||
fileserver_url="http://localhost:8080",
|
|
||||||
size_threshold=1_000_000
|
|
||||||
)
|
|
||||||
|
|
||||||
# The envelope will contain the download URL
|
|
||||||
print(f"File uploaded to: {env.payloads[0].data}")
|
|
||||||
```
|
|
||||||
|
|
||||||
#### JavaScript
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const { smartsend } = require('./src/NATSBridge');
|
|
||||||
|
|
||||||
// Create large data (> 1MB)
|
|
||||||
const largeData = new ArrayBuffer(2_000_000);
|
|
||||||
const view = new Uint8Array(largeData);
|
|
||||||
view.fill(42); // Fill with some data
|
|
||||||
|
|
||||||
await smartsend("/data/large", [
|
|
||||||
{ dataname: "large_file", data: largeData, type: "binary" }
|
|
||||||
], {
|
|
||||||
fileserverUrl: "http://localhost:8080",
|
|
||||||
sizeThreshold: 1_000_000
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Julia
|
|
||||||
|
|
||||||
```julia
|
|
||||||
using NATSBridge
|
|
||||||
|
|
||||||
# Create large data (> 1MB)
|
|
||||||
large_data = rand(UInt8, 2_000_000)
|
|
||||||
|
|
||||||
env = smartsend(
|
|
||||||
"/data/large",
|
|
||||||
[("large_file", large_data, "binary")],
|
|
||||||
fileserver_url="http://localhost:8080"
|
|
||||||
)
|
|
||||||
|
|
||||||
# The envelope will contain the download URL
|
|
||||||
println("File uploaded to: $(env.payloads[1].data)")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example 5: Mixed Content (Chat with Text + Image)
|
|
||||||
|
|
||||||
NATSBridge supports sending multiple payloads with different types in a single message:
|
|
||||||
|
|
||||||
#### Python/Micropython
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nats_bridge import smartsend
|
|
||||||
|
|
||||||
# Read image file
|
|
||||||
with open("avatar.png", "rb") as f:
|
|
||||||
image_data = f.read()
|
|
||||||
|
|
||||||
# Send mixed content
|
|
||||||
data = [
|
|
||||||
("message_text", "Hello with image!", "text"),
|
|
||||||
("user_avatar", image_data, "image")
|
|
||||||
]
|
|
||||||
|
|
||||||
env = smartsend("/chat/mixed", data, nats_url="nats://localhost:4222")
|
|
||||||
```
|
|
||||||
|
|
||||||
#### JavaScript
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const { smartsend } = require('./src/NATSBridge');
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
await smartsend("/chat/mixed", [
|
|
||||||
{
|
|
||||||
dataname: "message_text",
|
|
||||||
data: "Hello with image!",
|
|
||||||
type: "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
dataname: "user_avatar",
|
|
||||||
data: fs.readFileSync("avatar.png"),
|
|
||||||
type: "image"
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Julia
|
|
||||||
|
|
||||||
```julia
|
|
||||||
using NATSBridge
|
|
||||||
|
|
||||||
image_data = read("avatar.png")
|
|
||||||
|
|
||||||
data = [
|
|
||||||
("message_text", "Hello with image!", "text"),
|
|
||||||
("user_avatar", image_data, "image")
|
|
||||||
]
|
|
||||||
|
|
||||||
smartsend("/chat/mixed", data)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Example 6: Table Data (Arrow IPC)
|
|
||||||
|
|
||||||
For tabular data, NATSBridge uses Apache Arrow IPC format:
|
|
||||||
|
|
||||||
#### Python/Micropython
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nats_bridge import smartsend
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
# Create DataFrame
|
|
||||||
df = pd.DataFrame({
|
|
||||||
"id": [1, 2, 3],
|
|
||||||
"name": ["Alice", "Bob", "Charlie"],
|
|
||||||
"score": [95, 88, 92]
|
|
||||||
})
|
|
||||||
|
|
||||||
# Send as table type
|
|
||||||
data = [("students", df, "table")]
|
|
||||||
env = smartsend("/data/students", data, nats_url="nats://localhost:4222")
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Julia
|
|
||||||
|
|
||||||
```julia
|
|
||||||
using NATSBridge
|
|
||||||
using DataFrames
|
|
||||||
|
|
||||||
# Create DataFrame
|
|
||||||
df = DataFrame(
|
|
||||||
id = [1, 2, 3],
|
|
||||||
name = ["Alice", "Bob", "Charlie"],
|
|
||||||
score = [95, 88, 92]
|
|
||||||
)
|
|
||||||
|
|
||||||
data = [("students", df, "table")]
|
|
||||||
smartsend("/data/students", data)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Cross-Platform Communication
|
|
||||||
|
|
||||||
NATSBridge enables seamless communication between different platforms:
|
|
||||||
|
|
||||||
### Julia ↔ JavaScript
|
|
||||||
|
|
||||||
#### Julia Sender
|
|
||||||
|
|
||||||
```julia
|
|
||||||
using NATSBridge
|
|
||||||
|
|
||||||
# Send dictionary from Julia to JavaScript
|
|
||||||
config = Dict("step_size" => 0.01, "iterations" => 1000)
|
|
||||||
data = [("config", config, "dictionary")]
|
|
||||||
smartsend("/analysis/config", data, nats_url="nats://localhost:4222")
|
|
||||||
```
|
|
||||||
|
|
||||||
#### JavaScript Receiver
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const { smartreceive } = require('./src/NATSBridge');
|
|
||||||
|
|
||||||
// Receive dictionary from Julia
|
|
||||||
const envelope = await smartreceive(msg);
|
|
||||||
for (const payload of envelope.payloads) {
|
|
||||||
if (payload.type === "dictionary") {
|
|
||||||
console.log("Received config:", payload.data);
|
|
||||||
// payload.data = { step_size: 0.01, iterations: 1000 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### JavaScript ↔ Python
|
|
||||||
|
|
||||||
#### JavaScript Sender
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const { smartsend } = require('./src/NATSBridge');
|
|
||||||
|
|
||||||
await smartsend("/data/transfer", [
|
|
||||||
{ dataname: "message", data: "Hello from JS!", type: "text" }
|
|
||||||
]);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Python Receiver
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nats_bridge import smartreceive
|
|
||||||
|
|
||||||
envelope = smartreceive(msg)
|
|
||||||
for dataname, data, type in envelope["payloads"]:
|
|
||||||
if type == "text":
|
|
||||||
print(f"Received from JS: {data}")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Python ↔ Julia
|
|
||||||
|
|
||||||
#### Python Sender
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nats_bridge import smartsend
|
|
||||||
|
|
||||||
data = [("message", "Hello from Python!", "text")]
|
|
||||||
smartsend("/chat/python", data)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Julia Receiver
|
|
||||||
|
|
||||||
```julia
|
|
||||||
using NATSBridge
|
|
||||||
|
|
||||||
envelope = smartreceive(msg, fileserverDownloadHandler)
|
|
||||||
for (dataname, data, type) in envelope["payloads"]
|
|
||||||
if type == "text"
|
|
||||||
println("Received from Python: $data")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
1. **Explore the test directory** for more examples
|
|
||||||
2. **Check the documentation** for advanced configuration options
|
|
||||||
3. **Join the community** to share your use cases
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Connection Issues
|
|
||||||
|
|
||||||
- Ensure NATS server is running: `docker ps | grep nats`
|
|
||||||
- Check firewall settings
|
|
||||||
- Verify NATS URL configuration
|
|
||||||
|
|
||||||
### File Server Issues
|
|
||||||
|
|
||||||
- Ensure file server is running and accessible
|
|
||||||
- Check upload permissions
|
|
||||||
- Verify file server URL configuration
|
|
||||||
|
|
||||||
### Serialization Errors
|
|
||||||
|
|
||||||
- Verify data type matches the specified type
|
|
||||||
- Check that binary data is in the correct format (bytes/Vector{UInt8})
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT
|
|
||||||
File diff suppressed because it is too large
Load Diff
28
package.json
28
package.json
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "natsbridge",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Bi-Directional Data Bridge for JavaScript using NATS",
|
|
||||||
"main": "src/NATSBridge.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
|
||||||
"lint": "eslint src/*.js test/*.js"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"nats",
|
|
||||||
"message-broker",
|
|
||||||
"bridge",
|
|
||||||
"arrow",
|
|
||||||
"serialization"
|
|
||||||
],
|
|
||||||
"author": "",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"nats": "^2.9.0",
|
|
||||||
"apache-arrow": "^14.0.0",
|
|
||||||
"uuid": "^9.0.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"eslint": "^8.0.0",
|
|
||||||
"jest": "^29.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
14
plik_fileserver/docker-compose.yml
Normal file
14
plik_fileserver/docker-compose.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
services:
|
||||||
|
plik:
|
||||||
|
image: rootgg/plik:latest
|
||||||
|
container_name: plik-server
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
volumes:
|
||||||
|
# # Mount the config file (created below)
|
||||||
|
# - ./plikd.cfg:/home/plik/server/plikd.cfg
|
||||||
|
# Mount local folder for uploads and database
|
||||||
|
- ./plik-data:/data
|
||||||
|
# Set user to match your host UID to avoid permission issues
|
||||||
|
user: "1000:1000"
|
||||||
1029
src/NATSBridge.jl
1029
src/NATSBridge.jl
File diff suppressed because it is too large
Load Diff
@@ -1,709 +0,0 @@
|
|||||||
/**
|
|
||||||
* NATSBridge.js - Bi-Directional Data Bridge for JavaScript
|
|
||||||
* Implements smartsend and smartreceive for NATS communication
|
|
||||||
*
|
|
||||||
* This module provides functionality for sending and receiving data across network boundaries
|
|
||||||
* using NATS as the message bus, with support for both direct payload transport and
|
|
||||||
* URL-based transport for larger payloads.
|
|
||||||
*
|
|
||||||
* File Server Handler Architecture:
|
|
||||||
* The system uses handler functions to abstract file server operations, allowing support
|
|
||||||
* for different file server implementations (e.g., Plik, AWS S3, custom HTTP server).
|
|
||||||
*
|
|
||||||
* Handler Function Signatures:
|
|
||||||
*
|
|
||||||
* ```javascript
|
|
||||||
* // Upload handler - uploads data to file server and returns URL
|
|
||||||
* // The handler is passed to smartsend as fileserverUploadHandler parameter
|
|
||||||
* // It receives: (fileserver_url, dataname, data)
|
|
||||||
* // Returns: { status, uploadid, fileid, url }
|
|
||||||
* async function fileserverUploadHandler(fileserver_url, dataname, data) { ... }
|
|
||||||
*
|
|
||||||
* // Download handler - fetches data from file server URL with exponential backoff
|
|
||||||
* // The handler is passed to smartreceive as fileserverDownloadHandler parameter
|
|
||||||
* // It receives: (url, max_retries, base_delay, max_delay, correlation_id)
|
|
||||||
* // Returns: ArrayBuffer (the downloaded data)
|
|
||||||
* async function fileserverDownloadHandler(url, max_retries, base_delay, max_delay, correlation_id) { ... }
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Multi-Payload Support (Standard API):
|
|
||||||
* The system uses a standardized list-of-tuples format for all payload operations.
|
|
||||||
* Even when sending a single payload, the user must wrap it in a list.
|
|
||||||
*
|
|
||||||
* API Standard:
|
|
||||||
* ```javascript
|
|
||||||
* // Input format for smartsend (always a list of tuples with type info)
|
|
||||||
* [{ dataname, data, type }, ...]
|
|
||||||
*
|
|
||||||
* // Output format for smartreceive (always returns a list of tuples)
|
|
||||||
* [{ dataname, data, type }, ...]
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* Supported types: "text", "dictionary", "table", "image", "audio", "video", "binary"
|
|
||||||
*/
|
|
||||||
|
|
||||||
// ---------------------------------------------- 100 --------------------------------------------- #
|
|
||||||
|
|
||||||
// Constants
|
|
||||||
const DEFAULT_SIZE_THRESHOLD = 1_000_000; // 1MB - threshold for switching from direct to link transport
|
|
||||||
const DEFAULT_NATS_URL = "nats://localhost:4222"; // Default NATS server URL
|
|
||||||
const DEFAULT_FILESERVER_URL = "http://localhost:8080"; // Default HTTP file server URL for link transport
|
|
||||||
|
|
||||||
// Helper: Generate UUID v4
|
|
||||||
function uuid4() {
|
|
||||||
// Simple UUID v4 generator
|
|
||||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
||||||
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
|
||||||
return v.toString(16);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: Log with correlation ID and timestamp
|
|
||||||
function log_trace(correlation_id, message) {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
console.log(`[${timestamp}] [Correlation: ${correlation_id}] ${message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: Get size of data in bytes
|
|
||||||
function getDataSize(data) {
|
|
||||||
if (typeof data === 'string') {
|
|
||||||
return new TextEncoder().encode(data).length;
|
|
||||||
} else if (data instanceof ArrayBuffer || data instanceof Uint8Array) {
|
|
||||||
return data.byteLength;
|
|
||||||
} else if (typeof data === 'object' && data !== null) {
|
|
||||||
// For objects, serialize to JSON and measure
|
|
||||||
return new TextEncoder().encode(JSON.stringify(data)).length;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: Convert ArrayBuffer to Base64 string
|
|
||||||
function arrayBufferToBase64(buffer) {
|
|
||||||
const bytes = new Uint8Array(buffer);
|
|
||||||
let binary = '';
|
|
||||||
for (let i = 0; i < bytes.length; i++) {
|
|
||||||
binary += String.fromCharCode(bytes[i]);
|
|
||||||
}
|
|
||||||
return btoa(binary);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: Convert Base64 string to ArrayBuffer
|
|
||||||
function base64ToArrayBuffer(base64) {
|
|
||||||
const binaryString = atob(base64);
|
|
||||||
const len = binaryString.length;
|
|
||||||
const bytes = new Uint8Array(len);
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
bytes[i] = binaryString.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return bytes.buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: Serialize data based on type
|
|
||||||
function _serialize_data(data, type) {
|
|
||||||
/**
|
|
||||||
* Serialize data according to specified format
|
|
||||||
*
|
|
||||||
* Supported formats:
|
|
||||||
* - "text": Treats data as text and converts to UTF-8 bytes
|
|
||||||
* - "dictionary": Serializes data as JSON and returns the UTF-8 byte representation
|
|
||||||
* - "table": Serializes data as an Arrow IPC stream (table format) - NOT IMPLEMENTED (requires arrow library)
|
|
||||||
* - "image": Expects binary data (ArrayBuffer) and returns it as bytes
|
|
||||||
* - "audio": Expects binary data (ArrayBuffer) and returns it as bytes
|
|
||||||
* - "video": Expects binary data (ArrayBuffer) and returns it as bytes
|
|
||||||
* - "binary": Generic binary data (ArrayBuffer or Uint8Array) and returns bytes
|
|
||||||
*/
|
|
||||||
if (type === "text") {
|
|
||||||
if (typeof data === 'string') {
|
|
||||||
return new TextEncoder().encode(data).buffer;
|
|
||||||
} else {
|
|
||||||
throw new Error("Text data must be a String");
|
|
||||||
}
|
|
||||||
} else if (type === "dictionary") {
|
|
||||||
// JSON data - serialize directly
|
|
||||||
const jsonStr = JSON.stringify(data);
|
|
||||||
return new TextEncoder().encode(jsonStr).buffer;
|
|
||||||
} else if (type === "table") {
|
|
||||||
// Table data - convert to Arrow IPC stream (NOT IMPLEMENTED in pure JavaScript)
|
|
||||||
// This would require the apache-arrow library
|
|
||||||
throw new Error("Table serialization requires apache-arrow library");
|
|
||||||
} else if (type === "image") {
|
|
||||||
if (data instanceof ArrayBuffer || data instanceof Uint8Array) {
|
|
||||||
return data instanceof ArrayBuffer ? data : data.buffer;
|
|
||||||
} else {
|
|
||||||
throw new Error("Image data must be ArrayBuffer or Uint8Array");
|
|
||||||
}
|
|
||||||
} else if (type === "audio") {
|
|
||||||
if (data instanceof ArrayBuffer || data instanceof Uint8Array) {
|
|
||||||
return data instanceof ArrayBuffer ? data : data.buffer;
|
|
||||||
} else {
|
|
||||||
throw new Error("Audio data must be ArrayBuffer or Uint8Array");
|
|
||||||
}
|
|
||||||
} else if (type === "video") {
|
|
||||||
if (data instanceof ArrayBuffer || data instanceof Uint8Array) {
|
|
||||||
return data instanceof ArrayBuffer ? data : data.buffer;
|
|
||||||
} else {
|
|
||||||
throw new Error("Video data must be ArrayBuffer or Uint8Array");
|
|
||||||
}
|
|
||||||
} else if (type === "binary") {
|
|
||||||
if (data instanceof ArrayBuffer || data instanceof Uint8Array) {
|
|
||||||
return data instanceof ArrayBuffer ? data : data.buffer;
|
|
||||||
} else {
|
|
||||||
throw new Error("Binary data must be ArrayBuffer or Uint8Array");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error(`Unknown type: ${type}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: Deserialize bytes based on type
|
|
||||||
function _deserialize_data(data, type, correlation_id) {
|
|
||||||
/**
|
|
||||||
* Deserialize bytes to data based on type
|
|
||||||
*
|
|
||||||
* Supported formats:
|
|
||||||
* - "text": Converts bytes to string
|
|
||||||
* - "dictionary": Parses JSON string
|
|
||||||
* - "table": Parses Arrow IPC stream - NOT IMPLEMENTED (requires apache-arrow library)
|
|
||||||
* - "image": Returns binary data
|
|
||||||
* - "audio": Returns binary data
|
|
||||||
* - "video": Returns binary data
|
|
||||||
* - "binary": Returns binary data
|
|
||||||
*/
|
|
||||||
if (type === "text") {
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
return decoder.decode(new Uint8Array(data));
|
|
||||||
} else if (type === "dictionary") {
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
const jsonStr = decoder.decode(new Uint8Array(data));
|
|
||||||
return JSON.parse(jsonStr);
|
|
||||||
} else if (type === "table") {
|
|
||||||
// Table data - deserialize Arrow IPC stream (NOT IMPLEMENTED in pure JavaScript)
|
|
||||||
throw new Error("Table deserialization requires apache-arrow library");
|
|
||||||
} else if (type === "image") {
|
|
||||||
return data;
|
|
||||||
} else if (type === "audio") {
|
|
||||||
return data;
|
|
||||||
} else if (type === "video") {
|
|
||||||
return data;
|
|
||||||
} else if (type === "binary") {
|
|
||||||
return data;
|
|
||||||
} else {
|
|
||||||
throw new Error(`Unknown type: ${type}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: Upload data to file server
|
|
||||||
async function _upload_to_fileserver(fileserver_url, dataname, data, correlation_id) {
|
|
||||||
/**
|
|
||||||
* Upload data to HTTP file server (plik-like API)
|
|
||||||
*
|
|
||||||
* This function implements the plik one-shot upload mode:
|
|
||||||
* 1. Creates a one-shot upload session by sending POST request with {"OneShot": true}
|
|
||||||
* 2. Uploads the file data as multipart form data
|
|
||||||
* 3. Returns identifiers and download URL for the uploaded file
|
|
||||||
*/
|
|
||||||
log_trace(correlation_id, `Uploading ${dataname} to fileserver: ${fileserver_url}`);
|
|
||||||
|
|
||||||
// Step 1: Get upload ID and token
|
|
||||||
const url_getUploadID = `${fileserver_url}/upload`;
|
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
};
|
|
||||||
const body = JSON.stringify({ OneShot: true });
|
|
||||||
|
|
||||||
let response = await fetch(url_getUploadID, {
|
|
||||||
method: "POST",
|
|
||||||
headers: headers,
|
|
||||||
body: body
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to get upload ID: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseJson = await response.json();
|
|
||||||
const uploadid = responseJson.id;
|
|
||||||
const uploadtoken = responseJson.uploadToken;
|
|
||||||
|
|
||||||
// Step 2: Upload file data
|
|
||||||
const url_upload = `${fileserver_url}/file/${uploadid}`;
|
|
||||||
|
|
||||||
// Create multipart form data
|
|
||||||
const formData = new FormData();
|
|
||||||
// Create a Blob from the ArrayBuffer
|
|
||||||
const blob = new Blob([data], { type: "application/octet-stream" });
|
|
||||||
formData.append("file", blob, dataname);
|
|
||||||
|
|
||||||
response = await fetch(url_upload, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"X-UploadToken": uploadtoken
|
|
||||||
},
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to upload file: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileResponseJson = await response.json();
|
|
||||||
const fileid = fileResponseJson.id;
|
|
||||||
|
|
||||||
// Build the download URL
|
|
||||||
const url = `${fileserver_url}/file/${uploadid}/${fileid}/${encodeURIComponent(dataname)}`;
|
|
||||||
|
|
||||||
log_trace(correlation_id, `Uploaded to URL: ${url}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: response.status,
|
|
||||||
uploadid: uploadid,
|
|
||||||
fileid: fileid,
|
|
||||||
url: url
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: Fetch data from URL with exponential backoff
|
|
||||||
async function _fetch_with_backoff(url, max_retries, base_delay, max_delay, correlation_id) {
|
|
||||||
/**
|
|
||||||
* Fetch data from URL with retry logic using exponential backoff
|
|
||||||
*/
|
|
||||||
let delay = base_delay;
|
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= max_retries; attempt++) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url);
|
|
||||||
|
|
||||||
if (response.status === 200) {
|
|
||||||
log_trace(correlation_id, `Successfully fetched data from ${url} on attempt ${attempt}`);
|
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
|
||||||
return arrayBuffer;
|
|
||||||
} else {
|
|
||||||
throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
log_trace(correlation_id, `Attempt ${attempt} failed: ${e.message}`);
|
|
||||||
|
|
||||||
if (attempt < max_retries) {
|
|
||||||
// Sleep with exponential backoff
|
|
||||||
await new Promise(resolve => setTimeout(resolve, delay));
|
|
||||||
delay = Math.min(delay * 2, max_delay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Failed to fetch data after ${max_retries} attempts`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: Get payload bytes from data
|
|
||||||
function _get_payload_bytes(data) {
|
|
||||||
if (data instanceof ArrayBuffer || data instanceof Uint8Array) {
|
|
||||||
return data instanceof ArrayBuffer ? new Uint8Array(data) : data;
|
|
||||||
} else if (typeof data === 'string') {
|
|
||||||
return new TextEncoder().encode(data);
|
|
||||||
} else {
|
|
||||||
// For objects, serialize to JSON
|
|
||||||
return new TextEncoder().encode(JSON.stringify(data));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MessagePayload class
|
|
||||||
class MessagePayload {
|
|
||||||
/**
|
|
||||||
* Represents a single payload in the message envelope
|
|
||||||
*
|
|
||||||
* @param {Object} options - Payload options
|
|
||||||
* @param {string} options.id - ID of this payload (e.g., "uuid4")
|
|
||||||
* @param {string} options.dataname - Name of this payload (e.g., "login_image")
|
|
||||||
* @param {string} options.type - Payload type: "text", "dictionary", "table", "image", "audio", "video", "binary"
|
|
||||||
* @param {string} options.transport - "direct" or "link"
|
|
||||||
* @param {string} options.encoding - "none", "json", "base64", "arrow-ipc"
|
|
||||||
* @param {number} options.size - Data size in bytes
|
|
||||||
* @param {string|ArrayBuffer} options.data - Payload data (direct) or URL (link)
|
|
||||||
* @param {Object} options.metadata - Metadata for this payload
|
|
||||||
*/
|
|
||||||
constructor(options) {
|
|
||||||
this.id = options.id || uuid4();
|
|
||||||
this.dataname = options.dataname;
|
|
||||||
this.type = options.type;
|
|
||||||
this.transport = options.transport;
|
|
||||||
this.encoding = options.encoding;
|
|
||||||
this.size = options.size;
|
|
||||||
this.data = options.data;
|
|
||||||
this.metadata = options.metadata || {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to JSON object
|
|
||||||
toJSON() {
|
|
||||||
const obj = {
|
|
||||||
id: this.id,
|
|
||||||
dataname: this.dataname,
|
|
||||||
type: this.type,
|
|
||||||
transport: this.transport,
|
|
||||||
encoding: this.encoding,
|
|
||||||
size: this.size
|
|
||||||
};
|
|
||||||
|
|
||||||
// Include data based on transport type
|
|
||||||
if (this.transport === "direct" && this.data !== null) {
|
|
||||||
if (this.encoding === "base64" || this.encoding === "json") {
|
|
||||||
obj.data = this.data;
|
|
||||||
} else {
|
|
||||||
// For other encodings, use base64
|
|
||||||
const payloadBytes = _get_payload_bytes(this.data);
|
|
||||||
obj.data = arrayBufferToBase64(payloadBytes);
|
|
||||||
}
|
|
||||||
} else if (this.transport === "link" && this.data !== null) {
|
|
||||||
// For link transport, data is a URL string
|
|
||||||
obj.data = this.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(this.metadata).length > 0) {
|
|
||||||
obj.metadata = this.metadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MessageEnvelope class
|
|
||||||
class MessageEnvelope {
|
|
||||||
/**
|
|
||||||
* Represents the message envelope containing metadata and payloads
|
|
||||||
*
|
|
||||||
* @param {Object} options - Envelope options
|
|
||||||
* @param {string} options.sendTo - Topic/subject the sender sends to
|
|
||||||
* @param {Array<MessagePayload>} options.payloads - Array of payloads
|
|
||||||
* @param {string} options.correlationId - Unique identifier to track messages
|
|
||||||
* @param {string} options.msgId - This message id
|
|
||||||
* @param {string} options.timestamp - Message published timestamp
|
|
||||||
* @param {string} options.msgPurpose - Purpose of this message
|
|
||||||
* @param {string} options.senderName - Name of the sender
|
|
||||||
* @param {string} options.senderId - UUID of the sender
|
|
||||||
* @param {string} options.receiverName - Name of the receiver
|
|
||||||
* @param {string} options.receiverId - UUID of the receiver
|
|
||||||
* @param {string} options.replyTo - Topic to reply to
|
|
||||||
* @param {string} options.replyToMsgId - Message id this message is replying to
|
|
||||||
* @param {string} options.brokerURL - NATS server address
|
|
||||||
* @param {Object} options.metadata - Metadata for the envelope
|
|
||||||
*/
|
|
||||||
constructor(options) {
|
|
||||||
this.correlationId = options.correlationId || uuid4();
|
|
||||||
this.msgId = options.msgId || uuid4();
|
|
||||||
this.timestamp = options.timestamp || new Date().toISOString();
|
|
||||||
this.sendTo = options.sendTo;
|
|
||||||
this.msgPurpose = options.msgPurpose || "";
|
|
||||||
this.senderName = options.senderName || "";
|
|
||||||
this.senderId = options.senderId || uuid4();
|
|
||||||
this.receiverName = options.receiverName || "";
|
|
||||||
this.receiverId = options.receiverId || "";
|
|
||||||
this.replyTo = options.replyTo || "";
|
|
||||||
this.replyToMsgId = options.replyToMsgId || "";
|
|
||||||
this.brokerURL = options.brokerURL || DEFAULT_NATS_URL;
|
|
||||||
this.metadata = options.metadata || {};
|
|
||||||
this.payloads = options.payloads || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to JSON string
|
|
||||||
toJSON() {
|
|
||||||
const obj = {
|
|
||||||
correlationId: this.correlationId,
|
|
||||||
msgId: this.msgId,
|
|
||||||
timestamp: this.timestamp,
|
|
||||||
sendTo: this.sendTo,
|
|
||||||
msgPurpose: this.msgPurpose,
|
|
||||||
senderName: this.senderName,
|
|
||||||
senderId: this.senderId,
|
|
||||||
receiverName: this.receiverName,
|
|
||||||
receiverId: this.receiverId,
|
|
||||||
replyTo: this.replyTo,
|
|
||||||
replyToMsgId: this.replyToMsgId,
|
|
||||||
brokerURL: this.brokerURL
|
|
||||||
};
|
|
||||||
|
|
||||||
if (Object.keys(this.metadata).length > 0) {
|
|
||||||
obj.metadata = this.metadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.payloads.length > 0) {
|
|
||||||
obj.payloads = this.payloads.map(p => p.toJSON());
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to JSON string
|
|
||||||
toString() {
|
|
||||||
return JSON.stringify(this.toJSON());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SmartSend function
|
|
||||||
async function smartsend(subject, data, options = {}) {
|
|
||||||
/**
|
|
||||||
* Send data either directly via NATS or via a fileserver URL, depending on payload size
|
|
||||||
*
|
|
||||||
* This function intelligently routes data delivery based on payload size relative to a threshold.
|
|
||||||
* If the serialized payload is smaller than `size_threshold`, it encodes the data as Base64 and publishes directly over NATS.
|
|
||||||
* Otherwise, it uploads the data to a fileserver and publishes only the download URL over NATS.
|
|
||||||
*
|
|
||||||
* @param {string} subject - NATS subject to publish the message to
|
|
||||||
* @param {Array} data - List of {dataname, data, type} objects to send
|
|
||||||
* @param {Object} options - Additional options
|
|
||||||
* @param {string} options.natsUrl - URL of the NATS server (default: "nats://localhost:4222")
|
|
||||||
* @param {string} options.fileserverUrl - Base URL of the file server (default: "http://localhost:8080")
|
|
||||||
* @param {Function} options.fileserverUploadHandler - Function to handle fileserver uploads
|
|
||||||
* @param {number} options.sizeThreshold - Threshold in bytes separating direct vs link transport (default: 1MB)
|
|
||||||
* @param {string} options.correlationId - Optional correlation ID for tracing
|
|
||||||
* @param {string} options.msgPurpose - Purpose of the message (default: "chat")
|
|
||||||
* @param {string} options.senderName - Name of the sender (default: "NATSBridge")
|
|
||||||
* @param {string} options.receiverName - Name of the receiver (default: "")
|
|
||||||
* @param {string} options.receiverId - UUID of the receiver (default: "")
|
|
||||||
* @param {string} options.replyTo - Topic to reply to (default: "")
|
|
||||||
* @param {string} options.replyToMsgId - Message ID this message is replying to (default: "")
|
|
||||||
*
|
|
||||||
* @returns {Promise<MessageEnvelope>} - The envelope for tracking
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
natsUrl = DEFAULT_NATS_URL,
|
|
||||||
fileserverUrl = DEFAULT_FILESERVER_URL,
|
|
||||||
fileserverUploadHandler = _upload_to_fileserver,
|
|
||||||
sizeThreshold = DEFAULT_SIZE_THRESHOLD,
|
|
||||||
correlationId = uuid4(),
|
|
||||||
msgPurpose = "chat",
|
|
||||||
senderName = "NATSBridge",
|
|
||||||
receiverName = "",
|
|
||||||
receiverId = "",
|
|
||||||
replyTo = "",
|
|
||||||
replyToMsgId = ""
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
log_trace(correlationId, `Starting smartsend for subject: ${subject}`);
|
|
||||||
|
|
||||||
// Generate message metadata
|
|
||||||
const msgId = uuid4();
|
|
||||||
|
|
||||||
// Process each payload in the list
|
|
||||||
const payloads = [];
|
|
||||||
|
|
||||||
for (const payload of data) {
|
|
||||||
const dataname = payload.dataname;
|
|
||||||
const payloadData = payload.data;
|
|
||||||
const payloadType = payload.type;
|
|
||||||
|
|
||||||
// Serialize data based on type
|
|
||||||
const payloadBytes = _serialize_data(payloadData, payloadType);
|
|
||||||
const payloadSize = payloadBytes.byteLength;
|
|
||||||
|
|
||||||
log_trace(correlationId, `Serialized payload '${dataname}' (type: ${payloadType}) size: ${payloadSize} bytes`);
|
|
||||||
|
|
||||||
// Decision: Direct vs Link
|
|
||||||
if (payloadSize < sizeThreshold) {
|
|
||||||
// Direct path - Base64 encode and send via NATS
|
|
||||||
const payloadB64 = arrayBufferToBase64(payloadBytes);
|
|
||||||
log_trace(correlationId, `Using direct transport for ${payloadSize} bytes`);
|
|
||||||
|
|
||||||
// Create MessagePayload for direct transport
|
|
||||||
const payloadObj = new MessagePayload({
|
|
||||||
dataname: dataname,
|
|
||||||
type: payloadType,
|
|
||||||
transport: "direct",
|
|
||||||
encoding: "base64",
|
|
||||||
size: payloadSize,
|
|
||||||
data: payloadB64,
|
|
||||||
metadata: { payload_bytes: payloadSize }
|
|
||||||
});
|
|
||||||
payloads.push(payloadObj);
|
|
||||||
} else {
|
|
||||||
// Link path - Upload to HTTP server, send URL via NATS
|
|
||||||
log_trace(correlationId, `Using link transport, uploading to fileserver`);
|
|
||||||
|
|
||||||
// Upload to HTTP server
|
|
||||||
const response = await fileserverUploadHandler(fileserverUrl, dataname, payloadBytes, correlationId);
|
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
throw new Error(`Failed to upload data to fileserver: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = response.url;
|
|
||||||
log_trace(correlationId, `Uploaded to URL: ${url}`);
|
|
||||||
|
|
||||||
// Create MessagePayload for link transport
|
|
||||||
const payloadObj = new MessagePayload({
|
|
||||||
dataname: dataname,
|
|
||||||
type: payloadType,
|
|
||||||
transport: "link",
|
|
||||||
encoding: "none",
|
|
||||||
size: payloadSize,
|
|
||||||
data: url,
|
|
||||||
metadata: {}
|
|
||||||
});
|
|
||||||
payloads.push(payloadObj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create MessageEnvelope with all payloads
|
|
||||||
const env = new MessageEnvelope({
|
|
||||||
correlationId: correlationId,
|
|
||||||
msgId: msgId,
|
|
||||||
sendTo: subject,
|
|
||||||
msgPurpose: msgPurpose,
|
|
||||||
senderName: senderName,
|
|
||||||
receiverName: receiverName,
|
|
||||||
receiverId: receiverId,
|
|
||||||
replyTo: replyTo,
|
|
||||||
replyToMsgId: replyToMsgId,
|
|
||||||
brokerURL: natsUrl,
|
|
||||||
payloads: payloads
|
|
||||||
});
|
|
||||||
|
|
||||||
// Publish message to NATS
|
|
||||||
await publish_message(natsUrl, subject, env.toString(), correlationId);
|
|
||||||
|
|
||||||
return env;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: Publish message to NATS
|
|
||||||
async function publish_message(natsUrl, subject, message, correlation_id) {
|
|
||||||
/**
|
|
||||||
* Publish a message to a NATS subject with proper connection management
|
|
||||||
*
|
|
||||||
* @param {string} natsUrl - NATS server URL
|
|
||||||
* @param {string} subject - NATS subject to publish to
|
|
||||||
* @param {string} message - JSON message to publish
|
|
||||||
* @param {string} correlation_id - Correlation ID for logging
|
|
||||||
*/
|
|
||||||
log_trace(correlation_id, `Publishing message to ${subject}`);
|
|
||||||
|
|
||||||
// For Node.js, we would use nats.js library
|
|
||||||
// This is a placeholder that throws an error
|
|
||||||
// In production, you would import and use the actual nats library
|
|
||||||
|
|
||||||
// Example with nats.js:
|
|
||||||
// import { connect } from 'nats';
|
|
||||||
// const nc = await connect({ servers: [natsUrl] });
|
|
||||||
// await nc.publish(subject, message);
|
|
||||||
// nc.close();
|
|
||||||
|
|
||||||
// For now, just log the message
|
|
||||||
console.log(`[NATS PUBLISH] Subject: ${subject}, Message: ${message.substring(0, 100)}...`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// SmartReceive function
|
|
||||||
async function smartreceive(msg, options = {}) {
|
|
||||||
/**
|
|
||||||
* Receive and process messages from NATS
|
|
||||||
*
|
|
||||||
* This function processes incoming NATS messages, handling both direct transport
|
|
||||||
* (base64 decoded payloads) and link transport (URL-based payloads).
|
|
||||||
*
|
|
||||||
* @param {Object} msg - NATS message object with payload property
|
|
||||||
* @param {Object} options - Additional options
|
|
||||||
* @param {Function} options.fileserverDownloadHandler - Function to handle downloading data from file server URLs
|
|
||||||
* @param {number} options.maxRetries - Maximum retry attempts for fetching URL (default: 5)
|
|
||||||
* @param {number} options.baseDelay - Initial delay for exponential backoff in ms (default: 100)
|
|
||||||
* @param {number} options.maxDelay - Maximum delay for exponential backoff in ms (default: 5000)
|
|
||||||
*
|
|
||||||
* @returns {Promise<Object>} - Envelope dictionary with metadata and payloads field containing list of {dataname, data, type} objects
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
fileserverDownloadHandler = _fetch_with_backoff,
|
|
||||||
maxRetries = 5,
|
|
||||||
baseDelay = 100,
|
|
||||||
maxDelay = 5000
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
// Parse the JSON envelope
|
|
||||||
const jsonStr = typeof msg.payload === 'string' ? msg.payload : new TextDecoder().decode(msg.payload);
|
|
||||||
const json_data = JSON.parse(jsonStr);
|
|
||||||
|
|
||||||
log_trace(json_data.correlationId, `Processing received message`);
|
|
||||||
|
|
||||||
// Process all payloads in the envelope
|
|
||||||
const payloads_list = [];
|
|
||||||
|
|
||||||
// Get number of payloads
|
|
||||||
const num_payloads = json_data.payloads ? json_data.payloads.length : 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < num_payloads; i++) {
|
|
||||||
const payload = json_data.payloads[i];
|
|
||||||
const transport = payload.transport;
|
|
||||||
const dataname = payload.dataname;
|
|
||||||
|
|
||||||
if (transport === "direct") {
|
|
||||||
// Direct transport - payload is in the message
|
|
||||||
log_trace(json_data.correlationId, `Direct transport - decoding payload '${dataname}'`);
|
|
||||||
|
|
||||||
// Extract base64 payload from the payload
|
|
||||||
const payload_b64 = payload.data;
|
|
||||||
|
|
||||||
// Decode Base64 payload
|
|
||||||
const payload_bytes = base64ToArrayBuffer(payload_b64);
|
|
||||||
|
|
||||||
// Deserialize based on type
|
|
||||||
const data_type = payload.type;
|
|
||||||
const data = _deserialize_data(payload_bytes, data_type, json_data.correlationId);
|
|
||||||
|
|
||||||
payloads_list.push({ dataname, data, type: data_type });
|
|
||||||
} else if (transport === "link") {
|
|
||||||
// Link transport - payload is at URL
|
|
||||||
const url = payload.data;
|
|
||||||
log_trace(json_data.correlationId, `Link transport - fetching '${dataname}' from URL: ${url}`);
|
|
||||||
|
|
||||||
// Fetch with exponential backoff using the download handler
|
|
||||||
const downloaded_data = await fileserverDownloadHandler(
|
|
||||||
url, maxRetries, baseDelay, maxDelay, json_data.correlationId
|
|
||||||
);
|
|
||||||
|
|
||||||
// Deserialize based on type
|
|
||||||
const data_type = payload.type;
|
|
||||||
const data = _deserialize_data(downloaded_data, data_type, json_data.correlationId);
|
|
||||||
|
|
||||||
payloads_list.push({ dataname, data, type: data_type });
|
|
||||||
} else {
|
|
||||||
throw new Error(`Unknown transport type for payload '${dataname}': ${transport}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace payloads array with the processed list of {dataname, data, type} tuples
|
|
||||||
json_data.payloads = payloads_list;
|
|
||||||
|
|
||||||
return json_data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export for Node.js
|
|
||||||
if (typeof module !== 'undefined' && module.exports) {
|
|
||||||
module.exports = {
|
|
||||||
MessageEnvelope,
|
|
||||||
MessagePayload,
|
|
||||||
smartsend,
|
|
||||||
smartreceive,
|
|
||||||
_serialize_data,
|
|
||||||
_deserialize_data,
|
|
||||||
_fetch_with_backoff,
|
|
||||||
_upload_to_fileserver,
|
|
||||||
DEFAULT_SIZE_THRESHOLD,
|
|
||||||
DEFAULT_NATS_URL,
|
|
||||||
DEFAULT_FILESERVER_URL,
|
|
||||||
uuid4,
|
|
||||||
log_trace
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export for browser
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
window.NATSBridge = {
|
|
||||||
MessageEnvelope,
|
|
||||||
MessagePayload,
|
|
||||||
smartsend,
|
|
||||||
smartreceive,
|
|
||||||
_serialize_data,
|
|
||||||
_deserialize_data,
|
|
||||||
_fetch_with_backoff,
|
|
||||||
_upload_to_fileserver,
|
|
||||||
DEFAULT_SIZE_THRESHOLD,
|
|
||||||
DEFAULT_NATS_URL,
|
|
||||||
DEFAULT_FILESERVER_URL,
|
|
||||||
uuid4,
|
|
||||||
log_trace
|
|
||||||
};
|
|
||||||
}
|
|
||||||
295
src/README.md
295
src/README.md
@@ -1,295 +0,0 @@
|
|||||||
# NATSBridge
|
|
||||||
|
|
||||||
A high-performance, bi-directional data bridge for **Julia**, **JavaScript**, and **Python/Micropython** using NATS (Core & JetStream), implementing the Claim-Check pattern for large payloads.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
NATSBridge enables seamless communication between Julia, JavaScript, and Python/Micropython applications through NATS, with automatic transport selection based on payload size:
|
|
||||||
|
|
||||||
- **Direct Transport**: Payloads < 1MB are sent directly via NATS (Base64 encoded)
|
|
||||||
- **Link Transport**: Payloads >= 1MB are uploaded to an HTTP file server and referenced via URL
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- ✅ Bi-directional NATS communication across Julia ↔ JavaScript ↔ Python/Micropython
|
|
||||||
- ✅ Multi-payload support (mixed content in single message)
|
|
||||||
- ✅ Automatic transport selection based on payload size
|
|
||||||
- ✅ File server integration for large payloads
|
|
||||||
- ✅ Exponential backoff for URL fetching
|
|
||||||
- ✅ Correlation ID tracking
|
|
||||||
- ✅ Reply-to support for request-response pattern
|
|
||||||
|
|
||||||
## Supported Payload Types
|
|
||||||
|
|
||||||
| Type | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `text` | Plain text strings |
|
|
||||||
| `dictionary` | JSON-serializable dictionaries |
|
|
||||||
| `table` | Tabular data (Arrow IPC format) |
|
|
||||||
| `image` | Image data (PNG, JPG bytes) |
|
|
||||||
| `audio` | Audio data (WAV, MP3 bytes) |
|
|
||||||
| `video` | Video data (MP4, AVI bytes) |
|
|
||||||
| `binary` | Generic binary data |
|
|
||||||
|
|
||||||
## Implementation Guides
|
|
||||||
|
|
||||||
### [Julia Implementation](../tutorial_julia.md)
|
|
||||||
|
|
||||||
See the [Julia tutorial](../tutorial_julia.md) for getting started with Julia.
|
|
||||||
|
|
||||||
### [JavaScript Implementation](#javascript-implementation)
|
|
||||||
|
|
||||||
See [`NATSBridge.js`](NATSBridge.js) for the JavaScript implementation.
|
|
||||||
|
|
||||||
### [Python/Micropython Implementation](#pythonmicropython-implementation)
|
|
||||||
|
|
||||||
See [`nats_bridge.py`](nats_bridge.py) for the Python/Micropython implementation.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Julia
|
|
||||||
|
|
||||||
```julia
|
|
||||||
using Pkg
|
|
||||||
Pkg.add("NATS")
|
|
||||||
Pkg.add("Arrow")
|
|
||||||
Pkg.add("JSON3")
|
|
||||||
Pkg.add("HTTP")
|
|
||||||
Pkg.add("UUIDs")
|
|
||||||
Pkg.add("Dates")
|
|
||||||
```
|
|
||||||
|
|
||||||
### JavaScript
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install nats.js apache-arrow uuid base64-url
|
|
||||||
```
|
|
||||||
|
|
||||||
### Python/Micropython
|
|
||||||
|
|
||||||
1. Copy `nats_bridge.py` to your device
|
|
||||||
2. Ensure you have the following dependencies:
|
|
||||||
- `urequests` for HTTP requests (Micropython)
|
|
||||||
- `requests` for HTTP requests (Python)
|
|
||||||
- `base64` for base64 encoding
|
|
||||||
- `json` for JSON handling
|
|
||||||
- `socket` for networking (Micropython)
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Basic Text Message
|
|
||||||
|
|
||||||
#### Python/Micropython
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nats_bridge import smartsend, smartreceive
|
|
||||||
|
|
||||||
# Sender
|
|
||||||
data = [("message", "Hello World", "text")]
|
|
||||||
env = smartsend("/chat/room1", data, nats_url="nats://localhost:4222")
|
|
||||||
|
|
||||||
# Receiver
|
|
||||||
payloads = smartreceive(msg)
|
|
||||||
for dataname, data, type in payloads:
|
|
||||||
print("Received {}: {}".format(dataname, data))
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Julia
|
|
||||||
|
|
||||||
```julia
|
|
||||||
using NATSBridge
|
|
||||||
|
|
||||||
# Sender
|
|
||||||
data = [("message", "Hello World", "text")]
|
|
||||||
env = smartsend("/chat/room1", data, nats_url="nats://localhost:4222")
|
|
||||||
|
|
||||||
# Receiver
|
|
||||||
envelope = smartreceive(msg, fileserverDownloadHandler)
|
|
||||||
# envelope["payloads"] = [("message", "Hello World", "text"), ...]
|
|
||||||
```
|
|
||||||
|
|
||||||
#### JavaScript
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const { smartsend, smartreceive } = require('./src/NATSBridge');
|
|
||||||
|
|
||||||
// Sender
|
|
||||||
await smartsend("/chat/room1", [
|
|
||||||
{ dataname: "message", data: "Hello World", type: "text" }
|
|
||||||
], { natsUrl: "nats://localhost:4222" });
|
|
||||||
|
|
||||||
// Receiver
|
|
||||||
const envelope = await smartreceive(msg);
|
|
||||||
// envelope.payloads = [{ dataname: "message", data: "Hello World", type: "text" }, ...]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Sending JSON Configuration
|
|
||||||
|
|
||||||
#### Python/Micropython
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nats_bridge import smartsend
|
|
||||||
|
|
||||||
config = {
|
|
||||||
"wifi_ssid": "MyNetwork",
|
|
||||||
"wifi_password": "password123",
|
|
||||||
"update_interval": 60
|
|
||||||
}
|
|
||||||
|
|
||||||
data = [("config", config, "dictionary")]
|
|
||||||
env = smartsend("/device/config", data, nats_url="nats://localhost:4222")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Mixed Content (Chat with Text + Image)
|
|
||||||
|
|
||||||
#### Python/Micropython
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nats_bridge import smartsend
|
|
||||||
|
|
||||||
image_data = b"\x89PNG..." # PNG bytes
|
|
||||||
|
|
||||||
data = [
|
|
||||||
("message_text", "Hello with image!", "text"),
|
|
||||||
("user_avatar", image_data, "binary")
|
|
||||||
]
|
|
||||||
|
|
||||||
env = smartsend("/chat/mixed", data, nats_url="nats://localhost:4222")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Request-Response Pattern
|
|
||||||
|
|
||||||
#### Python/Micropython
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nats_bridge import smartsend
|
|
||||||
|
|
||||||
# Send command with reply-to
|
|
||||||
data = [("command", {"action": "read_sensor"}, "dictionary")]
|
|
||||||
env = smartsend(
|
|
||||||
"/device/command",
|
|
||||||
data,
|
|
||||||
nats_url="nats://localhost:4222",
|
|
||||||
reply_to="/device/response",
|
|
||||||
reply_to_msg_id="cmd-001"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Large Payloads (File Server)
|
|
||||||
|
|
||||||
#### Python/Micropython
|
|
||||||
|
|
||||||
```python
|
|
||||||
from nats_bridge import smartsend
|
|
||||||
|
|
||||||
# Large data (> 1MB)
|
|
||||||
large_data = b"A" * 2000000 # 2MB
|
|
||||||
|
|
||||||
env = smartsend(
|
|
||||||
"/data/large",
|
|
||||||
[("large_file", large_data, "binary")],
|
|
||||||
nats_url="nats://localhost:4222",
|
|
||||||
fileserver_url="http://localhost:8080",
|
|
||||||
size_threshold=1000000 # 1MB threshold
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Reference
|
|
||||||
|
|
||||||
### `smartsend(subject, data, ...)`
|
|
||||||
|
|
||||||
Send data via NATS with automatic transport selection.
|
|
||||||
|
|
||||||
**Arguments:**
|
|
||||||
- `subject` (str): NATS subject to publish to
|
|
||||||
- `data` (list): List of `(dataname, data, type)` tuples
|
|
||||||
- `nats_url` (str): NATS server URL (default: `nats://localhost:4222`)
|
|
||||||
- `fileserver_url` (str): HTTP file server URL (default: `http://localhost:8080`)
|
|
||||||
- `size_threshold` (int): Threshold in bytes (default: 1,000,000)
|
|
||||||
- `correlation_id` (str): Optional correlation ID for tracing
|
|
||||||
- `msg_purpose` (str): Message purpose (default: `"chat"`)
|
|
||||||
- `sender_name` (str): Sender name (default: `"NATSBridge"`)
|
|
||||||
- `receiver_name` (str): Receiver name (default: `""`)
|
|
||||||
- `receiver_id` (str): Receiver ID (default: `""`)
|
|
||||||
- `reply_to` (str): Reply topic (default: `""`)
|
|
||||||
- `reply_to_msg_id` (str): Reply message ID (default: `""`)
|
|
||||||
|
|
||||||
**Returns:** `MessageEnvelope` object
|
|
||||||
|
|
||||||
### `smartreceive(msg, ...)`
|
|
||||||
|
|
||||||
Receive and process NATS messages.
|
|
||||||
|
|
||||||
**Arguments:**
|
|
||||||
- `msg`: NATS message (dict or JSON string)
|
|
||||||
- `fileserver_download_handler` (function): Function to fetch data from URLs
|
|
||||||
- `max_retries` (int): Maximum retry attempts (default: 5)
|
|
||||||
- `base_delay` (int): Initial delay in ms (default: 100)
|
|
||||||
- `max_delay` (int): Maximum delay in ms (default: 5000)
|
|
||||||
|
|
||||||
**Returns:** List of `(dataname, data, type)` tuples
|
|
||||||
|
|
||||||
### `MessageEnvelope`
|
|
||||||
|
|
||||||
Represents a complete NATS message envelope.
|
|
||||||
|
|
||||||
**Attributes:**
|
|
||||||
- `correlation_id`: Unique identifier for tracing
|
|
||||||
- `msg_id`: Unique message identifier
|
|
||||||
- `timestamp`: Message publication timestamp
|
|
||||||
- `send_to`: NATS subject
|
|
||||||
- `msg_purpose`: Message purpose
|
|
||||||
- `sender_name`: Sender name
|
|
||||||
- `sender_id`: Sender UUID
|
|
||||||
- `receiver_name`: Receiver name
|
|
||||||
- `receiver_id`: Receiver UUID
|
|
||||||
- `reply_to`: Reply topic
|
|
||||||
- `reply_to_msg_id`: Reply message ID
|
|
||||||
- `broker_url`: NATS broker URL
|
|
||||||
- `metadata`: Message-level metadata
|
|
||||||
- `payloads`: List of MessagePayload objects
|
|
||||||
|
|
||||||
### `MessagePayload`
|
|
||||||
|
|
||||||
Represents a single payload within a message envelope.
|
|
||||||
|
|
||||||
**Attributes:**
|
|
||||||
- `id`: Unique payload identifier
|
|
||||||
- `dataname`: Name of the payload
|
|
||||||
- `type`: Payload type ("text", "dictionary", etc.)
|
|
||||||
- `transport`: Transport method ("direct" or "link")
|
|
||||||
- `encoding`: Encoding method ("none", "base64", etc.)
|
|
||||||
- `size`: Payload size in bytes
|
|
||||||
- `data`: Payload data (bytes for direct, URL for link)
|
|
||||||
- `metadata`: Payload-level metadata
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
See [`examples/micropython_example.py`](../examples/micropython_example.py) for more detailed examples.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
Run the test suite:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Python/Micropython
|
|
||||||
python test/test_micropython_basic.py
|
|
||||||
|
|
||||||
# JavaScript
|
|
||||||
node test/test_js_to_js_text_sender.js
|
|
||||||
node test/test_js_to_js_text_receiver.js
|
|
||||||
|
|
||||||
# Julia
|
|
||||||
julia test/test_julia_to_julia_text_sender.jl
|
|
||||||
julia test/test_julia_to_julia_text_receiver.jl
|
|
||||||
```
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- **Julia**: NATS server (nats.io), HTTP file server (optional)
|
|
||||||
- **JavaScript**: NATS server (nats.io), HTTP file server (optional)
|
|
||||||
- **Python/Micropython**: NATS server (nats.io), HTTP file server (optional)
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT
|
|
||||||
@@ -1,667 +0,0 @@
|
|||||||
"""
|
|
||||||
Micropython NATS Bridge - Bi-Directional Data Bridge for Micropython
|
|
||||||
|
|
||||||
This module provides functionality for sending and receiving data over NATS
|
|
||||||
using the Claim-Check pattern for large payloads.
|
|
||||||
|
|
||||||
Supported types: "text", "dictionary", "table", "image", "audio", "video", "binary"
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import random
|
|
||||||
import time
|
|
||||||
import usocket
|
|
||||||
import uselect
|
|
||||||
import ustruct
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
try:
|
|
||||||
import ussl
|
|
||||||
HAS_SSL = True
|
|
||||||
except ImportError:
|
|
||||||
HAS_SSL = False
|
|
||||||
|
|
||||||
# Constants
|
|
||||||
DEFAULT_SIZE_THRESHOLD = 1000000 # 1MB - threshold for switching from direct to link transport
|
|
||||||
DEFAULT_NATS_URL = "nats://localhost:4222"
|
|
||||||
DEFAULT_FILESERVER_URL = "http://localhost:8080"
|
|
||||||
|
|
||||||
# ============================================= 100 ============================================== #
|
|
||||||
|
|
||||||
|
|
||||||
class MessagePayload:
|
|
||||||
"""Internal message payload structure representing a single payload within a NATS message envelope."""
|
|
||||||
|
|
||||||
def __init__(self, data, msg_type, id="", dataname="", transport="direct",
|
|
||||||
encoding="none", size=0, metadata=None):
|
|
||||||
"""
|
|
||||||
Initialize a MessagePayload.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: Payload data (bytes for direct, URL string for link)
|
|
||||||
msg_type: Payload type ("text", "dictionary", "table", "image", "audio", "video", "binary")
|
|
||||||
id: Unique identifier for this payload (auto-generated if empty)
|
|
||||||
dataname: Name of the payload (auto-generated UUID if empty)
|
|
||||||
transport: Transport method ("direct" or "link")
|
|
||||||
encoding: Encoding method ("none", "json", "base64", "arrow-ipc")
|
|
||||||
size: Size of the payload in bytes
|
|
||||||
metadata: Optional metadata dictionary
|
|
||||||
"""
|
|
||||||
self.id = id if id else self._generate_uuid()
|
|
||||||
self.dataname = dataname if dataname else self._generate_uuid()
|
|
||||||
self.type = msg_type
|
|
||||||
self.transport = transport
|
|
||||||
self.encoding = encoding
|
|
||||||
self.size = size
|
|
||||||
self.data = data
|
|
||||||
self.metadata = metadata if metadata else {}
|
|
||||||
|
|
||||||
def _generate_uuid(self):
|
|
||||||
"""Generate a UUID string."""
|
|
||||||
return str(uuid.uuid4())
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
"""Convert payload to dictionary for JSON serialization."""
|
|
||||||
payload_dict = {
|
|
||||||
"id": self.id,
|
|
||||||
"dataname": self.dataname,
|
|
||||||
"type": self.type,
|
|
||||||
"transport": self.transport,
|
|
||||||
"encoding": self.encoding,
|
|
||||||
"size": self.size,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Include data based on transport type
|
|
||||||
if self.transport == "direct" and self.data is not None:
|
|
||||||
if self.encoding == "base64" or self.encoding == "json":
|
|
||||||
payload_dict["data"] = self.data
|
|
||||||
else:
|
|
||||||
# For other encodings, use base64
|
|
||||||
payload_dict["data"] = self._to_base64(self.data)
|
|
||||||
elif self.transport == "link" and self.data is not None:
|
|
||||||
# For link transport, data is a URL string
|
|
||||||
payload_dict["data"] = self.data
|
|
||||||
|
|
||||||
if self.metadata:
|
|
||||||
payload_dict["metadata"] = self.metadata
|
|
||||||
|
|
||||||
return payload_dict
|
|
||||||
|
|
||||||
def _to_base64(self, data):
|
|
||||||
"""Convert bytes to base64 string."""
|
|
||||||
if isinstance(data, bytes):
|
|
||||||
# Simple base64 encoding without library
|
|
||||||
import ubinascii
|
|
||||||
return ubinascii.b2a_base64(data).decode('utf-8').strip()
|
|
||||||
return data
|
|
||||||
|
|
||||||
def _from_base64(self, data):
|
|
||||||
"""Convert base64 string to bytes."""
|
|
||||||
import ubinascii
|
|
||||||
return ubinascii.a2b_base64(data)
|
|
||||||
|
|
||||||
|
|
||||||
class MessageEnvelope:
|
|
||||||
"""Internal message envelope structure containing multiple payloads with metadata."""
|
|
||||||
|
|
||||||
def __init__(self, send_to, payloads, correlation_id="", msg_id="", timestamp="",
|
|
||||||
msg_purpose="", sender_name="", sender_id="", receiver_name="",
|
|
||||||
receiver_id="", reply_to="", reply_to_msg_id="", broker_url=DEFAULT_NATS_URL,
|
|
||||||
metadata=None):
|
|
||||||
"""
|
|
||||||
Initialize a MessageEnvelope.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
send_to: NATS subject/topic to publish the message to
|
|
||||||
payloads: List of MessagePayload objects
|
|
||||||
correlation_id: Unique identifier to track messages (auto-generated if empty)
|
|
||||||
msg_id: Unique message identifier (auto-generated if empty)
|
|
||||||
timestamp: Message publication timestamp
|
|
||||||
msg_purpose: Purpose of the message ("ACK", "NACK", "updateStatus", "shutdown", "chat", etc.)
|
|
||||||
sender_name: Name of the sender
|
|
||||||
sender_id: UUID of the sender
|
|
||||||
receiver_name: Name of the receiver (empty means broadcast)
|
|
||||||
receiver_id: UUID of the receiver (empty means broadcast)
|
|
||||||
reply_to: Topic where receiver should reply
|
|
||||||
reply_to_msg_id: Message ID this message is replying to
|
|
||||||
broker_url: NATS broker URL
|
|
||||||
metadata: Optional message-level metadata
|
|
||||||
"""
|
|
||||||
self.correlation_id = correlation_id if correlation_id else self._generate_uuid()
|
|
||||||
self.msg_id = msg_id if msg_id else self._generate_uuid()
|
|
||||||
self.timestamp = timestamp if timestamp else self._get_timestamp()
|
|
||||||
self.send_to = send_to
|
|
||||||
self.msg_purpose = msg_purpose
|
|
||||||
self.sender_name = sender_name
|
|
||||||
self.sender_id = sender_id if sender_id else self._generate_uuid()
|
|
||||||
self.receiver_name = receiver_name
|
|
||||||
self.receiver_id = receiver_id if receiver_id else self._generate_uuid()
|
|
||||||
self.reply_to = reply_to
|
|
||||||
self.reply_to_msg_id = reply_to_msg_id
|
|
||||||
self.broker_url = broker_url
|
|
||||||
self.metadata = metadata if metadata else {}
|
|
||||||
self.payloads = payloads
|
|
||||||
|
|
||||||
def _generate_uuid(self):
|
|
||||||
"""Generate a UUID string."""
|
|
||||||
return str(uuid.uuid4())
|
|
||||||
|
|
||||||
def _get_timestamp(self):
|
|
||||||
"""Get current timestamp in ISO format."""
|
|
||||||
# Simplified timestamp - Micropython may not have full datetime
|
|
||||||
return "2026-02-21T" + time.strftime("%H:%M:%S", time.localtime())
|
|
||||||
|
|
||||||
def to_json(self):
|
|
||||||
"""Convert envelope to JSON string."""
|
|
||||||
obj = {
|
|
||||||
"correlationId": self.correlation_id,
|
|
||||||
"msgId": self.msg_id,
|
|
||||||
"timestamp": self.timestamp,
|
|
||||||
"sendTo": self.send_to,
|
|
||||||
"msgPurpose": self.msg_purpose,
|
|
||||||
"senderName": self.sender_name,
|
|
||||||
"senderId": self.sender_id,
|
|
||||||
"receiverName": self.receiver_name,
|
|
||||||
"receiverId": self.receiver_id,
|
|
||||||
"replyTo": self.reply_to,
|
|
||||||
"replyToMsgId": self.reply_to_msg_id,
|
|
||||||
"brokerURL": self.broker_url
|
|
||||||
}
|
|
||||||
|
|
||||||
# Include metadata if not empty
|
|
||||||
if self.metadata:
|
|
||||||
obj["metadata"] = self.metadata
|
|
||||||
|
|
||||||
# Convert payloads to JSON array
|
|
||||||
if self.payloads:
|
|
||||||
payloads_json = []
|
|
||||||
for payload in self.payloads:
|
|
||||||
payloads_json.append(payload.to_dict())
|
|
||||||
obj["payloads"] = payloads_json
|
|
||||||
|
|
||||||
return json.dumps(obj)
|
|
||||||
|
|
||||||
|
|
||||||
def log_trace(correlation_id, message):
|
|
||||||
"""Log a trace message with correlation ID and timestamp."""
|
|
||||||
timestamp = time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime())
|
|
||||||
print("[{}] [Correlation: {}] {}".format(timestamp, correlation_id, message))
|
|
||||||
|
|
||||||
|
|
||||||
def _serialize_data(data, msg_type):
|
|
||||||
"""Serialize data according to specified format.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: Data to serialize
|
|
||||||
msg_type: Target format ("text", "dictionary", "table", "image", "audio", "video", "binary")
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bytes: Binary representation of the serialized data
|
|
||||||
"""
|
|
||||||
if msg_type == "text":
|
|
||||||
if isinstance(data, str):
|
|
||||||
return data.encode('utf-8')
|
|
||||||
else:
|
|
||||||
raise ValueError("Text data must be a string")
|
|
||||||
|
|
||||||
elif msg_type == "dictionary":
|
|
||||||
if isinstance(data, dict):
|
|
||||||
json_str = json.dumps(data)
|
|
||||||
return json_str.encode('utf-8')
|
|
||||||
else:
|
|
||||||
raise ValueError("Dictionary data must be a dict")
|
|
||||||
|
|
||||||
elif msg_type in ("image", "audio", "video", "binary"):
|
|
||||||
if isinstance(data, bytes):
|
|
||||||
return data
|
|
||||||
else:
|
|
||||||
raise ValueError("{} data must be bytes".format(msg_type.capitalize()))
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise ValueError("Unknown type: {}".format(msg_type))
|
|
||||||
|
|
||||||
|
|
||||||
def _deserialize_data(data_bytes, msg_type, correlation_id):
|
|
||||||
"""Deserialize bytes to data based on type.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data_bytes: Serialized data as bytes
|
|
||||||
msg_type: Data type ("text", "dictionary", "table", "image", "audio", "video", "binary")
|
|
||||||
correlation_id: Correlation ID for logging
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Deserialized data
|
|
||||||
"""
|
|
||||||
if msg_type == "text":
|
|
||||||
return data_bytes.decode('utf-8')
|
|
||||||
|
|
||||||
elif msg_type == "dictionary":
|
|
||||||
json_str = data_bytes.decode('utf-8')
|
|
||||||
return json.loads(json_str)
|
|
||||||
|
|
||||||
elif msg_type in ("image", "audio", "video", "binary"):
|
|
||||||
return data_bytes
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise ValueError("Unknown type: {}".format(msg_type))
|
|
||||||
|
|
||||||
|
|
||||||
class NATSConnection:
|
|
||||||
"""Simple NATS connection for Micropython."""
|
|
||||||
|
|
||||||
def __init__(self, url=DEFAULT_NATS_URL):
|
|
||||||
"""Initialize NATS connection.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
url: NATS server URL (e.g., "nats://localhost:4222")
|
|
||||||
"""
|
|
||||||
self.url = url
|
|
||||||
self.host = "localhost"
|
|
||||||
self.port = 4222
|
|
||||||
self.conn = None
|
|
||||||
self._parse_url(url)
|
|
||||||
|
|
||||||
def _parse_url(self, url):
|
|
||||||
"""Parse NATS URL to extract host and port."""
|
|
||||||
if url.startswith("nats://"):
|
|
||||||
url = url[7:]
|
|
||||||
elif url.startswith("tls://"):
|
|
||||||
url = url[6:]
|
|
||||||
|
|
||||||
if ":" in url:
|
|
||||||
self.host, port_str = url.split(":")
|
|
||||||
self.port = int(port_str)
|
|
||||||
else:
|
|
||||||
self.host = url
|
|
||||||
|
|
||||||
def connect(self):
|
|
||||||
"""Connect to NATS server."""
|
|
||||||
addr = usocket.getaddrinfo(self.host, self.port)[0][-1]
|
|
||||||
self.conn = usocket.socket()
|
|
||||||
self.conn.connect(addr)
|
|
||||||
log_trace("", "Connected to NATS server at {}:{}".format(self.host, self.port))
|
|
||||||
|
|
||||||
def publish(self, subject, message):
|
|
||||||
"""Publish a message to a NATS subject.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
subject: NATS subject to publish to
|
|
||||||
message: Message to publish (should be bytes or string)
|
|
||||||
"""
|
|
||||||
if isinstance(message, str):
|
|
||||||
message = message.encode('utf-8')
|
|
||||||
|
|
||||||
# Simple NATS protocol implementation
|
|
||||||
msg = "PUB {} {}\r\n".format(subject, len(message))
|
|
||||||
msg = msg.encode('utf-8') + message + b"\r\n"
|
|
||||||
self.conn.send(msg)
|
|
||||||
log_trace("", "Message published to {}".format(subject))
|
|
||||||
|
|
||||||
def subscribe(self, subject, callback):
|
|
||||||
"""Subscribe to a NATS subject.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
subject: NATS subject to subscribe to
|
|
||||||
callback: Callback function to handle incoming messages
|
|
||||||
"""
|
|
||||||
log_trace("", "Subscribed to {}".format(subject))
|
|
||||||
# Simplified subscription - in a real implementation, you'd handle SUB/PUB messages
|
|
||||||
# For Micropython, we'll use a simple polling approach
|
|
||||||
self.subscribed_subject = subject
|
|
||||||
self.subscription_callback = callback
|
|
||||||
|
|
||||||
def wait_message(self, timeout=1000):
|
|
||||||
"""Wait for incoming message.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
timeout: Timeout in milliseconds
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
NATS message object or None if timeout
|
|
||||||
"""
|
|
||||||
# Simplified message reading
|
|
||||||
# In a real implementation, you'd read from the socket
|
|
||||||
# For now, this is a placeholder
|
|
||||||
return None
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""Close the NATS connection."""
|
|
||||||
if self.conn:
|
|
||||||
self.conn.close()
|
|
||||||
self.conn = None
|
|
||||||
log_trace("", "NATS connection closed")
|
|
||||||
|
|
||||||
|
|
||||||
def _fetch_with_backoff(url, max_retries=5, base_delay=100, max_delay=5000, correlation_id=""):
|
|
||||||
"""Fetch data from URL with exponential backoff.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
url: URL to fetch from
|
|
||||||
max_retries: Maximum number of retry attempts
|
|
||||||
base_delay: Initial delay in milliseconds
|
|
||||||
max_delay: Maximum delay in milliseconds
|
|
||||||
correlation_id: Correlation ID for logging
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bytes: Fetched data
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
Exception: If all retry attempts fail
|
|
||||||
"""
|
|
||||||
delay = base_delay
|
|
||||||
for attempt in range(1, max_retries + 1):
|
|
||||||
try:
|
|
||||||
# Simple HTTP GET request
|
|
||||||
# This is a simplified implementation
|
|
||||||
# For production, you'd want a proper HTTP client
|
|
||||||
import urequests
|
|
||||||
response = urequests.get(url)
|
|
||||||
if response.status_code == 200:
|
|
||||||
log_trace(correlation_id, "Successfully fetched data from {} on attempt {}".format(url, attempt))
|
|
||||||
return response.content
|
|
||||||
else:
|
|
||||||
raise Exception("Failed to fetch: {}".format(response.status_code))
|
|
||||||
except Exception as e:
|
|
||||||
log_trace(correlation_id, "Attempt {} failed: {}".format(attempt, str(e)))
|
|
||||||
if attempt < max_retries:
|
|
||||||
time.sleep(delay / 1000.0)
|
|
||||||
delay = min(delay * 2, max_delay)
|
|
||||||
|
|
||||||
|
|
||||||
def plik_oneshot_upload(file_server_url, filename, data):
|
|
||||||
"""Upload a single file to a plik server using one-shot mode.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
file_server_url: Base URL of the plik server
|
|
||||||
filename: Name of the file being uploaded
|
|
||||||
data: Raw byte data of the file content
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Dictionary with keys:
|
|
||||||
- "status": HTTP server response status
|
|
||||||
- "uploadid": ID of the one-shot upload session
|
|
||||||
- "fileid": ID of the uploaded file within the session
|
|
||||||
- "url": Full URL to download the uploaded file
|
|
||||||
"""
|
|
||||||
import urequests
|
|
||||||
import json
|
|
||||||
|
|
||||||
# Get upload ID
|
|
||||||
url_get_upload_id = "{}/upload".format(file_server_url)
|
|
||||||
headers = {"Content-Type": "application/json"}
|
|
||||||
body = json.dumps({"OneShot": True})
|
|
||||||
|
|
||||||
response = urequests.post(url_get_upload_id, headers=headers, data=body)
|
|
||||||
response_json = json.loads(response.content)
|
|
||||||
|
|
||||||
uploadid = response_json.get("id")
|
|
||||||
uploadtoken = response_json.get("uploadToken")
|
|
||||||
|
|
||||||
# Upload file
|
|
||||||
url_upload = "{}/file/{}".format(file_server_url, uploadid)
|
|
||||||
headers = {"X-UploadToken": uploadtoken}
|
|
||||||
|
|
||||||
# For Micropython, we need to construct the multipart form data manually
|
|
||||||
# This is a simplified approach
|
|
||||||
boundary = "----WebKitFormBoundary{}".format(uuid.uuid4().hex[:16])
|
|
||||||
|
|
||||||
# Create multipart body
|
|
||||||
part1 = "--{}\r\n".format(boundary)
|
|
||||||
part1 += "Content-Disposition: form-data; name=\"file\"; filename=\"{}\"\r\n".format(filename)
|
|
||||||
part1 += "Content-Type: application/octet-stream\r\n\r\n"
|
|
||||||
part1_bytes = part1.encode('utf-8')
|
|
||||||
|
|
||||||
part2 = "\r\n--{}--".format(boundary)
|
|
||||||
part2_bytes = part2.encode('utf-8')
|
|
||||||
|
|
||||||
# Combine all parts
|
|
||||||
full_body = part1_bytes + data + part2_bytes
|
|
||||||
|
|
||||||
# Set content type with boundary
|
|
||||||
content_type = "multipart/form-data; boundary={}".format(boundary)
|
|
||||||
|
|
||||||
response = urequests.post(url_upload, headers={"Content-Type": content_type}, data=full_body)
|
|
||||||
response_json = json.loads(response.content)
|
|
||||||
|
|
||||||
fileid = response_json.get("id")
|
|
||||||
url = "{}/file/{}/{}".format(file_server_url, uploadid, filename)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": response.status_code,
|
|
||||||
"uploadid": uploadid,
|
|
||||||
"fileid": fileid,
|
|
||||||
"url": url
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def smartsend(subject, data, nats_url=DEFAULT_NATS_URL, fileserver_url=DEFAULT_FILESERVER_URL,
|
|
||||||
fileserver_upload_handler=plik_oneshot_upload, size_threshold=DEFAULT_SIZE_THRESHOLD,
|
|
||||||
correlation_id=None, msg_purpose="chat", sender_name="NATSBridge",
|
|
||||||
receiver_name="", receiver_id="", reply_to="", reply_to_msg_id=""):
|
|
||||||
"""Send data either directly via NATS or via a fileserver URL, depending on payload size.
|
|
||||||
|
|
||||||
This function intelligently routes data delivery based on payload size relative to a threshold.
|
|
||||||
If the serialized payload is smaller than `size_threshold`, it encodes the data as Base64 and
|
|
||||||
publishes directly over NATS. Otherwise, it uploads the data to a fileserver and publishes
|
|
||||||
only the download URL over NATS.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
subject: NATS subject to publish the message to
|
|
||||||
data: List of (dataname, data, type) tuples to send
|
|
||||||
nats_url: URL of the NATS server
|
|
||||||
fileserver_url: URL of the HTTP file server
|
|
||||||
fileserver_upload_handler: Function to handle fileserver uploads
|
|
||||||
size_threshold: Threshold in bytes separating direct vs link transport
|
|
||||||
correlation_id: Optional correlation ID for tracing
|
|
||||||
msg_purpose: Purpose of the message
|
|
||||||
sender_name: Name of the sender
|
|
||||||
receiver_name: Name of the receiver
|
|
||||||
receiver_id: UUID of the receiver
|
|
||||||
reply_to: Topic to reply to
|
|
||||||
reply_to_msg_id: Message ID this message is replying to
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
MessageEnvelope: The envelope object for tracking
|
|
||||||
"""
|
|
||||||
# Generate correlation ID if not provided
|
|
||||||
cid = correlation_id if correlation_id else str(uuid.uuid4())
|
|
||||||
|
|
||||||
log_trace(cid, "Starting smartsend for subject: {}".format(subject))
|
|
||||||
|
|
||||||
# Generate message metadata
|
|
||||||
msg_id = str(uuid.uuid4())
|
|
||||||
|
|
||||||
# Process each payload in the list
|
|
||||||
payloads = []
|
|
||||||
|
|
||||||
for dataname, payload_data, payload_type in data:
|
|
||||||
# Serialize data based on type
|
|
||||||
payload_bytes = _serialize_data(payload_data, payload_type)
|
|
||||||
|
|
||||||
payload_size = len(payload_bytes)
|
|
||||||
log_trace(cid, "Serialized payload '{}' (type: {}) size: {} bytes".format(
|
|
||||||
dataname, payload_type, payload_size))
|
|
||||||
|
|
||||||
# Decision: Direct vs Link
|
|
||||||
if payload_size < size_threshold:
|
|
||||||
# Direct path - Base64 encode and send via NATS
|
|
||||||
payload_b64 = _serialize_data(payload_bytes, "binary") # Already bytes
|
|
||||||
# Convert to base64 string for JSON
|
|
||||||
import ubinascii
|
|
||||||
payload_b64_str = ubinascii.b2a_base64(payload_bytes).decode('utf-8').strip()
|
|
||||||
|
|
||||||
log_trace(cid, "Using direct transport for {} bytes".format(payload_size))
|
|
||||||
|
|
||||||
# Create MessagePayload for direct transport
|
|
||||||
payload = MessagePayload(
|
|
||||||
payload_b64_str,
|
|
||||||
payload_type,
|
|
||||||
id=str(uuid.uuid4()),
|
|
||||||
dataname=dataname,
|
|
||||||
transport="direct",
|
|
||||||
encoding="base64",
|
|
||||||
size=payload_size,
|
|
||||||
metadata={"payload_bytes": payload_size}
|
|
||||||
)
|
|
||||||
payloads.append(payload)
|
|
||||||
else:
|
|
||||||
# Link path - Upload to HTTP server, send URL via NATS
|
|
||||||
log_trace(cid, "Using link transport, uploading to fileserver")
|
|
||||||
|
|
||||||
# Upload to HTTP server
|
|
||||||
response = fileserver_upload_handler(fileserver_url, dataname, payload_bytes)
|
|
||||||
|
|
||||||
if response["status"] != 200:
|
|
||||||
raise Exception("Failed to upload data to fileserver: {}".format(response["status"]))
|
|
||||||
|
|
||||||
url = response["url"]
|
|
||||||
log_trace(cid, "Uploaded to URL: {}".format(url))
|
|
||||||
|
|
||||||
# Create MessagePayload for link transport
|
|
||||||
payload = MessagePayload(
|
|
||||||
url,
|
|
||||||
payload_type,
|
|
||||||
id=str(uuid.uuid4()),
|
|
||||||
dataname=dataname,
|
|
||||||
transport="link",
|
|
||||||
encoding="none",
|
|
||||||
size=payload_size,
|
|
||||||
metadata={}
|
|
||||||
)
|
|
||||||
payloads.append(payload)
|
|
||||||
|
|
||||||
# Create MessageEnvelope with all payloads
|
|
||||||
env = MessageEnvelope(
|
|
||||||
subject,
|
|
||||||
payloads,
|
|
||||||
correlation_id=cid,
|
|
||||||
msg_id=msg_id,
|
|
||||||
msg_purpose=msg_purpose,
|
|
||||||
sender_name=sender_name,
|
|
||||||
sender_id=str(uuid.uuid4()),
|
|
||||||
receiver_name=receiver_name,
|
|
||||||
receiver_id=receiver_id,
|
|
||||||
reply_to=reply_to,
|
|
||||||
reply_to_msg_id=reply_to_msg_id,
|
|
||||||
broker_url=nats_url,
|
|
||||||
metadata={}
|
|
||||||
)
|
|
||||||
|
|
||||||
msg_json = env.to_json()
|
|
||||||
|
|
||||||
# Publish to NATS
|
|
||||||
nats_conn = NATSConnection(nats_url)
|
|
||||||
nats_conn.connect()
|
|
||||||
nats_conn.publish(subject, msg_json)
|
|
||||||
nats_conn.close()
|
|
||||||
|
|
||||||
return env
|
|
||||||
|
|
||||||
|
|
||||||
def smartreceive(msg, fileserver_download_handler=_fetch_with_backoff, max_retries=5,
|
|
||||||
base_delay=100, max_delay=5000):
|
|
||||||
"""Receive and process messages from NATS.
|
|
||||||
|
|
||||||
This function processes incoming NATS messages, handling both direct transport
|
|
||||||
(base64 decoded payloads) and link transport (URL-based payloads).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
msg: NATS message to process (dict with payload data)
|
|
||||||
fileserver_download_handler: Function to handle downloading data from file server URLs
|
|
||||||
max_retries: Maximum retry attempts for fetching URL
|
|
||||||
base_delay: Initial delay for exponential backoff in ms
|
|
||||||
max_delay: Maximum delay for exponential backoff in ms
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Envelope dictionary with metadata and 'payloads' field containing list of (dataname, data, type) tuples
|
|
||||||
"""
|
|
||||||
# Parse the JSON envelope
|
|
||||||
json_data = msg if isinstance(msg, dict) else json.loads(msg)
|
|
||||||
log_trace(json_data.get("correlationId", ""), "Processing received message")
|
|
||||||
|
|
||||||
# Process all payloads in the envelope
|
|
||||||
payloads_list = []
|
|
||||||
|
|
||||||
# Get number of payloads
|
|
||||||
num_payloads = len(json_data.get("payloads", []))
|
|
||||||
|
|
||||||
for i in range(num_payloads):
|
|
||||||
payload = json_data["payloads"][i]
|
|
||||||
transport = payload.get("transport", "")
|
|
||||||
dataname = payload.get("dataname", "")
|
|
||||||
|
|
||||||
if transport == "direct":
|
|
||||||
log_trace(json_data.get("correlationId", ""),
|
|
||||||
"Direct transport - decoding payload '{}'".format(dataname))
|
|
||||||
|
|
||||||
# Extract base64 payload from the payload
|
|
||||||
payload_b64 = payload.get("data", "")
|
|
||||||
|
|
||||||
# Decode Base64 payload
|
|
||||||
import ubinascii
|
|
||||||
payload_bytes = ubinascii.a2b_base64(payload_b64.encode('utf-8'))
|
|
||||||
|
|
||||||
# Deserialize based on type
|
|
||||||
data_type = payload.get("type", "")
|
|
||||||
data = _deserialize_data(payload_bytes, data_type, json_data.get("correlationId", ""))
|
|
||||||
|
|
||||||
payloads_list.append((dataname, data, data_type))
|
|
||||||
|
|
||||||
elif transport == "link":
|
|
||||||
# Extract download URL from the payload
|
|
||||||
url = payload.get("data", "")
|
|
||||||
log_trace(json_data.get("correlationId", ""),
|
|
||||||
"Link transport - fetching '{}' from URL: {}".format(dataname, url))
|
|
||||||
|
|
||||||
# Fetch with exponential backoff
|
|
||||||
downloaded_data = fileserver_download_handler(
|
|
||||||
url, max_retries, base_delay, max_delay, json_data.get("correlationId", "")
|
|
||||||
)
|
|
||||||
|
|
||||||
# Deserialize based on type
|
|
||||||
data_type = payload.get("type", "")
|
|
||||||
data = _deserialize_data(downloaded_data, data_type, json_data.get("correlationId", ""))
|
|
||||||
|
|
||||||
payloads_list.append((dataname, data, data_type))
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise ValueError("Unknown transport type for payload '{}': {}".format(dataname, transport))
|
|
||||||
|
|
||||||
# Replace payloads field with the processed list of (dataname, data, type) tuples
|
|
||||||
json_data["payloads"] = payloads_list
|
|
||||||
|
|
||||||
return json_data
|
|
||||||
|
|
||||||
|
|
||||||
# Utility functions
|
|
||||||
def generate_uuid():
|
|
||||||
"""Generate a UUID string."""
|
|
||||||
return str(uuid.uuid4())
|
|
||||||
|
|
||||||
|
|
||||||
def get_timestamp():
|
|
||||||
"""Get current timestamp in ISO format."""
|
|
||||||
return time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime())
|
|
||||||
|
|
||||||
|
|
||||||
# Example usage
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print("NATSBridge for Micropython")
|
|
||||||
print("=========================")
|
|
||||||
print("This module provides:")
|
|
||||||
print(" - MessageEnvelope: Message envelope structure")
|
|
||||||
print(" - MessagePayload: Payload structure")
|
|
||||||
print(" - smartsend: Send data via NATS with automatic transport selection")
|
|
||||||
print(" - smartreceive: Receive and process messages from NATS")
|
|
||||||
print(" - plik_oneshot_upload: Upload files to HTTP file server")
|
|
||||||
print(" - _fetch_with_backoff: Fetch data from URLs with retry logic")
|
|
||||||
print()
|
|
||||||
print("Usage:")
|
|
||||||
print(" from nats_bridge import smartsend, smartreceive")
|
|
||||||
print(" data = [(\"message\", \"Hello World\", \"text\")]")
|
|
||||||
print(" env = smartsend(\"my.subject\", data)")
|
|
||||||
print()
|
|
||||||
print(" # On receiver:")
|
|
||||||
print(" payloads = smartreceive(msg)")
|
|
||||||
print(" for dataname, data, type in payloads:")
|
|
||||||
print(" print(f\"Received {dataname} of type {type}: {data}\")")
|
|
||||||
843
src/natsbridge.py
Normal file
843
src/natsbridge.py
Normal file
@@ -0,0 +1,843 @@
|
|||||||
|
"""
|
||||||
|
NATSBridge - Cross-Platform Bi-Directional Data Bridge
|
||||||
|
Python Desktop Implementation
|
||||||
|
|
||||||
|
This module provides functionality for sending and receiving data across network boundaries
|
||||||
|
using NATS as the message bus, with support for both direct payload transport and
|
||||||
|
URL-based transport for larger payloads.
|
||||||
|
|
||||||
|
@package natsbridge
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Callable, Dict, List, Tuple, Union
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pyarrow as arrow
|
||||||
|
import pyarrow.ipc as ipc
|
||||||
|
ARROW_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
ARROW_AVAILABLE = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
import nats
|
||||||
|
from nats.aio.client import Client as NATSClient
|
||||||
|
NATS_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
NATS_AVAILABLE = False
|
||||||
|
|
||||||
|
# ---------------------------------------------- Constants ---------------------------------------------- #
|
||||||
|
|
||||||
|
"""
|
||||||
|
Default size threshold for switching from direct to link transport (0.5MB)
|
||||||
|
"""
|
||||||
|
DEFAULT_SIZE_THRESHOLD = 500_000
|
||||||
|
|
||||||
|
"""
|
||||||
|
Default NATS server URL
|
||||||
|
"""
|
||||||
|
DEFAULT_BROKER_URL = "nats://localhost:4222"
|
||||||
|
|
||||||
|
"""
|
||||||
|
Default HTTP file server URL for link transport
|
||||||
|
"""
|
||||||
|
DEFAULT_FILESERVER_URL = "http://localhost:8080"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------- Utility Functions ---------------------------------------------- #
|
||||||
|
|
||||||
|
def log_trace(correlation_id: str, message: str) -> None:
|
||||||
|
"""
|
||||||
|
Log a trace message with correlation ID and timestamp.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
correlation_id: Correlation ID for tracing
|
||||||
|
message: Message content to log
|
||||||
|
"""
|
||||||
|
timestamp = datetime.utcnow().isoformat() + 'Z'
|
||||||
|
print(f"[{timestamp}] [Correlation: {correlation_id}] {message}")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------- Serialization Functions ---------------------------------------------- #
|
||||||
|
|
||||||
|
def _serialize_data(data: Any, payload_type: str) -> bytes:
|
||||||
|
"""
|
||||||
|
Serialize data according to specified format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Data to serialize (string for "text", JSON-serializable for "dictionary",
|
||||||
|
table-like for "arrowtable"/"jsontable", binary for "image", "audio", "video", "binary")
|
||||||
|
payload_type: Target format: "text", "dictionary", "arrowtable", "jsontable",
|
||||||
|
"image", "audio", "video", "binary"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Binary representation of the serialized data
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Error: If payload_type is not one of the supported types
|
||||||
|
Error: If payload_type is "image", "audio", or "video" but data is not bytes
|
||||||
|
Error: If payload_type is "arrowtable" but data is not a pandas DataFrame or pyarrow Table
|
||||||
|
Error: If payload_type is "jsontable" but data is not a list of dicts
|
||||||
|
"""
|
||||||
|
if payload_type == 'text':
|
||||||
|
if isinstance(data, str):
|
||||||
|
return data.encode('utf-8')
|
||||||
|
else:
|
||||||
|
raise ValueError('Text data must be a string')
|
||||||
|
elif payload_type == 'dictionary':
|
||||||
|
json_str = json.dumps(data)
|
||||||
|
return json_str.encode('utf-8')
|
||||||
|
elif payload_type == 'arrowtable':
|
||||||
|
if not ARROW_AVAILABLE:
|
||||||
|
raise RuntimeError('pyarrow not available for arrowtable serialization')
|
||||||
|
|
||||||
|
import io
|
||||||
|
buf = io.BytesIO()
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
if isinstance(data, pd.DataFrame):
|
||||||
|
table = arrow.Table.from_pandas(data)
|
||||||
|
sink = ipc.new_file(buf, table.schema)
|
||||||
|
ipc.write_table(table, sink)
|
||||||
|
sink.close()
|
||||||
|
return buf.getvalue()
|
||||||
|
elif isinstance(data, arrow.Table):
|
||||||
|
sink = ipc.new_file(buf, data.schema)
|
||||||
|
ipc.write_table(data, sink)
|
||||||
|
sink.close()
|
||||||
|
return buf.getvalue()
|
||||||
|
else:
|
||||||
|
raise ValueError('Arrow table data must be a pandas DataFrame or pyarrow Table')
|
||||||
|
elif payload_type == 'jsontable':
|
||||||
|
# Serialize list of dicts to JSON format
|
||||||
|
if isinstance(data, list) and all(isinstance(row, dict) for row in data):
|
||||||
|
json_str = json.dumps(data)
|
||||||
|
return json_str.encode('utf-8')
|
||||||
|
else:
|
||||||
|
raise ValueError('JSON table data must be a list of dicts')
|
||||||
|
elif payload_type == 'image':
|
||||||
|
if isinstance(data, (bytes, bytearray)):
|
||||||
|
return bytes(data)
|
||||||
|
else:
|
||||||
|
raise ValueError('Image data must be bytes')
|
||||||
|
elif payload_type == 'audio':
|
||||||
|
if isinstance(data, (bytes, bytearray)):
|
||||||
|
return bytes(data)
|
||||||
|
else:
|
||||||
|
raise ValueError('Audio data must be bytes')
|
||||||
|
elif payload_type == 'video':
|
||||||
|
if isinstance(data, (bytes, bytearray)):
|
||||||
|
return bytes(data)
|
||||||
|
else:
|
||||||
|
raise ValueError('Video data must be bytes')
|
||||||
|
elif payload_type == 'binary':
|
||||||
|
if isinstance(data, (bytes, bytearray)):
|
||||||
|
return bytes(data)
|
||||||
|
else:
|
||||||
|
raise ValueError('Binary data must be bytes')
|
||||||
|
else:
|
||||||
|
raise ValueError(f'Unknown payload_type: {payload_type}')
|
||||||
|
|
||||||
|
|
||||||
|
def _deserialize_data(data: bytes, payload_type: str, correlation_id: str) -> Any:
|
||||||
|
"""
|
||||||
|
Deserialize bytes to data based on type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Serialized data as bytes
|
||||||
|
payload_type: Data type ("text", "dictionary", "arrowtable", "jsontable",
|
||||||
|
"image", "audio", "video", "binary")
|
||||||
|
correlation_id: Correlation ID for logging
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Deserialized data (String for "text", DataFrame for "arrowtable",
|
||||||
|
Vector{Dict} for "jsontable"/"dictionary", bytes for "image", "audio", "video", "binary")
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Error: If payload_type is not one of the supported types
|
||||||
|
"""
|
||||||
|
if payload_type == 'text':
|
||||||
|
return data.decode('utf-8')
|
||||||
|
elif payload_type == 'dictionary':
|
||||||
|
json_str = data.decode('utf-8')
|
||||||
|
return json.loads(json_str)
|
||||||
|
elif payload_type == 'arrowtable':
|
||||||
|
if not ARROW_AVAILABLE:
|
||||||
|
raise RuntimeError('pyarrow not available for arrowtable deserialization')
|
||||||
|
|
||||||
|
import io
|
||||||
|
buf = io.BytesIO(data)
|
||||||
|
reader = ipc.open_file(buf)
|
||||||
|
return reader.read_all().to_pandas()
|
||||||
|
elif payload_type == 'jsontable':
|
||||||
|
# Deserialize JSON to list of dicts
|
||||||
|
json_str = data.decode('utf-8')
|
||||||
|
return json.loads(json_str)
|
||||||
|
elif payload_type == 'image':
|
||||||
|
return data
|
||||||
|
elif payload_type == 'audio':
|
||||||
|
return data
|
||||||
|
elif payload_type == 'video':
|
||||||
|
return data
|
||||||
|
elif payload_type == 'binary':
|
||||||
|
return data
|
||||||
|
else:
|
||||||
|
raise ValueError(f'Unknown payload_type: {payload_type}')
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------- File Server Handlers ---------------------------------------------- #
|
||||||
|
|
||||||
|
async def plik_oneshot_upload(
|
||||||
|
file_server_url: str,
|
||||||
|
dataname: str,
|
||||||
|
data: bytes
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Upload data to plik server in one-shot mode.
|
||||||
|
|
||||||
|
This function uploads a raw byte array to a plik server in one-shot mode (no upload session).
|
||||||
|
It first creates a one-shot upload session by sending a POST request with {"OneShot": true},
|
||||||
|
retrieves an upload ID and token, then uploads the file data as multipart form data using the token.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_server_url: Base URL of the plik server (e.g., "http://localhost:8080")
|
||||||
|
dataname: Name of the file being uploaded
|
||||||
|
data: Raw byte data of the file content
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys:
|
||||||
|
- "status": HTTP server response status
|
||||||
|
- "uploadid": ID of the one-shot upload session
|
||||||
|
- "fileid": ID of the uploaded file within the session
|
||||||
|
- "url": Full URL to download the uploaded file
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> fileserver_url = "http://localhost:8080"
|
||||||
|
>>> dataname = "test.txt"
|
||||||
|
>>> data = b"hello world"
|
||||||
|
>>> result = await plik_oneshot_upload(file_server_url, dataname, data)
|
||||||
|
>>> result["status"], result["uploadid"], result["fileid"], result["url"]
|
||||||
|
"""
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
# Get upload id
|
||||||
|
url_getUploadID = f"{file_server_url}/upload"
|
||||||
|
headers = {'Content-Type': 'application/json'}
|
||||||
|
body = json.dumps({"OneShot": True})
|
||||||
|
|
||||||
|
async with session.post(url_getUploadID, headers=headers, data=body) as response:
|
||||||
|
response_json = await response.json()
|
||||||
|
uploadid = response_json['id']
|
||||||
|
uploadtoken = response_json['uploadToken']
|
||||||
|
|
||||||
|
# Upload file
|
||||||
|
url_upload = f"{file_server_url}/file/{uploadid}"
|
||||||
|
headers = {'X-UploadToken': uploadtoken}
|
||||||
|
|
||||||
|
form = aiohttp.FormData()
|
||||||
|
form.add_field('file', data, filename=dataname, content_type='application/octet-stream')
|
||||||
|
|
||||||
|
async with session.post(url_upload, headers=headers, data=form) as upload_response:
|
||||||
|
upload_json = await upload_response.json()
|
||||||
|
fileid = upload_json['id']
|
||||||
|
|
||||||
|
url = f"{file_server_url}/file/{uploadid}/{fileid}/{dataname}"
|
||||||
|
|
||||||
|
return {
|
||||||
|
'status': upload_response.status,
|
||||||
|
'uploadid': uploadid,
|
||||||
|
'fileid': fileid,
|
||||||
|
'url': url
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_with_backoff(
|
||||||
|
url: str,
|
||||||
|
max_retries: int,
|
||||||
|
base_delay: int,
|
||||||
|
max_delay: int,
|
||||||
|
correlation_id: str
|
||||||
|
) -> bytes:
|
||||||
|
"""
|
||||||
|
Fetch data from URL with exponential backoff.
|
||||||
|
|
||||||
|
This internal function retrieves data from a URL with retry logic using
|
||||||
|
exponential backoff to handle transient failures.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: URL to fetch from
|
||||||
|
max_retries: Maximum number of retry attempts
|
||||||
|
base_delay: Initial delay in milliseconds
|
||||||
|
max_delay: Maximum delay in milliseconds
|
||||||
|
correlation_id: Correlation ID for logging
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Fetched data as bytes
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Error: If all retry attempts fail
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> data = await fetch_with_backoff("http://example.com/file.zip", 5, 100, 5000, "correlation123")
|
||||||
|
"""
|
||||||
|
delay = base_delay
|
||||||
|
|
||||||
|
for attempt in range(1, max_retries + 1):
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(url) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
log_trace(correlation_id, f"Successfully fetched data from {url} on attempt {attempt}")
|
||||||
|
return await response.read()
|
||||||
|
else:
|
||||||
|
raise Exception(f"Failed to fetch: {response.status}")
|
||||||
|
except Exception as e:
|
||||||
|
log_trace(correlation_id, f"Attempt {attempt} failed: {type(e).__name__}")
|
||||||
|
|
||||||
|
if attempt < max_retries:
|
||||||
|
await asyncio.sleep(delay / 1000.0)
|
||||||
|
delay = min(delay * 2, max_delay)
|
||||||
|
|
||||||
|
raise Exception(f"Failed to fetch data after {max_retries} attempts")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------- NATS Client ---------------------------------------------- #
|
||||||
|
|
||||||
|
class NATSClient:
|
||||||
|
"""NATS client wrapper for connection management."""
|
||||||
|
|
||||||
|
def __init__(self, url: str = DEFAULT_BROKER_URL):
|
||||||
|
"""
|
||||||
|
Create a new NATS client.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: NATS server URL
|
||||||
|
"""
|
||||||
|
self.url = url
|
||||||
|
self._client: NATSClient = None
|
||||||
|
|
||||||
|
async def connect(self) -> NATSClient:
|
||||||
|
"""
|
||||||
|
Connect to NATS server.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
NATS client instance
|
||||||
|
"""
|
||||||
|
if NATS_AVAILABLE:
|
||||||
|
self._client = nats.connect(self.url)
|
||||||
|
await self._client
|
||||||
|
else:
|
||||||
|
raise RuntimeError('nats-py not available')
|
||||||
|
return self._client
|
||||||
|
|
||||||
|
async def publish(self, subject: str, message: str, correlation_id: str = "") -> None:
|
||||||
|
"""
|
||||||
|
Publish message to NATS subject.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
subject: NATS subject to publish to
|
||||||
|
message: Message to publish
|
||||||
|
correlation_id: Correlation ID for logging
|
||||||
|
"""
|
||||||
|
if self._client:
|
||||||
|
await self._client.publish(subject, message)
|
||||||
|
if correlation_id:
|
||||||
|
log_trace(correlation_id, f"Message published to {subject}")
|
||||||
|
|
||||||
|
async def close(self) -> None:
|
||||||
|
"""Close the NATS connection."""
|
||||||
|
if self._client:
|
||||||
|
await self._client.drain()
|
||||||
|
await self._client.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------- Core Functions ---------------------------------------------- #
|
||||||
|
|
||||||
|
def _build_envelope(
|
||||||
|
subject: str,
|
||||||
|
payloads: List[Dict[str, Any]],
|
||||||
|
options: Dict[str, Any]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Build message envelope from payloads and metadata.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
subject: NATS subject
|
||||||
|
payloads: Array of payload objects
|
||||||
|
options: Envelope metadata options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Envelope object
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'correlation_id': options['correlation_id'],
|
||||||
|
'msg_id': options['msg_id'],
|
||||||
|
'timestamp': datetime.utcnow().isoformat() + 'Z',
|
||||||
|
'send_to': subject,
|
||||||
|
'msg_purpose': options['msg_purpose'],
|
||||||
|
'sender_name': options['sender_name'],
|
||||||
|
'sender_id': options['sender_id'],
|
||||||
|
'receiver_name': options['receiver_name'],
|
||||||
|
'receiver_id': options['receiver_id'],
|
||||||
|
'reply_to': options['reply_to'],
|
||||||
|
'reply_to_msg_id': options['reply_to_msg_id'],
|
||||||
|
'broker_url': options['broker_url'],
|
||||||
|
'metadata': options.get('metadata', {}),
|
||||||
|
'payloads': payloads
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_payload(
|
||||||
|
dataname: str,
|
||||||
|
payload_type: str,
|
||||||
|
payload_bytes: bytes,
|
||||||
|
transport: str,
|
||||||
|
data: Union[str, bytes]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Build payload object from serialized data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dataname: Name of the payload
|
||||||
|
payload_type: Type of the payload
|
||||||
|
payload_bytes: Serialized payload bytes
|
||||||
|
transport: Transport type ("direct" or "link")
|
||||||
|
data: Data (base64 for direct, URL for link)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Payload object
|
||||||
|
"""
|
||||||
|
# Determine encoding based on payload type (matching Julia/JS implementation)
|
||||||
|
encoding = 'base64'
|
||||||
|
if payload_type == 'jsontable':
|
||||||
|
encoding = 'json'
|
||||||
|
elif payload_type == 'arrowtable':
|
||||||
|
encoding = 'arrow-ipc'
|
||||||
|
|
||||||
|
return {
|
||||||
|
'id': str(uuid.uuid4()),
|
||||||
|
'dataname': dataname,
|
||||||
|
'payload_type': payload_type,
|
||||||
|
'transport': transport,
|
||||||
|
'encoding': encoding,
|
||||||
|
'size': len(payload_bytes),
|
||||||
|
'data': data,
|
||||||
|
'metadata': {'payload_bytes': len(payload_bytes)} if transport == 'direct' else {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def publish_message(
|
||||||
|
broker_url_or_client: Union[str, NATSClient, Any],
|
||||||
|
subject: str,
|
||||||
|
message: str,
|
||||||
|
correlation_id: str
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Publish message to NATS.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
broker_url_or_client: NATS URL, client, or connection
|
||||||
|
subject: NATS subject to publish to
|
||||||
|
message: JSON message to publish
|
||||||
|
correlation_id: Correlation ID for tracing
|
||||||
|
"""
|
||||||
|
if isinstance(broker_url_or_client, NATSClient):
|
||||||
|
client = broker_url_or_client
|
||||||
|
elif NATS_AVAILABLE and hasattr(broker_url_or_client, 'publish'):
|
||||||
|
# Direct NATS client connection
|
||||||
|
await broker_url_or_client.publish(subject, message)
|
||||||
|
log_trace(correlation_id, f"Message published to {subject}")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# String URL - create new client
|
||||||
|
client = NATSClient(broker_url_or_client)
|
||||||
|
await client.connect()
|
||||||
|
|
||||||
|
await client.publish(subject, message, correlation_id)
|
||||||
|
|
||||||
|
if isinstance(broker_url_or_client, NATSClient):
|
||||||
|
await broker_url_or_client.close()
|
||||||
|
elif not (NATS_AVAILABLE and hasattr(broker_url_or_client, 'publish')):
|
||||||
|
await client.close()
|
||||||
|
|
||||||
|
|
||||||
|
async def smartsend(
|
||||||
|
subject: str,
|
||||||
|
data: List[Tuple[str, Any, str]],
|
||||||
|
broker_url: str = DEFAULT_BROKER_URL,
|
||||||
|
fileserver_url: str = DEFAULT_FILESERVER_URL,
|
||||||
|
fileserver_upload_handler: Callable = plik_oneshot_upload,
|
||||||
|
size_threshold: int = DEFAULT_SIZE_THRESHOLD,
|
||||||
|
correlation_id: str = None,
|
||||||
|
msg_purpose: str = "chat",
|
||||||
|
sender_name: str = "NATSBridge",
|
||||||
|
receiver_name: str = "",
|
||||||
|
receiver_id: str = "",
|
||||||
|
reply_to: str = "",
|
||||||
|
reply_to_msg_id: str = "",
|
||||||
|
is_publish: bool = True,
|
||||||
|
nats_connection: Any = None,
|
||||||
|
msg_id: str = None,
|
||||||
|
sender_id: str = None
|
||||||
|
) -> Tuple[Dict, str]:
|
||||||
|
"""
|
||||||
|
Send data via NATS with automatic transport selection.
|
||||||
|
|
||||||
|
This function intelligently routes data delivery based on payload size.
|
||||||
|
If the serialized payload is smaller than size_threshold, it encodes the data as Base64
|
||||||
|
and publishes directly over NATS. Otherwise, it uploads the data to a fileserver
|
||||||
|
and publishes only the download URL over NATS.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
subject: NATS subject to publish the message to
|
||||||
|
data: List of (dataname, data, type) tuples to send
|
||||||
|
- dataname: Name of the payload
|
||||||
|
- data: The actual data to send
|
||||||
|
- type: Payload type: "text", "dictionary", "arrowtable", "jsontable", "image", "audio", "video", "binary"
|
||||||
|
broker_url: URL of the NATS server
|
||||||
|
fileserver_url: URL of the HTTP file server for large payloads
|
||||||
|
fileserver_upload_handler: Function to handle fileserver uploads (must return Dict with "status",
|
||||||
|
"uploadid", "fileid", "url" keys)
|
||||||
|
size_threshold: Threshold in bytes separating direct vs link transport
|
||||||
|
correlation_id: Correlation ID for tracing (auto-generated UUID if not provided)
|
||||||
|
msg_purpose: Purpose of the message: "ACK", "NACK", "updateStatus", "shutdown", "chat", etc.
|
||||||
|
sender_name: Name of the sender
|
||||||
|
receiver_name: Name of the receiver (empty string means broadcast)
|
||||||
|
receiver_id: UUID of the receiver (empty string means broadcast)
|
||||||
|
reply_to: Topic to reply to (empty string if no reply expected)
|
||||||
|
reply_to_msg_id: Message ID this message is replying to
|
||||||
|
is_publish: Whether to automatically publish the message to NATS
|
||||||
|
nats_connection: Pre-existing NATS connection (if provided, uses this connection instead of
|
||||||
|
creating a new one; saves connection establishment overhead)
|
||||||
|
msg_id: Message ID (auto-generated UUID if not provided)
|
||||||
|
sender_id: Sender ID (auto-generated UUID if not provided)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (env, env_json_str) where:
|
||||||
|
- env: Dict containing all metadata and payloads
|
||||||
|
- env_json_str: JSON string for publishing to NATS
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> # Send a single payload (still wrapped in a list)
|
||||||
|
>>> data = {"key": "value"}
|
||||||
|
>>> env, env_json_str = await smartsend(
|
||||||
|
... "my.subject",
|
||||||
|
... [("dataname1", data, "dictionary")],
|
||||||
|
... broker_url="nats://localhost:4222"
|
||||||
|
... )
|
||||||
|
>>>
|
||||||
|
>>> # Send multiple payloads with different types
|
||||||
|
>>> data1 = {"key1": "value1"}
|
||||||
|
>>> data2 = [1, 2, 3, 4, 5]
|
||||||
|
>>> env, env_json_str = await smartsend(
|
||||||
|
... "my.subject",
|
||||||
|
... [("dataname1", data1, "dictionary"), ("dataname2", data2, "arrowtable")]
|
||||||
|
... )
|
||||||
|
>>>
|
||||||
|
>>> # Send a large array using fileserver upload
|
||||||
|
>>> data = list(range(10_000_000)) # ~80 MB
|
||||||
|
>>> env, env_json_str = await smartsend(
|
||||||
|
... "large.data",
|
||||||
|
... [("large_table", data, "arrowtable")]
|
||||||
|
... )
|
||||||
|
>>>
|
||||||
|
>>> # Send jsontable (JSON format for human-readable tabular data)
|
||||||
|
>>> users = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
|
||||||
|
>>> env, env_json_str = await smartsend(
|
||||||
|
... "json.data",
|
||||||
|
... [("users", users, "jsontable")]
|
||||||
|
... )
|
||||||
|
>>>
|
||||||
|
>>> # Mixed content (e.g., chat with text and image)
|
||||||
|
>>> env, env_json_str = await smartsend(
|
||||||
|
... "chat.subject",
|
||||||
|
... [
|
||||||
|
... ("message_text", "Hello!", "text"),
|
||||||
|
... ("user_image", image_data, "image"),
|
||||||
|
... ("audio_clip", audio_data, "audio")
|
||||||
|
... ]
|
||||||
|
... )
|
||||||
|
>>>
|
||||||
|
>>> # Publish the JSON string directly using NATS request-reply pattern
|
||||||
|
>>> # reply = await nats.request(broker_url, subject, env_json_str, reply_to=reply_to_topic)
|
||||||
|
"""
|
||||||
|
if correlation_id is None:
|
||||||
|
correlation_id = str(uuid.uuid4())
|
||||||
|
if msg_id is None:
|
||||||
|
msg_id = str(uuid.uuid4())
|
||||||
|
if sender_id is None:
|
||||||
|
sender_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
log_trace(correlation_id, f"Starting smartsend for subject: {subject}")
|
||||||
|
|
||||||
|
# Process payloads
|
||||||
|
payloads = []
|
||||||
|
for dataname, payload_data, payload_type in data:
|
||||||
|
payload_bytes = _serialize_data(payload_data, payload_type)
|
||||||
|
payload_size = len(payload_bytes)
|
||||||
|
|
||||||
|
log_trace(correlation_id, f"Serialized payload '{dataname}' (type: {payload_type}) size: {payload_size} bytes")
|
||||||
|
|
||||||
|
if payload_size < size_threshold:
|
||||||
|
# Direct path
|
||||||
|
payload_b64 = base64.b64encode(payload_bytes).decode('utf-8')
|
||||||
|
log_trace(correlation_id, f"Using direct transport for {payload_size} bytes")
|
||||||
|
|
||||||
|
payload = _build_payload(dataname, payload_type, payload_bytes, 'direct', payload_b64)
|
||||||
|
payloads.append(payload)
|
||||||
|
else:
|
||||||
|
# Link path
|
||||||
|
log_trace(correlation_id, "Using link transport, uploading to fileserver")
|
||||||
|
|
||||||
|
response = await fileserver_upload_handler(fileserver_url, dataname, payload_bytes)
|
||||||
|
|
||||||
|
if response['status'] != 200:
|
||||||
|
raise Exception(f"Failed to upload data to fileserver: {response['status']}")
|
||||||
|
|
||||||
|
log_trace(correlation_id, f"Uploaded to URL: {response['url']}")
|
||||||
|
|
||||||
|
payload = _build_payload(dataname, payload_type, payload_bytes, 'link', response['url'])
|
||||||
|
payloads.append(payload)
|
||||||
|
|
||||||
|
# Build envelope
|
||||||
|
env = _build_envelope(subject, payloads, {
|
||||||
|
'correlation_id': correlation_id,
|
||||||
|
'msg_id': msg_id,
|
||||||
|
'msg_purpose': msg_purpose,
|
||||||
|
'sender_name': sender_name,
|
||||||
|
'sender_id': sender_id,
|
||||||
|
'receiver_name': receiver_name,
|
||||||
|
'receiver_id': receiver_id,
|
||||||
|
'reply_to': reply_to,
|
||||||
|
'reply_to_msg_id': reply_to_msg_id,
|
||||||
|
'broker_url': broker_url
|
||||||
|
})
|
||||||
|
|
||||||
|
env_json_str = json.dumps(env)
|
||||||
|
|
||||||
|
if is_publish:
|
||||||
|
if nats_connection:
|
||||||
|
await publish_message(nats_connection, subject, env_json_str, correlation_id)
|
||||||
|
else:
|
||||||
|
await publish_message(broker_url, subject, env_json_str, correlation_id)
|
||||||
|
|
||||||
|
return env, env_json_str
|
||||||
|
|
||||||
|
|
||||||
|
async def smartreceive(
|
||||||
|
msg: Any,
|
||||||
|
fileserver_download_handler: Callable = fetch_with_backoff,
|
||||||
|
max_retries: int = 5,
|
||||||
|
base_delay: int = 100,
|
||||||
|
max_delay: int = 5000
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Receive and process NATS messages.
|
||||||
|
|
||||||
|
This function processes incoming NATS messages, handling both direct transport
|
||||||
|
(base64 decoded payloads) and link transport (URL-based payloads).
|
||||||
|
It deserializes the data based on the transport type and returns the result.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
msg: NATS message to process
|
||||||
|
fileserver_download_handler: Function to handle downloading data from file server URLs
|
||||||
|
max_retries: Maximum retry attempts for fetching URL
|
||||||
|
base_delay: Initial delay for exponential backoff in ms
|
||||||
|
max_delay: Maximum delay for exponential backoff in ms
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with envelope metadata and payloads field containing List[Tuple[str, Any, str]]
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> # Receive and process message
|
||||||
|
>>> env = await smartreceive(msg, fileserver_download_handler=fetch_with_backoff)
|
||||||
|
>>> # env is a Dict with "payloads" key containing List[Tuple[str, Any, str]]
|
||||||
|
>>> # Access payloads: for dataname, data, type_ in env["payloads"]
|
||||||
|
>>> for dataname, data, type_ in env["payloads"]:
|
||||||
|
>>> print(f"{dataname}: {data} (type: {type_})")
|
||||||
|
"""
|
||||||
|
# Parse the JSON envelope
|
||||||
|
if isinstance(msg, dict):
|
||||||
|
# Already parsed
|
||||||
|
env_json_obj = msg
|
||||||
|
elif hasattr(msg, 'payload'):
|
||||||
|
# NATS message object
|
||||||
|
payload = msg.payload if isinstance(msg.payload, str) else msg.payload.decode('utf-8')
|
||||||
|
env_json_obj = json.loads(payload)
|
||||||
|
else:
|
||||||
|
# Assume it's already a JSON string or dict
|
||||||
|
env_json_obj = json.loads(msg) if isinstance(msg, str) else msg
|
||||||
|
|
||||||
|
log_trace(env_json_obj['correlation_id'], "Processing received message")
|
||||||
|
|
||||||
|
# Process all payloads in the envelope
|
||||||
|
payloads_list = []
|
||||||
|
num_payloads = len(env_json_obj['payloads'])
|
||||||
|
|
||||||
|
for i in range(num_payloads):
|
||||||
|
payload_obj = env_json_obj['payloads'][i]
|
||||||
|
transport = payload_obj['transport']
|
||||||
|
dataname = payload_obj['dataname']
|
||||||
|
|
||||||
|
if transport == 'direct':
|
||||||
|
log_trace(env_json_obj['correlation_id'], f"Direct transport - decoding payload '{dataname}'")
|
||||||
|
|
||||||
|
# Extract base64 payload from the payload
|
||||||
|
payload_b64 = payload_obj['data']
|
||||||
|
|
||||||
|
# Decode Base64 payload
|
||||||
|
payload_bytes = base64.b64decode(payload_b64)
|
||||||
|
|
||||||
|
# Deserialize based on type
|
||||||
|
data_type = payload_obj['payload_type']
|
||||||
|
data = _deserialize_data(payload_bytes, data_type, env_json_obj['correlation_id'])
|
||||||
|
|
||||||
|
payloads_list.append((dataname, data, data_type))
|
||||||
|
elif transport == 'link':
|
||||||
|
# Extract download URL from the payload
|
||||||
|
url = payload_obj['data']
|
||||||
|
log_trace(env_json_obj['correlation_id'], f"Link transport - fetching '{dataname}' from URL: {url}")
|
||||||
|
|
||||||
|
# Fetch with exponential backoff using the download handler
|
||||||
|
downloaded_data = await fileserver_download_handler(
|
||||||
|
url,
|
||||||
|
max_retries,
|
||||||
|
base_delay,
|
||||||
|
max_delay,
|
||||||
|
env_json_obj['correlation_id']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Deserialize based on type
|
||||||
|
data_type = payload_obj['payload_type']
|
||||||
|
data = _deserialize_data(downloaded_data, data_type, env_json_obj['correlation_id'])
|
||||||
|
|
||||||
|
payloads_list.append((dataname, data, data_type))
|
||||||
|
else:
|
||||||
|
raise Exception(f"Unknown transport type for payload '{dataname}': {transport}")
|
||||||
|
|
||||||
|
env_json_obj['payloads'] = payloads_list
|
||||||
|
return env_json_obj
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------- Module Exports ---------------------------------------------- #
|
||||||
|
|
||||||
|
class NATSBridge:
|
||||||
|
"""
|
||||||
|
Cross-platform NATS bridge implementation.
|
||||||
|
|
||||||
|
This class provides a convenient interface for NATSBridge functionality,
|
||||||
|
encapsulating the main functions and providing a class-based API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
DEFAULT_SIZE_THRESHOLD = DEFAULT_SIZE_THRESHOLD
|
||||||
|
DEFAULT_BROKER_URL = DEFAULT_BROKER_URL
|
||||||
|
DEFAULT_FILESERVER_URL = DEFAULT_FILESERVER_URL
|
||||||
|
|
||||||
|
def __init__(self, broker_url: str = None, fileserver_url: str = None):
|
||||||
|
"""
|
||||||
|
Initialize NATSBridge.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
broker_url: NATS server URL (defaults to DEFAULT_BROKER_URL)
|
||||||
|
fileserver_url: HTTP file server URL (defaults to DEFAULT_FILESERVER_URL)
|
||||||
|
"""
|
||||||
|
self.broker_url = broker_url or self.DEFAULT_BROKER_URL
|
||||||
|
self.fileserver_url = fileserver_url or self.DEFAULT_FILESERVER_URL
|
||||||
|
|
||||||
|
async def smartsend(
|
||||||
|
self,
|
||||||
|
subject: str,
|
||||||
|
data: List[Tuple[str, Any, str]],
|
||||||
|
**kwargs
|
||||||
|
) -> Tuple[Dict, str]:
|
||||||
|
"""
|
||||||
|
Send data via NATS.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
subject: NATS subject to publish to
|
||||||
|
data: List of (dataname, data, type) tuples
|
||||||
|
**kwargs: Additional options passed to smartsend
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (env, env_json_str)
|
||||||
|
"""
|
||||||
|
kwargs['broker_url'] = kwargs.get('broker_url', self.broker_url)
|
||||||
|
kwargs['fileserver_url'] = kwargs.get('fileserver_url', self.fileserver_url)
|
||||||
|
return await smartsend(subject, data, **kwargs)
|
||||||
|
|
||||||
|
async def smartreceive(
|
||||||
|
self,
|
||||||
|
msg: Any,
|
||||||
|
**kwargs
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Receive and process NATS message.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
msg: NATS message to process
|
||||||
|
**kwargs: Additional options passed to smartreceive
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with envelope metadata and payloads
|
||||||
|
"""
|
||||||
|
return await smartreceive(msg, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# Convenience functions for module-level usage
|
||||||
|
def send(
|
||||||
|
subject: str,
|
||||||
|
data: List[Tuple[str, Any, str]],
|
||||||
|
**kwargs
|
||||||
|
) -> Tuple[Dict, str]:
|
||||||
|
"""
|
||||||
|
Convenience function for sending data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
subject: NATS subject to publish to
|
||||||
|
data: List of (dataname, data, type) tuples
|
||||||
|
**kwargs: Additional options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (env, env_json_str)
|
||||||
|
"""
|
||||||
|
return asyncio.run(smartsend(subject, data, **kwargs))
|
||||||
|
|
||||||
|
|
||||||
|
def receive(
|
||||||
|
msg: Any,
|
||||||
|
**kwargs
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Convenience function for receiving messages.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
msg: NATS message to process
|
||||||
|
**kwargs: Additional options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with envelope metadata and payloads
|
||||||
|
"""
|
||||||
|
return asyncio.run(smartreceive(msg, **kwargs))
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'smartsend',
|
||||||
|
'smartreceive',
|
||||||
|
'plik_oneshot_upload',
|
||||||
|
'fetch_with_backoff',
|
||||||
|
'NATSBridge',
|
||||||
|
'send',
|
||||||
|
'receive',
|
||||||
|
'DEFAULT_SIZE_THRESHOLD',
|
||||||
|
'DEFAULT_BROKER_URL',
|
||||||
|
'DEFAULT_FILESERVER_URL',
|
||||||
|
'NATSClient',
|
||||||
|
'_serialize_data',
|
||||||
|
'_deserialize_data',
|
||||||
|
'log_trace',
|
||||||
|
'publish_message'
|
||||||
|
]
|
||||||
808
src/natsbridge_csr.js
Normal file
808
src/natsbridge_csr.js
Normal file
@@ -0,0 +1,808 @@
|
|||||||
|
/**
|
||||||
|
* NATSBridge - Cross-Platform Bi-Directional Data Bridge
|
||||||
|
* Browser-Compatible Implementation (Client-Side Rendering)
|
||||||
|
*
|
||||||
|
* This module provides functionality for sending and receiving data across network boundaries
|
||||||
|
* using NATS as the message bus, with support for both direct payload transport and
|
||||||
|
* URL-based transport for larger payloads.
|
||||||
|
*
|
||||||
|
* Supported payload types: "text", "dictionary", "arrowtable", "jsontable", "image", "audio", "video", "binary"
|
||||||
|
*
|
||||||
|
* Browser-compatible version uses:
|
||||||
|
* - nats.ws for WebSocket-based NATS connections
|
||||||
|
* - Web Crypto API for UUID generation
|
||||||
|
* - Uint8Array instead of Buffer
|
||||||
|
* - fetch API for file server communication
|
||||||
|
*
|
||||||
|
* @module NATSBridgeCSR
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Import browser-compatible NATS client
|
||||||
|
import * as nats from 'nats.ws';
|
||||||
|
|
||||||
|
// Use native fetch available in browsers
|
||||||
|
import { tableFromArrays, tableToIPC } from 'apache-arrow/browser';
|
||||||
|
|
||||||
|
// ---------------------------------------------- Constants ---------------------------------------------- //
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default size threshold for switching from direct to link transport (0.5MB)
|
||||||
|
*/
|
||||||
|
const DEFAULT_SIZE_THRESHOLD = 500_000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default NATS server URL (WebSocket protocol)
|
||||||
|
*/
|
||||||
|
const DEFAULT_BROKER_URL = 'ws://localhost:4222';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default HTTP file server URL for link transport
|
||||||
|
*/
|
||||||
|
const DEFAULT_FILESERVER_URL = 'http://localhost:8080';
|
||||||
|
|
||||||
|
// ---------------------------------------------- Utility Functions ---------------------------------------------- //
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert Uint8Array to Base64 string
|
||||||
|
* @param {Uint8Array} data - Data to encode
|
||||||
|
* @returns {string} Base64 encoded string
|
||||||
|
*/
|
||||||
|
function bufferToBase64(data) {
|
||||||
|
const bytes = new Uint8Array(data);
|
||||||
|
let binary = '';
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert Base64 string to Uint8Array
|
||||||
|
* @param {string} base64 - Base64 encoded string
|
||||||
|
* @returns {Uint8Array} Decoded binary data
|
||||||
|
*/
|
||||||
|
function base64ToBuffer(base64) {
|
||||||
|
const binary = atob(base64);
|
||||||
|
const len = binary.length;
|
||||||
|
const bytes = new Uint8Array(len);
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
bytes[i] = binary.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate UUID v4 using Web Crypto API
|
||||||
|
* @returns {string} UUID string
|
||||||
|
*/
|
||||||
|
function uuidv4() {
|
||||||
|
const array = new Uint8Array(16);
|
||||||
|
crypto.getRandomValues(array);
|
||||||
|
array[6] = (array[6] & 0x0f) | 0x40;
|
||||||
|
array[8] = (array[8] & 0x3f) | 0x80;
|
||||||
|
return Array.from(array, (val) => val.toString(16).padStart(2, '0').toUpperCase()).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a trace message with correlation ID and timestamp
|
||||||
|
* @param {string} correlationId - Correlation ID for tracing
|
||||||
|
* @param {string} message - Message content to log
|
||||||
|
*/
|
||||||
|
function logTrace(correlationId, message) {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
console.log(`[${timestamp}] [Correlation: ${correlationId}] ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------- Serialization Functions ---------------------------------------------- //
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize data according to specified format
|
||||||
|
* @param {any} data - Data to serialize
|
||||||
|
* @param {string} payloadType - Target format: "text", "dictionary", "arrowtable", "jsontable", "image", "audio", "video", "binary"
|
||||||
|
* @returns {Uint8Array} Binary representation of the serialized data
|
||||||
|
*/
|
||||||
|
async function serializeData(data, payloadType) {
|
||||||
|
if (payloadType === 'text') {
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
return new Uint8Array(new TextEncoder().encode(data));
|
||||||
|
} else {
|
||||||
|
throw new Error('Text data must be a string');
|
||||||
|
}
|
||||||
|
} else if (payloadType === 'dictionary') {
|
||||||
|
const jsonStr = JSON.stringify(data);
|
||||||
|
return new Uint8Array(new TextEncoder().encode(jsonStr));
|
||||||
|
} else if (payloadType === 'arrowtable') {
|
||||||
|
// Convert array of objects to Arrow IPC format
|
||||||
|
if (!Array.isArray(data) || data.length === 0) {
|
||||||
|
throw new Error('Arrow table data must be a non-empty array of objects');
|
||||||
|
}
|
||||||
|
|
||||||
|
return serializeArrowTable(data);
|
||||||
|
} else if (payloadType === 'jsontable') {
|
||||||
|
// Serialize array of objects to JSON format
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
throw new Error('JSON table data must be an array');
|
||||||
|
}
|
||||||
|
const jsonStr = JSON.stringify(data);
|
||||||
|
return new Uint8Array(new TextEncoder().encode(jsonStr));
|
||||||
|
} else if (payloadType === 'image') {
|
||||||
|
if (data instanceof Uint8Array || data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
|
||||||
|
return new Uint8Array(data);
|
||||||
|
} else {
|
||||||
|
throw new Error('Image data must be Uint8Array, ArrayBuffer, or ArrayBuffer view');
|
||||||
|
}
|
||||||
|
} else if (payloadType === 'audio') {
|
||||||
|
if (data instanceof Uint8Array || data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
|
||||||
|
return new Uint8Array(data);
|
||||||
|
} else {
|
||||||
|
throw new Error('Audio data must be Uint8Array, ArrayBuffer, or ArrayBuffer view');
|
||||||
|
}
|
||||||
|
} else if (payloadType === 'video') {
|
||||||
|
if (data instanceof Uint8Array || data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
|
||||||
|
return new Uint8Array(data);
|
||||||
|
} else {
|
||||||
|
throw new Error('Video data must be Uint8Array, ArrayBuffer, or ArrayBuffer view');
|
||||||
|
}
|
||||||
|
} else if (payloadType === 'binary') {
|
||||||
|
if (data instanceof Uint8Array || data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
|
||||||
|
return new Uint8Array(data);
|
||||||
|
} else {
|
||||||
|
throw new Error('Binary data must be Uint8Array, ArrayBuffer, or ArrayBuffer view');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown payload_type: ${payloadType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to properly serialize table data to Arrow IPC
|
||||||
|
* @param {Array<Object>} data - Array of objects representing table rows
|
||||||
|
* @returns {Uint8Array} Arrow IPC formatted buffer
|
||||||
|
*/
|
||||||
|
function serializeArrowTable(data) {
|
||||||
|
if (!Array.isArray(data) || data.length === 0) {
|
||||||
|
throw new Error('Table data must be a non-empty array of objects');
|
||||||
|
}
|
||||||
|
|
||||||
|
logTrace('serializeArrowTable', `Serializing table with ${data.length} rows`);
|
||||||
|
|
||||||
|
// Convert array of objects to a key-value format expected by tableFromArrays
|
||||||
|
const columns = {};
|
||||||
|
const keys = Object.keys(data[0]);
|
||||||
|
for (const key of keys) {
|
||||||
|
columns[key] = data.map(row => row[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
logTrace('serializeArrowTable', `Columns: ${Object.keys(columns).join(', ')}`);
|
||||||
|
|
||||||
|
const table = tableFromArrays(columns);
|
||||||
|
|
||||||
|
logTrace('serializeArrowTable', `Arrow table created with ${table.numRows} rows, ${table.numCols} cols`);
|
||||||
|
|
||||||
|
// Convert to IPC format
|
||||||
|
const ipcBuffer = tableToIPC(table);
|
||||||
|
|
||||||
|
logTrace('serializeArrowTable', `IPC buffer type: ${typeof ipcBuffer}, byteLength: ${ipcBuffer.byteLength}`);
|
||||||
|
|
||||||
|
const resultBuffer = new Uint8Array(ipcBuffer);
|
||||||
|
logTrace('serializeArrowTable', `Result buffer: ${resultBuffer.length} bytes`);
|
||||||
|
|
||||||
|
// Debug: Show first 20 bytes in hex
|
||||||
|
const hexPreview = [];
|
||||||
|
for (let i = 0; i < Math.min(20, resultBuffer.length); i++) {
|
||||||
|
hexPreview.push(resultBuffer[i].toString(16).padStart(2, '0'));
|
||||||
|
}
|
||||||
|
logTrace('serializeArrowTable', `First 20 bytes (hex): ${hexPreview.join(' ')}`);
|
||||||
|
|
||||||
|
return resultBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserialize bytes to data based on type
|
||||||
|
* @param {Uint8Array|ArrayBuffer} data - Serialized data as bytes
|
||||||
|
* @param {string} payloadType - Data type
|
||||||
|
* @param {string} correlationId - Correlation ID for logging
|
||||||
|
* @returns {any} Deserialized data
|
||||||
|
*/
|
||||||
|
async function deserializeData(data, payloadType, correlationId) {
|
||||||
|
const buffer = data instanceof Uint8Array ? data : new Uint8Array(data);
|
||||||
|
|
||||||
|
logTrace(correlationId, `deserializeData: type=${payloadType}, bufferLength=${buffer.length}`);
|
||||||
|
|
||||||
|
// Debug: Show first 20 bytes in hex for binary data
|
||||||
|
if (payloadType === 'arrowtable' || payloadType === 'jsontable' || payloadType === 'image' || payloadType === 'binary') {
|
||||||
|
const hexPreview = [];
|
||||||
|
for (let i = 0; i < Math.min(20, buffer.length); i++) {
|
||||||
|
hexPreview.push(buffer[i].toString(16).padStart(2, '0'));
|
||||||
|
}
|
||||||
|
logTrace(correlationId, `deserializeData: First 20 bytes (hex): ${hexPreview.join(' ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payloadType === 'text') {
|
||||||
|
const result = new TextDecoder().decode(buffer);
|
||||||
|
logTrace(correlationId, `deserializeData: text result length=${result.length}`);
|
||||||
|
return result;
|
||||||
|
} else if (payloadType === 'dictionary') {
|
||||||
|
const jsonStr = new TextDecoder().decode(buffer);
|
||||||
|
const result = JSON.parse(jsonStr);
|
||||||
|
logTrace(correlationId, `deserializeData: dictionary keys=${Object.keys(result).join(', ')}`);
|
||||||
|
return result;
|
||||||
|
} else if (payloadType === 'arrowtable') {
|
||||||
|
logTrace(correlationId, `deserializeData: Attempting Arrow table deserialization`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try tableFromIPC (browser API)
|
||||||
|
const table = tableFromIPC(buffer);
|
||||||
|
logTrace(correlationId, `deserializeData: Arrow table from IPC - rows=${table.numRows}, cols=${table.numCols}`);
|
||||||
|
return table;
|
||||||
|
} catch (e) {
|
||||||
|
logTrace(correlationId, `deserializeData: tableFromIPC failed: ${e.message}`);
|
||||||
|
throw new Error(`Unable to deserialize Arrow table: ${e.message}`);
|
||||||
|
}
|
||||||
|
} else if (payloadType === 'jsontable') {
|
||||||
|
const jsonStr = new TextDecoder().decode(buffer);
|
||||||
|
const result = JSON.parse(jsonStr);
|
||||||
|
logTrace(correlationId, `deserializeData: jsontable result length=${Array.isArray(result) ? result.length : 'N/A'}`);
|
||||||
|
return result;
|
||||||
|
} else if (payloadType === 'image') {
|
||||||
|
logTrace(correlationId, `deserializeData: image buffer length=${buffer.length}`);
|
||||||
|
return buffer;
|
||||||
|
} else if (payloadType === 'audio') {
|
||||||
|
logTrace(correlationId, `deserializeData: audio buffer length=${buffer.length}`);
|
||||||
|
return buffer;
|
||||||
|
} else if (payloadType === 'video') {
|
||||||
|
logTrace(correlationId, `deserializeData: video buffer length=${buffer.length}`);
|
||||||
|
return buffer;
|
||||||
|
} else if (payloadType === 'binary') {
|
||||||
|
logTrace(correlationId, `deserializeData: binary buffer length=${buffer.length}`);
|
||||||
|
return buffer;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown payload_type: ${payloadType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------- File Server Handlers ---------------------------------------------- //
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload data to plik server in one-shot mode
|
||||||
|
* @param {string} fileServerUrl - Base URL of the plik server
|
||||||
|
* @param {string} dataname - Name of the file being uploaded
|
||||||
|
* @param {Uint8Array} data - Raw byte data of the file content
|
||||||
|
* @returns {Promise<{status: number, uploadid: string, fileid: string, url: string}>}
|
||||||
|
*/
|
||||||
|
async function plikOneshotUpload(fileServerUrl, dataname, data) {
|
||||||
|
const buffer = data instanceof Uint8Array ? data : new Uint8Array(data);
|
||||||
|
|
||||||
|
// Get upload id
|
||||||
|
const urlGetUploadID = `${fileServerUrl}/upload`;
|
||||||
|
const headers = { 'Content-Type': 'application/json' };
|
||||||
|
const body = JSON.stringify({ OneShot: true });
|
||||||
|
|
||||||
|
const httpResponse = await fetch(urlGetUploadID, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseJson = await httpResponse.json();
|
||||||
|
const uploadid = responseJson.id;
|
||||||
|
const uploadtoken = responseJson.uploadToken;
|
||||||
|
|
||||||
|
// Upload file
|
||||||
|
const urlUpload = `${fileServerUrl}/file/${uploadid}`;
|
||||||
|
const form = new FormData();
|
||||||
|
const blob = new Blob([buffer], { type: 'application/octet-stream' });
|
||||||
|
form.append('file', blob, dataname);
|
||||||
|
|
||||||
|
const uploadHeaders = {
|
||||||
|
'X-UploadToken': uploadtoken
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadResponse = await fetch(urlUpload, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: uploadHeaders,
|
||||||
|
body: form
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploadJson = await uploadResponse.json();
|
||||||
|
const fileid = uploadJson.id;
|
||||||
|
|
||||||
|
const url = `${fileServerUrl}/file/${uploadid}/${fileid}/${dataname}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: uploadResponse.status,
|
||||||
|
uploadid,
|
||||||
|
fileid,
|
||||||
|
url
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch data from URL with exponential backoff
|
||||||
|
* @param {string} url - URL to fetch from
|
||||||
|
* @param {number} maxRetries - Maximum number of retry attempts
|
||||||
|
* @param {number} baseDelay - Initial delay in milliseconds
|
||||||
|
* @param {number} maxDelay - Maximum delay in milliseconds
|
||||||
|
* @param {string} correlationId - Correlation ID for logging
|
||||||
|
* @returns {Promise<Uint8Array>} Fetched data as bytes
|
||||||
|
*/
|
||||||
|
async function fetchWithBackoff(url, maxRetries, baseDelay, maxDelay, correlationId) {
|
||||||
|
let delay = baseDelay;
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
logTrace(correlationId, `Successfully fetched data from ${url} on attempt ${attempt}`);
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
return new Uint8Array(arrayBuffer);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Failed to fetch: ${response.status}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logTrace(correlationId, `Attempt ${attempt} failed: ${e.constructor.name} - ${e.message}`);
|
||||||
|
|
||||||
|
if (attempt < maxRetries) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
delay = Math.min(delay * 2, maxDelay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Failed to fetch data after ${maxRetries} attempts`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------- NATS Client ---------------------------------------------- //
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NATS client wrapper for connection management
|
||||||
|
*/
|
||||||
|
class NATSClient {
|
||||||
|
/**
|
||||||
|
* Create a new NATS client
|
||||||
|
* @param {string} url - NATS server URL (ws:// or wss://)
|
||||||
|
*/
|
||||||
|
constructor(url) {
|
||||||
|
this.url = url;
|
||||||
|
this.connection = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to NATS server
|
||||||
|
* @returns {Promise<NATS.Connection>}
|
||||||
|
*/
|
||||||
|
async connect() {
|
||||||
|
this.connection = await nats.connect({ servers: this.url });
|
||||||
|
return this.connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publish message to NATS subject
|
||||||
|
* @param {string} subject - NATS subject to publish to
|
||||||
|
* @param {string} message - Message to publish
|
||||||
|
* @param {string} correlationId - Correlation ID for logging
|
||||||
|
*/
|
||||||
|
async publish(subject, message, correlationId) {
|
||||||
|
if (!this.connection) {
|
||||||
|
await this.connect();
|
||||||
|
}
|
||||||
|
await this.connection.publish(subject, message);
|
||||||
|
logTrace(correlationId, `Message published to ${subject}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the NATS connection
|
||||||
|
*/
|
||||||
|
async close() {
|
||||||
|
if (this.connection) {
|
||||||
|
this.connection.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------- Core Functions ---------------------------------------------- //
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publish message to NATS
|
||||||
|
* @param {string|NATSClient|NATS.Connection} brokerUrlOrClient - NATS URL, client, or connection
|
||||||
|
* @param {string} subject - NATS subject to publish to
|
||||||
|
* @param {string} message - JSON message to publish
|
||||||
|
* @param {string} correlationId - Correlation ID for tracing
|
||||||
|
*/
|
||||||
|
async function publishMessage(brokerUrlOrClient, subject, message, correlationId) {
|
||||||
|
let conn;
|
||||||
|
|
||||||
|
if (brokerUrlOrClient instanceof NATSClient) {
|
||||||
|
conn = brokerUrlOrClient;
|
||||||
|
} else if (brokerUrlOrClient && typeof brokerUrlOrClient.publish === 'function') {
|
||||||
|
// Create a wrapper for direct connection (duck-typing check for NATS connection)
|
||||||
|
conn = {
|
||||||
|
async publish(subj, msg) {
|
||||||
|
await brokerUrlOrClient.publish(subj, msg);
|
||||||
|
},
|
||||||
|
async close() {
|
||||||
|
await brokerUrlOrClient.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// String URL - create new client
|
||||||
|
const client = new NATSClient(brokerUrlOrClient);
|
||||||
|
conn = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
await conn.publish(subject, message, correlationId);
|
||||||
|
|
||||||
|
if (conn instanceof NATSClient) {
|
||||||
|
await conn.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build message envelope from payloads and metadata
|
||||||
|
* @param {string} subject - NATS subject
|
||||||
|
* @param {Array} payloads - Array of payload objects
|
||||||
|
* @param {Object} options - Envelope metadata options
|
||||||
|
* @returns {Object} Envelope object
|
||||||
|
*/
|
||||||
|
function buildEnvelope(subject, payloads, options) {
|
||||||
|
return {
|
||||||
|
correlation_id: options.correlation_id,
|
||||||
|
msg_id: options.msg_id,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
send_to: subject,
|
||||||
|
msg_purpose: options.msg_purpose,
|
||||||
|
sender_name: options.sender_name,
|
||||||
|
sender_id: options.sender_id,
|
||||||
|
receiver_name: options.receiver_name,
|
||||||
|
receiver_id: options.receiver_id,
|
||||||
|
reply_to: options.reply_to,
|
||||||
|
reply_to_msg_id: options.reply_to_msg_id,
|
||||||
|
broker_url: options.broker_url,
|
||||||
|
metadata: options.metadata || {},
|
||||||
|
payloads: payloads
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build payload object from serialized data
|
||||||
|
* @param {string} dataname - Name of the payload
|
||||||
|
* @param {string} payloadType - Type of the payload
|
||||||
|
* @param {Uint8Array} payloadBytes - Serialized payload bytes
|
||||||
|
* @param {string} transport - Transport type ("direct" or "link")
|
||||||
|
* @param {string} data - Data (base64 for direct, URL for link)
|
||||||
|
* @returns {Object} Payload object
|
||||||
|
*/
|
||||||
|
function buildPayload(dataname, payloadType, payloadBytes, transport, data) {
|
||||||
|
// Determine encoding based on payload type (matching Julia implementation)
|
||||||
|
let encoding = 'base64';
|
||||||
|
if (payloadType === 'jsontable') {
|
||||||
|
encoding = 'json';
|
||||||
|
} else if (payloadType === 'arrowtable') {
|
||||||
|
encoding = 'arrow-ipc';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: uuidv4(),
|
||||||
|
dataname,
|
||||||
|
payload_type: payloadType,
|
||||||
|
transport,
|
||||||
|
encoding,
|
||||||
|
size: payloadBytes.byteLength,
|
||||||
|
data,
|
||||||
|
metadata: transport === 'direct' ? { payload_bytes: payloadBytes.byteLength } : {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send data via NATS with automatic transport selection
|
||||||
|
*
|
||||||
|
* This function intelligently routes data delivery based on payload size.
|
||||||
|
* If the serialized payload is smaller than size_threshold, it encodes the data as Base64
|
||||||
|
* and publishes directly over NATS. Otherwise, it uploads the data to a fileserver
|
||||||
|
* and publishes only the download URL over NATS.
|
||||||
|
*
|
||||||
|
* @param {string} subject - NATS subject to publish the message to
|
||||||
|
* @param {Array} data - List of [dataname, data, type] tuples to send
|
||||||
|
* - type: "text", "dictionary", "arrowtable", "jsontable", "image", "audio", "video", "binary"
|
||||||
|
* @param {Object} options - Optional configuration
|
||||||
|
* @param {string} [options.broker_url=DEFAULT_BROKER_URL] - URL of the NATS server (WebSocket)
|
||||||
|
* @param {string} [options.fileserver_url=DEFAULT_FILESERVER_URL] - URL of the HTTP file server
|
||||||
|
* @param {Function} [options.fileserver_upload_handler=plikOneshotUpload] - Function to handle fileserver uploads
|
||||||
|
* @param {number} [options.size_threshold=DEFAULT_SIZE_THRESHOLD] - Threshold separating direct vs link transport
|
||||||
|
* @param {string} [options.correlation_id=uuidv4()] - Correlation ID for tracing
|
||||||
|
* @param {string} [options.msg_purpose="chat"] - Purpose of the message
|
||||||
|
* @param {string} [options.sender_name="NATSBridge"] - Name of the sender
|
||||||
|
* @param {string} [options.receiver_name=""] - Name of the receiver (empty means broadcast)
|
||||||
|
* @param {string} [options.receiver_id=""] - UUID of the receiver (empty means broadcast)
|
||||||
|
* @param {string} [options.reply_to=""] - Topic to reply to
|
||||||
|
* @param {string} [options.reply_to_msg_id=""] - Message ID this message is replying to
|
||||||
|
* @param {boolean} [options.is_publish=true] - Whether to automatically publish the message
|
||||||
|
* @param {NATSClient|NATS.Connection} [options.nats_connection=null] - Pre-existing NATS connection
|
||||||
|
* @param {string} [options.msg_id=uuidv4()] - Message ID
|
||||||
|
* @param {string} [options.sender_id=uuidv4()] - Sender ID
|
||||||
|
* @returns {Promise<[Object, string]>} Tuple of [env, env_json_str]
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Send a single payload
|
||||||
|
* const [env, envJsonStr] = await NATSBridgeCSR.smartsend(
|
||||||
|
* "/test",
|
||||||
|
* [["dataname1", data1, "dictionary"]],
|
||||||
|
* { broker_url: "ws://localhost:4222" }
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* // Send multiple payloads
|
||||||
|
* const [env, envJsonStr] = await NATSBridgeCSR.smartsend(
|
||||||
|
* "/test",
|
||||||
|
* [
|
||||||
|
* ["dataname1", data1, "dictionary"],
|
||||||
|
* ["dataname2", data2, "arrowtable"]
|
||||||
|
* ],
|
||||||
|
* { broker_url: "ws://localhost:4222" }
|
||||||
|
* );
|
||||||
|
*/
|
||||||
|
async function smartsend(subject, data, options = {}) {
|
||||||
|
const {
|
||||||
|
broker_url = DEFAULT_BROKER_URL,
|
||||||
|
fileserver_url = DEFAULT_FILESERVER_URL,
|
||||||
|
fileserver_upload_handler = plikOneshotUpload,
|
||||||
|
size_threshold = DEFAULT_SIZE_THRESHOLD,
|
||||||
|
correlation_id = uuidv4(),
|
||||||
|
msg_purpose = 'chat',
|
||||||
|
sender_name = 'NATSBridge',
|
||||||
|
receiver_name = '',
|
||||||
|
receiver_id = '',
|
||||||
|
reply_to = '',
|
||||||
|
reply_to_msg_id = '',
|
||||||
|
is_publish = true,
|
||||||
|
nats_connection = null,
|
||||||
|
msg_id = uuidv4(),
|
||||||
|
sender_id = uuidv4()
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
logTrace(correlation_id, `Starting smartsend for subject: ${subject}`);
|
||||||
|
logTrace(correlation_id, `smartsend: data array length=${data.length}`);
|
||||||
|
|
||||||
|
// Debug: Log input data structure
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const [dataname, payloadData, payloadType] = data[i];
|
||||||
|
logTrace(correlation_id, `smartsend: payload[${i}] dataname=${dataname}, type=${payloadType}, data type=${typeof payloadData}, constructor=${payloadData?.constructor?.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process payloads
|
||||||
|
const payloads = [];
|
||||||
|
for (const [dataname, payloadData, payloadType] of data) {
|
||||||
|
logTrace(correlation_id, `smartsend: Processing payload '${dataname}' type=${payloadType}`);
|
||||||
|
logTrace(correlation_id, `smartsend: payloadData type=${typeof payloadData}, constructor=${payloadData?.constructor?.name}`);
|
||||||
|
|
||||||
|
const payloadBytes = await serializeData(payloadData, payloadType);
|
||||||
|
const payloadSize = payloadBytes.byteLength;
|
||||||
|
|
||||||
|
logTrace(correlation_id, `Serialized payload '${dataname}' (type: ${payloadType}) size: ${payloadSize} bytes`);
|
||||||
|
|
||||||
|
// Debug: Show first 20 bytes of serialized data for table type
|
||||||
|
if (payloadType === 'table') {
|
||||||
|
const hexPreview = [];
|
||||||
|
for (let i = 0; i < Math.min(20, payloadBytes.length); i++) {
|
||||||
|
hexPreview.push(payloadBytes[i].toString(16).padStart(2, '0'));
|
||||||
|
}
|
||||||
|
logTrace(correlation_id, `Serialized table data first 20 bytes (hex): ${hexPreview.join(' ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payloadSize < size_threshold) {
|
||||||
|
// Direct path
|
||||||
|
const payloadB64 = bufferToBase64(payloadBytes);
|
||||||
|
logTrace(correlation_id, `Using direct transport for ${payloadSize} bytes, base64 length=${payloadB64.length}`);
|
||||||
|
|
||||||
|
const payload = buildPayload(dataname, payloadType, payloadBytes, 'direct', payloadB64);
|
||||||
|
payloads.push(payload);
|
||||||
|
} else {
|
||||||
|
// Link path
|
||||||
|
logTrace(correlation_id, `Using link transport, uploading to fileserver`);
|
||||||
|
|
||||||
|
const response = await fileserver_upload_handler(fileserver_url, dataname, payloadBytes);
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(`Failed to upload data to fileserver: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logTrace(correlation_id, `Uploaded to URL: ${response.url}`);
|
||||||
|
|
||||||
|
const payload = buildPayload(dataname, payloadType, payloadBytes, 'link', response.url);
|
||||||
|
payloads.push(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build envelope
|
||||||
|
const env = buildEnvelope(subject, payloads, {
|
||||||
|
correlation_id,
|
||||||
|
msg_id,
|
||||||
|
msg_purpose,
|
||||||
|
sender_name,
|
||||||
|
sender_id,
|
||||||
|
receiver_name,
|
||||||
|
receiver_id,
|
||||||
|
reply_to,
|
||||||
|
reply_to_msg_id,
|
||||||
|
broker_url
|
||||||
|
});
|
||||||
|
|
||||||
|
const env_json_str = JSON.stringify(env);
|
||||||
|
|
||||||
|
if (is_publish) {
|
||||||
|
if (nats_connection) {
|
||||||
|
await publishMessage(nats_connection, subject, env_json_str, correlation_id);
|
||||||
|
} else {
|
||||||
|
await publishMessage(broker_url, subject, env_json_str, correlation_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [env, env_json_str];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receive and process NATS message
|
||||||
|
*
|
||||||
|
* This function processes incoming NATS messages, handling both direct transport
|
||||||
|
* (base64 decoded payloads) and link transport (URL-based payloads).
|
||||||
|
* It deserializes the data based on the transport type and returns the result.
|
||||||
|
*
|
||||||
|
* @param {Object} msg - NATS message object with payload property
|
||||||
|
* @param {Object} options - Optional configuration
|
||||||
|
* @param {Function} [options.fileserver_download_handler=fetchWithBackoff] - Function to handle fileserver downloads
|
||||||
|
* @param {number} [options.max_retries=5] - Maximum retry attempts for fetching URL
|
||||||
|
* @param {number} [options.base_delay=100] - Initial delay for exponential backoff in ms
|
||||||
|
* @param {number} [options.max_delay=5000] - Maximum delay for exponential backoff in ms
|
||||||
|
* @returns {Promise<Object>} Envelope object with processed payloads
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Receive and process message
|
||||||
|
* const env = await NATSBridgeCSR.smartreceive(msg, {
|
||||||
|
* fileserver_download_handler: NATSBridgeCSR.fetchWithBackoff,
|
||||||
|
* max_retries: 5,
|
||||||
|
* base_delay: 100,
|
||||||
|
* max_delay: 5000
|
||||||
|
* });
|
||||||
|
* // env.payloads is an Array of [dataname, data, type] arrays
|
||||||
|
* for (const [dataname, data, type] of env.payloads) {
|
||||||
|
* console.log(`${dataname}: ${data} (type: ${type})`);
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
async function smartreceive(msg, options = {}) {
|
||||||
|
const {
|
||||||
|
fileserver_download_handler = fetchWithBackoff,
|
||||||
|
max_retries = 5,
|
||||||
|
base_delay = 100,
|
||||||
|
max_delay = 5000
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
// Debug: Log message object structure
|
||||||
|
logTrace('smartreceive', `smartreceive: msg object keys: ${Object.keys(msg).join(', ')}`);
|
||||||
|
logTrace('smartreceive', `smartreceive: msg.data type: ${typeof msg.data}, constructor: ${msg.data?.constructor?.name}`);
|
||||||
|
logTrace('smartreceive', `smartreceive: msg.payload type: ${typeof msg.payload}, constructor: ${msg.payload?.constructor?.name}`);
|
||||||
|
|
||||||
|
// Parse the JSON envelope
|
||||||
|
// NATS.js v2.x uses msg.data instead of msg.payload
|
||||||
|
let payload;
|
||||||
|
if (msg.data !== undefined) {
|
||||||
|
payload = typeof msg.data === 'string' ? msg.data : new TextDecoder().decode(msg.data);
|
||||||
|
} else if (msg.payload !== undefined) {
|
||||||
|
payload = typeof msg.payload === 'string' ? msg.payload : new TextDecoder().decode(msg.payload);
|
||||||
|
} else {
|
||||||
|
throw new Error('Message has neither data nor payload property');
|
||||||
|
}
|
||||||
|
|
||||||
|
logTrace('smartreceive', `smartreceive: raw payload length=${payload.length}`);
|
||||||
|
|
||||||
|
// Debug: Show first 200 chars of payload
|
||||||
|
const payloadPreview = payload.substring(0, 200);
|
||||||
|
logTrace('smartreceive', `smartreceive: payload preview: ${payloadPreview}`);
|
||||||
|
|
||||||
|
let envJsonObj;
|
||||||
|
try {
|
||||||
|
envJsonObj = JSON.parse(payload);
|
||||||
|
} catch (e) {
|
||||||
|
logTrace('smartreceive', `smartreceive: JSON parse failed: ${e.message}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
logTrace(envJsonObj.correlation_id, 'Processing received message');
|
||||||
|
logTrace(envJsonObj.correlation_id, `smartreceive: envelope has ${envJsonObj.payloads.length} payloads`);
|
||||||
|
|
||||||
|
// Process all payloads in the envelope
|
||||||
|
const payloadsList = [];
|
||||||
|
const numPayloads = envJsonObj.payloads.length;
|
||||||
|
|
||||||
|
logTrace(envJsonObj.correlation_id, `smartreceive: Processing ${numPayloads} payloads`);
|
||||||
|
|
||||||
|
for (let i = 0; i < numPayloads; i++) {
|
||||||
|
const payloadObj = envJsonObj.payloads[i];
|
||||||
|
const transport = payloadObj.transport;
|
||||||
|
const dataname = payloadObj.dataname;
|
||||||
|
const payloadType = payloadObj.payload_type;
|
||||||
|
|
||||||
|
logTrace(envJsonObj.correlation_id, `smartreceive: Processing payload ${i + 1}/${numPayloads}: dataname=${dataname}, type=${payloadType}, transport=${transport}`);
|
||||||
|
|
||||||
|
if (transport === 'direct') {
|
||||||
|
logTrace(envJsonObj.correlation_id, `Direct transport - decoding payload '${dataname}'`);
|
||||||
|
|
||||||
|
// Extract base64 payload from the payload
|
||||||
|
const payloadB64 = payloadObj.data;
|
||||||
|
logTrace(envJsonObj.correlation_id, `Direct transport: base64 length=${payloadB64?.length}`);
|
||||||
|
|
||||||
|
// Decode Base64 payload
|
||||||
|
const payloadBytes = base64ToBuffer(payloadB64);
|
||||||
|
logTrace(envJsonObj.correlation_id, `Direct transport: decoded bytes=${payloadBytes.length}`);
|
||||||
|
|
||||||
|
// Deserialize based on type
|
||||||
|
const dataType = payloadObj.payload_type;
|
||||||
|
const data = await deserializeData(payloadBytes, dataType, envJsonObj.correlation_id);
|
||||||
|
logTrace(envJsonObj.correlation_id, `Direct transport: deserialized data type=${typeof data}, constructor=${data?.constructor?.name}`);
|
||||||
|
|
||||||
|
payloadsList.push([dataname, data, dataType]);
|
||||||
|
} else if (transport === 'link') {
|
||||||
|
// Extract download URL from the payload
|
||||||
|
const url = payloadObj.data;
|
||||||
|
logTrace(envJsonObj.correlation_id, `Link transport - fetching '${dataname}' from URL: ${url}`);
|
||||||
|
|
||||||
|
// Fetch with exponential backoff using the download handler
|
||||||
|
const downloadedData = await fileserver_download_handler(
|
||||||
|
url,
|
||||||
|
max_retries,
|
||||||
|
base_delay,
|
||||||
|
max_delay,
|
||||||
|
envJsonObj.correlation_id
|
||||||
|
);
|
||||||
|
|
||||||
|
// Deserialize based on type
|
||||||
|
const dataType = payloadObj.payload_type;
|
||||||
|
const data = await deserializeData(downloadedData, dataType, envJsonObj.correlation_id);
|
||||||
|
|
||||||
|
payloadsList.push([dataname, data, dataType]);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown transport type for payload '${dataname}': ${transport}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logTrace(envJsonObj.correlation_id, `smartreceive: Successfully processed all ${payloadsList.length} payloads`);
|
||||||
|
envJsonObj.payloads = payloadsList;
|
||||||
|
return envJsonObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------- Module Exports ---------------------------------------------- //
|
||||||
|
|
||||||
|
const NATSBridgeCSR = {
|
||||||
|
/**
|
||||||
|
* NATS client class for connection management
|
||||||
|
*/
|
||||||
|
NATSClient,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send data via NATS with automatic transport selection
|
||||||
|
*/
|
||||||
|
smartsend,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receive and process NATS message
|
||||||
|
*/
|
||||||
|
smartreceive,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload data to plik server in one-shot mode
|
||||||
|
*/
|
||||||
|
plikOneshotUpload,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch data from URL with exponential backoff
|
||||||
|
*/
|
||||||
|
fetchWithBackoff,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default constants
|
||||||
|
*/
|
||||||
|
DEFAULT_SIZE_THRESHOLD,
|
||||||
|
DEFAULT_BROKER_URL,
|
||||||
|
DEFAULT_FILESERVER_URL
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NATSBridgeCSR;
|
||||||
673
src/natsbridge_mpy.py
Normal file
673
src/natsbridge_mpy.py
Normal file
@@ -0,0 +1,673 @@
|
|||||||
|
"""
|
||||||
|
NATSBridge - Cross-Platform Bi-Directional Data Bridge
|
||||||
|
MicroPython Implementation
|
||||||
|
|
||||||
|
This module provides functionality for sending and receiving data across network boundaries
|
||||||
|
using NATS as the message bus, with support for both direct payload transport and
|
||||||
|
URL-based transport for larger payloads.
|
||||||
|
|
||||||
|
Note: MicroPython has significant constraints compared to desktop implementations:
|
||||||
|
- Limited memory (~256KB - 1MB)
|
||||||
|
- No Arrow IPC support (memory constraints)
|
||||||
|
- Synchronous API (no async/await)
|
||||||
|
- Lower size threshold for direct transport
|
||||||
|
"""
|
||||||
|
|
||||||
|
import network
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
import uos
|
||||||
|
import struct
|
||||||
|
import random
|
||||||
|
|
||||||
|
# ---------------------------------------------- Constants ---------------------------------------------- #
|
||||||
|
|
||||||
|
"""
|
||||||
|
Default size threshold for switching from direct to link transport (100KB for MicroPython)
|
||||||
|
"""
|
||||||
|
DEFAULT_SIZE_THRESHOLD = 100000
|
||||||
|
|
||||||
|
"""
|
||||||
|
Default NATS server URL
|
||||||
|
"""
|
||||||
|
DEFAULT_BROKER_URL = "nats://localhost:4222"
|
||||||
|
|
||||||
|
"""
|
||||||
|
Default HTTP file server URL for link transport
|
||||||
|
"""
|
||||||
|
DEFAULT_FILESERVER_URL = "http://localhost:8080"
|
||||||
|
|
||||||
|
"""
|
||||||
|
Hard limit for payload size in MicroPython (50KB)
|
||||||
|
"""
|
||||||
|
MAX_PAYLOAD_SIZE = 50000
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------- Utility Functions ---------------------------------------------- #
|
||||||
|
|
||||||
|
def log_trace(correlation_id, message):
|
||||||
|
"""
|
||||||
|
Log a trace message with correlation ID and timestamp.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
correlation_id: Correlation ID for tracing
|
||||||
|
message: Message content to log
|
||||||
|
"""
|
||||||
|
timestamp = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
|
||||||
|
print(f"[{timestamp}] [Correlation: {correlation_id}] {message}")
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_uuid():
|
||||||
|
"""
|
||||||
|
Generate a simple UUID compatible with MicroPython.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
UUID string
|
||||||
|
"""
|
||||||
|
# Generate a simple UUID-like string
|
||||||
|
# Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||||
|
hex_chars = '0123456789abcdef'
|
||||||
|
uuid_str = ''.join([random.choice(hex_chars) for _ in range(32)])
|
||||||
|
# Insert hyphens at proper positions
|
||||||
|
return f"{uuid_str[:8]}-{uuid_str[8:12]}-{uuid_str[12:16]}-{uuid_str[16:20]}-{uuid_str[20:]}"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------- Serialization Functions ---------------------------------------------- #
|
||||||
|
|
||||||
|
def _serialize_data(data, payload_type):
|
||||||
|
"""
|
||||||
|
Serialize data according to specified format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Data to serialize (string for "text", dict for "dictionary",
|
||||||
|
bytes for "image", "audio", "video", "binary")
|
||||||
|
payload_type: Target format: "text", "dictionary", "image", "audio", "video", "binary"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Binary representation of the serialized data
|
||||||
|
|
||||||
|
Note:
|
||||||
|
MicroPython does not support "table" type due to memory constraints.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If payload_type is not one of the supported types
|
||||||
|
"""
|
||||||
|
if payload_type == 'text':
|
||||||
|
if isinstance(data, str):
|
||||||
|
return data.encode('utf-8')
|
||||||
|
else:
|
||||||
|
raise ValueError('Text data must be a string')
|
||||||
|
elif payload_type == 'dictionary':
|
||||||
|
json_str = json.dumps(data)
|
||||||
|
return json_str.encode('utf-8')
|
||||||
|
elif payload_type in ('image', 'audio', 'video', 'binary'):
|
||||||
|
if isinstance(data, (bytes, bytearray, memoryview)):
|
||||||
|
return bytes(data)
|
||||||
|
else:
|
||||||
|
raise ValueError(f'{payload_type} data must be bytes')
|
||||||
|
else:
|
||||||
|
raise ValueError(f'Unknown payload_type: {payload_type}')
|
||||||
|
|
||||||
|
|
||||||
|
def _deserialize_data(data, payload_type):
|
||||||
|
"""
|
||||||
|
Deserialize bytes to data based on type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Serialized data as bytes
|
||||||
|
payload_type: Data type ("text", "dictionary", "image", "audio", "video", "binary")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Deserialized data (String for "text", dict for "dictionary", bytes for others)
|
||||||
|
|
||||||
|
Note:
|
||||||
|
MicroPython does not support "table" type due to memory constraints.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If payload_type is not one of the supported types
|
||||||
|
"""
|
||||||
|
if payload_type == 'text':
|
||||||
|
return data.decode('utf-8')
|
||||||
|
elif payload_type == 'dictionary':
|
||||||
|
json_str = data.decode('utf-8')
|
||||||
|
return json.loads(json_str)
|
||||||
|
elif payload_type in ('image', 'audio', 'video', 'binary'):
|
||||||
|
return data
|
||||||
|
else:
|
||||||
|
raise ValueError(f'Unknown payload_type: {payload_type}')
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------- File Server Handlers ---------------------------------------------- #
|
||||||
|
|
||||||
|
def _sync_fileserver_upload(file_server_url, dataname, data):
|
||||||
|
"""
|
||||||
|
Synchronous file upload to HTTP server.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This is a simplified implementation for MicroPython.
|
||||||
|
In practice, would use network.HTTP or similar.
|
||||||
|
Currently raises NotImplementedError as file upload is not fully supported.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_server_url: Base URL of the file server
|
||||||
|
dataname: Name of the file being uploaded
|
||||||
|
data: Raw byte data of the file content
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with keys: 'status', 'url'
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError: File upload is not implemented in MicroPython
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("File upload not fully implemented in MicroPython. "
|
||||||
|
"Use direct transport only for memory-constrained devices.")
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_fileserver_download(url, max_retries, base_delay, max_delay, correlation_id):
|
||||||
|
"""
|
||||||
|
Synchronous file download with exponential backoff.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This is a simplified implementation for MicroPython.
|
||||||
|
In practice, would use network.HTTP or similar.
|
||||||
|
Currently raises NotImplementedError as file download is not fully supported.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: URL to download from
|
||||||
|
max_retries: Maximum retry attempts
|
||||||
|
base_delay: Initial delay in ms
|
||||||
|
max_delay: Maximum delay in ms
|
||||||
|
correlation_id: Correlation ID for logging
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Downloaded bytes
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError: File download is not implemented in MicroPython
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("File download not fully implemented in MicroPython. "
|
||||||
|
"Use direct transport only for memory-constrained devices.")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------- NATS Client ---------------------------------------------- #
|
||||||
|
|
||||||
|
class NATSClient:
|
||||||
|
"""
|
||||||
|
NATS client wrapper for MicroPython.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This is a simplified implementation for MicroPython.
|
||||||
|
Full NATS client implementation would require additional network stack support.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, url=DEFAULT_BROKER_URL):
|
||||||
|
"""
|
||||||
|
Initialize NATS client.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: NATS server URL
|
||||||
|
"""
|
||||||
|
self.url = url
|
||||||
|
self._connected = False
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
"""
|
||||||
|
Connect to NATS server.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This is a placeholder implementation.
|
||||||
|
Actual NATS client would require network stack support.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if connected, False otherwise
|
||||||
|
"""
|
||||||
|
# Placeholder - actual implementation would connect to NATS server
|
||||||
|
self._connected = True
|
||||||
|
return self._connected
|
||||||
|
|
||||||
|
def publish(self, subject, message):
|
||||||
|
"""
|
||||||
|
Publish message to NATS subject.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This is a placeholder implementation.
|
||||||
|
Actual NATS client would require network stack support.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
subject: NATS subject to publish to
|
||||||
|
message: Message to publish
|
||||||
|
"""
|
||||||
|
if not self._connected:
|
||||||
|
raise RuntimeError("Not connected to NATS server")
|
||||||
|
# Placeholder - actual implementation would publish to NATS
|
||||||
|
print(f"[NATS] Publish to {subject}: {message[:50]}...")
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Close the NATS connection."""
|
||||||
|
self._connected = False
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------- Core Functions ---------------------------------------------- #
|
||||||
|
|
||||||
|
def _build_envelope(subject, payloads, options):
|
||||||
|
"""
|
||||||
|
Build message envelope from payloads and metadata.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
subject: NATS subject
|
||||||
|
payloads: Array of payload objects
|
||||||
|
options: Envelope metadata options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Envelope dict
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'correlation_id': options['correlation_id'],
|
||||||
|
'msg_id': options['msg_id'],
|
||||||
|
'timestamp': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime()),
|
||||||
|
'send_to': subject,
|
||||||
|
'msg_purpose': options['msg_purpose'],
|
||||||
|
'sender_name': options['sender_name'],
|
||||||
|
'sender_id': options['sender_id'],
|
||||||
|
'receiver_name': options['receiver_name'],
|
||||||
|
'receiver_id': options['receiver_id'],
|
||||||
|
'reply_to': options['reply_to'],
|
||||||
|
'reply_to_msg_id': options['reply_to_msg_id'],
|
||||||
|
'broker_url': options['broker_url'],
|
||||||
|
'metadata': {},
|
||||||
|
'payloads': payloads
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_payload(dataname, payload_type, payload_bytes, transport, data):
|
||||||
|
"""
|
||||||
|
Build payload object from serialized data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dataname: Name of the payload
|
||||||
|
payload_type: Type of the payload
|
||||||
|
payload_bytes: Serialized payload bytes
|
||||||
|
transport: Transport type ("direct" or "link")
|
||||||
|
data: Data (base64 for direct, URL for link)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Payload dict
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'id': _generate_uuid(),
|
||||||
|
'dataname': dataname,
|
||||||
|
'payload_type': payload_type,
|
||||||
|
'transport': transport,
|
||||||
|
'encoding': 'base64' if transport == 'direct' else 'none',
|
||||||
|
'size': len(payload_bytes),
|
||||||
|
'data': data,
|
||||||
|
'metadata': {'payload_bytes': len(payload_bytes)} if transport == 'direct' else {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _publish(subject, message, correlation_id):
|
||||||
|
"""
|
||||||
|
Publish message to NATS.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This is a simplified implementation for MicroPython.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
subject: NATS subject to publish to
|
||||||
|
message: JSON message to publish
|
||||||
|
correlation_id: Correlation ID for logging
|
||||||
|
"""
|
||||||
|
log_trace(correlation_id, f"Publishing to {subject}")
|
||||||
|
# Placeholder - actual implementation would use NATSClient
|
||||||
|
# client = NATSClient()
|
||||||
|
# client.connect()
|
||||||
|
# client.publish(subject, message)
|
||||||
|
# client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def smartsend(subject, data, **kwargs):
|
||||||
|
"""
|
||||||
|
Send data via NATS with automatic transport selection.
|
||||||
|
|
||||||
|
This function intelligently routes data delivery based on payload size.
|
||||||
|
If the serialized payload is smaller than size_threshold, it encodes the data as Base64
|
||||||
|
and publishes directly over NATS. Otherwise, it uploads the data to a fileserver
|
||||||
|
and publishes only the download URL over NATS.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
MicroPython has memory constraints, so the default size_threshold is lower (100KB).
|
||||||
|
Table type is not supported due to memory constraints.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
subject: NATS subject to publish the message to
|
||||||
|
data: List of (dataname, data, type) tuples to send
|
||||||
|
- dataname: Name of the payload
|
||||||
|
- data: The actual data to send
|
||||||
|
- type: Payload type: "text", "dictionary", "image", "audio", "video", "binary"
|
||||||
|
broker_url: NATS server URL (default: DEFAULT_BROKER_URL)
|
||||||
|
fileserver_url: HTTP file server URL (default: DEFAULT_FILESERVER_URL)
|
||||||
|
fileserver_upload_handler: Function to handle fileserver uploads (default: _sync_fileserver_upload)
|
||||||
|
size_threshold: Threshold in bytes separating direct vs link transport (default: 100000)
|
||||||
|
correlation_id: Correlation ID for tracing (auto-generated if not provided)
|
||||||
|
msg_purpose: Purpose of the message (default: "chat")
|
||||||
|
sender_name: Name of the sender (default: "NATSBridge")
|
||||||
|
receiver_name: Name of the receiver (empty means broadcast)
|
||||||
|
receiver_id: UUID of the receiver (empty means broadcast)
|
||||||
|
reply_to: Topic to reply to (empty if no reply expected)
|
||||||
|
reply_to_msg_id: Message ID this message is replying to
|
||||||
|
is_publish: Whether to automatically publish the message (default: True)
|
||||||
|
msg_id: Message ID (auto-generated if not provided)
|
||||||
|
sender_id: Sender ID (auto-generated if not provided)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (env, env_json_str) where:
|
||||||
|
- env: Dict containing all metadata and payloads
|
||||||
|
- env_json_str: JSON string for publishing to NATS
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> # Send text payload
|
||||||
|
>>> env, env_json_str = NATSBridge.smartsend(
|
||||||
|
... "/chat",
|
||||||
|
... [("message", "Hello!", "text")],
|
||||||
|
... broker_url="nats://localhost:4222"
|
||||||
|
... )
|
||||||
|
>>>
|
||||||
|
>>> # Send dictionary payload
|
||||||
|
>>> env, env_json_str = NATSBridge.smartsend(
|
||||||
|
... "/config",
|
||||||
|
... [("config", {"key": "value"}, "dictionary")],
|
||||||
|
... broker_url="nats://localhost:4222"
|
||||||
|
... )
|
||||||
|
>>>
|
||||||
|
>>> # Send binary payload (image, audio, video)
|
||||||
|
>>> env, env_json_str = NATSBridge.smartsend(
|
||||||
|
... "/media",
|
||||||
|
... [("image", image_bytes, "image")],
|
||||||
|
... broker_url="nats://localhost:4222"
|
||||||
|
... )
|
||||||
|
"""
|
||||||
|
# Extract options with defaults
|
||||||
|
correlation_id = kwargs.get('correlation_id', _generate_uuid())
|
||||||
|
msg_id = kwargs.get('msg_id', _generate_uuid())
|
||||||
|
sender_id = kwargs.get('sender_id', _generate_uuid())
|
||||||
|
broker_url = kwargs.get('broker_url', DEFAULT_BROKER_URL)
|
||||||
|
fileserver_url = kwargs.get('fileserver_url', DEFAULT_FILESERVER_URL)
|
||||||
|
size_threshold = kwargs.get('size_threshold', DEFAULT_SIZE_THRESHOLD)
|
||||||
|
msg_purpose = kwargs.get('msg_purpose', 'chat')
|
||||||
|
sender_name = kwargs.get('sender_name', 'NATSBridge')
|
||||||
|
receiver_name = kwargs.get('receiver_name', '')
|
||||||
|
receiver_id = kwargs.get('receiver_id', '')
|
||||||
|
reply_to = kwargs.get('reply_to', '')
|
||||||
|
reply_to_msg_id = kwargs.get('reply_to_msg_id', '')
|
||||||
|
is_publish = kwargs.get('is_publish', True)
|
||||||
|
fileserver_upload_handler = kwargs.get('fileserver_upload_handler', _sync_fileserver_upload)
|
||||||
|
|
||||||
|
log_trace(correlation_id, f"Starting smartsend for subject: {subject}")
|
||||||
|
|
||||||
|
# Process payloads
|
||||||
|
payloads = []
|
||||||
|
for dataname, payload_data, payload_type in data:
|
||||||
|
payload_bytes = _serialize_data(payload_data, payload_type)
|
||||||
|
payload_size = len(payload_bytes)
|
||||||
|
|
||||||
|
# Check against hard limit for MicroPython
|
||||||
|
if payload_size > MAX_PAYLOAD_SIZE:
|
||||||
|
raise MemoryError(f"Payload '{dataname}' exceeds max size {MAX_PAYLOAD_SIZE} bytes")
|
||||||
|
|
||||||
|
log_trace(correlation_id, f"Serialized payload '{dataname}' (type: {payload_type}) size: {payload_size} bytes")
|
||||||
|
|
||||||
|
if payload_size < size_threshold:
|
||||||
|
# Direct path
|
||||||
|
payload_b64 = base64.b64encode(payload_bytes).decode('ascii')
|
||||||
|
log_trace(correlation_id, f"Using direct transport for {payload_size} bytes")
|
||||||
|
|
||||||
|
payload = _build_payload(dataname, payload_type, payload_bytes, 'direct', payload_b64)
|
||||||
|
payloads.append(payload)
|
||||||
|
else:
|
||||||
|
# Link path (limited support)
|
||||||
|
log_trace(correlation_id, "Using link transport, uploading to fileserver")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = fileserver_upload_handler(fileserver_url, dataname, payload_bytes)
|
||||||
|
log_trace(correlation_id, f"Uploaded to URL: {response['url']}")
|
||||||
|
|
||||||
|
payload = _build_payload(dataname, payload_type, payload_bytes, 'link', response['url'])
|
||||||
|
payloads.append(payload)
|
||||||
|
except NotImplementedError:
|
||||||
|
# Fall back to direct transport if file upload not available
|
||||||
|
log_trace(correlation_id, "File upload not available, using direct transport")
|
||||||
|
payload_b64 = base64.b64encode(payload_bytes).decode('ascii')
|
||||||
|
payload = _build_payload(dataname, payload_type, payload_bytes, 'direct', payload_b64)
|
||||||
|
payloads.append(payload)
|
||||||
|
|
||||||
|
# Build envelope
|
||||||
|
env = _build_envelope(subject, payloads, {
|
||||||
|
'correlation_id': correlation_id,
|
||||||
|
'msg_id': msg_id,
|
||||||
|
'msg_purpose': msg_purpose,
|
||||||
|
'sender_name': sender_name,
|
||||||
|
'sender_id': sender_id,
|
||||||
|
'receiver_name': receiver_name,
|
||||||
|
'receiver_id': receiver_id,
|
||||||
|
'reply_to': reply_to,
|
||||||
|
'reply_to_msg_id': reply_to_msg_id,
|
||||||
|
'broker_url': broker_url
|
||||||
|
})
|
||||||
|
|
||||||
|
env_json_str = json.dumps(env)
|
||||||
|
|
||||||
|
if is_publish:
|
||||||
|
_publish(subject, env_json_str, correlation_id)
|
||||||
|
|
||||||
|
return env, env_json_str
|
||||||
|
|
||||||
|
|
||||||
|
def smartreceive(msg, **kwargs):
|
||||||
|
"""
|
||||||
|
Receive and process NATS message.
|
||||||
|
|
||||||
|
This function processes incoming NATS messages, handling both direct transport
|
||||||
|
(base64 decoded payloads) and link transport (URL-based payloads).
|
||||||
|
It deserializes the data based on the transport type and returns the result.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
MicroPython has memory constraints, so large payloads should be avoided.
|
||||||
|
Table type is not supported due to memory constraints.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
msg: NATS message to process (can be string, dict, or object with 'payload' attribute)
|
||||||
|
fileserver_download_handler: Function to handle downloading data from file server URLs
|
||||||
|
max_retries: Maximum retry attempts (default: 3)
|
||||||
|
base_delay: Initial delay in ms (default: 100)
|
||||||
|
max_delay: Maximum delay in ms (default: 1000)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with envelope metadata and payloads field containing List[Tuple[str, Any, str]]
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> # Receive and process message
|
||||||
|
>>> env = NATSBridge.smartreceive(msg, fileserver_download_handler=_sync_fileserver_download)
|
||||||
|
>>> # env is a Dict with "payloads" key containing List[Tuple[str, Any, str]]
|
||||||
|
>>> for dataname, data, type_ in env["payloads"]:
|
||||||
|
... print(f"{dataname}: {data} (type: {type_})")
|
||||||
|
"""
|
||||||
|
# Parse the JSON envelope
|
||||||
|
if isinstance(msg, dict):
|
||||||
|
# Already parsed
|
||||||
|
env_json_obj = msg
|
||||||
|
elif hasattr(msg, 'payload'):
|
||||||
|
# Object with payload attribute
|
||||||
|
payload = msg.payload if isinstance(msg.payload, str) else msg.payload.decode('utf-8')
|
||||||
|
env_json_obj = json.loads(payload)
|
||||||
|
else:
|
||||||
|
# Assume it's already a JSON string or dict
|
||||||
|
env_json_obj = json.loads(msg) if isinstance(msg, str) else msg
|
||||||
|
|
||||||
|
correlation_id = env_json_obj['correlation_id']
|
||||||
|
log_trace(correlation_id, "Processing received message")
|
||||||
|
|
||||||
|
# Process all payloads in the envelope
|
||||||
|
payloads_list = []
|
||||||
|
num_payloads = len(env_json_obj['payloads'])
|
||||||
|
|
||||||
|
for i in range(num_payloads):
|
||||||
|
payload_obj = env_json_obj['payloads'][i]
|
||||||
|
transport = payload_obj['transport']
|
||||||
|
dataname = payload_obj['dataname']
|
||||||
|
|
||||||
|
if transport == 'direct':
|
||||||
|
log_trace(correlation_id, f"Direct transport - decoding payload '{dataname}'")
|
||||||
|
|
||||||
|
# Extract base64 payload from the payload
|
||||||
|
payload_b64 = payload_obj['data']
|
||||||
|
|
||||||
|
# Decode Base64 payload
|
||||||
|
payload_bytes = base64.b64decode(payload_b64)
|
||||||
|
|
||||||
|
# Deserialize based on type
|
||||||
|
data_type = payload_obj['payload_type']
|
||||||
|
data = _deserialize_data(payload_bytes, data_type)
|
||||||
|
|
||||||
|
payloads_list.append((dataname, data, data_type))
|
||||||
|
elif transport == 'link':
|
||||||
|
# Extract download URL from the payload
|
||||||
|
url = payload_obj['data']
|
||||||
|
log_trace(correlation_id, f"Link transport - fetching '{dataname}' from URL: {url}")
|
||||||
|
|
||||||
|
# Fetch with exponential backoff using the download handler
|
||||||
|
fileserver_download_handler = kwargs.get('fileserver_download_handler', _sync_fileserver_download)
|
||||||
|
max_retries = kwargs.get('max_retries', 3)
|
||||||
|
base_delay = kwargs.get('base_delay', 100)
|
||||||
|
max_delay = kwargs.get('max_delay', 1000)
|
||||||
|
|
||||||
|
downloaded_data = fileserver_download_handler(
|
||||||
|
url,
|
||||||
|
max_retries,
|
||||||
|
base_delay,
|
||||||
|
max_delay,
|
||||||
|
correlation_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Deserialize based on type
|
||||||
|
data_type = payload_obj['payload_type']
|
||||||
|
data = _deserialize_data(downloaded_data, data_type)
|
||||||
|
|
||||||
|
payloads_list.append((dataname, data, data_type))
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown transport type for payload '{dataname}': {transport}")
|
||||||
|
|
||||||
|
env_json_obj['payloads'] = payloads_list
|
||||||
|
return env_json_obj
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------- Module Exports ---------------------------------------------- #
|
||||||
|
|
||||||
|
class NATSBridge:
|
||||||
|
"""
|
||||||
|
MicroPython NATS bridge implementation.
|
||||||
|
|
||||||
|
This class provides a convenient interface for NATSBridge functionality,
|
||||||
|
encapsulating the main functions and providing a class-based API.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
MicroPython has significant constraints:
|
||||||
|
- No Arrow IPC support (memory constraints)
|
||||||
|
- Only direct transport (< 100KB threshold enforced)
|
||||||
|
- Simplified UUID generation
|
||||||
|
- No async/await (synchronous API)
|
||||||
|
"""
|
||||||
|
|
||||||
|
DEFAULT_SIZE_THRESHOLD = DEFAULT_SIZE_THRESHOLD
|
||||||
|
DEFAULT_BROKER_URL = DEFAULT_BROKER_URL
|
||||||
|
DEFAULT_FILESERVER_URL = DEFAULT_FILESERVER_URL
|
||||||
|
MAX_PAYLOAD_SIZE = MAX_PAYLOAD_SIZE
|
||||||
|
|
||||||
|
def __init__(self, broker_url=None, fileserver_url=None):
|
||||||
|
"""
|
||||||
|
Initialize NATSBridge.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
broker_url: NATS server URL (defaults to DEFAULT_BROKER_URL)
|
||||||
|
fileserver_url: HTTP file server URL (defaults to DEFAULT_FILESERVER_URL)
|
||||||
|
"""
|
||||||
|
self.broker_url = broker_url or self.DEFAULT_BROKER_URL
|
||||||
|
self.fileserver_url = fileserver_url or self.DEFAULT_FILESERVER_URL
|
||||||
|
|
||||||
|
def smartsend(self, subject, data, **kwargs):
|
||||||
|
"""
|
||||||
|
Send data via NATS.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
subject: NATS subject to publish to
|
||||||
|
data: List of (dataname, data, type) tuples
|
||||||
|
**kwargs: Additional options passed to smartsend
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (env, env_json_str)
|
||||||
|
"""
|
||||||
|
kwargs['broker_url'] = kwargs.get('broker_url', self.broker_url)
|
||||||
|
kwargs['fileserver_url'] = kwargs.get('fileserver_url', self.fileserver_url)
|
||||||
|
return smartsend(subject, data, **kwargs)
|
||||||
|
|
||||||
|
def smartreceive(self, msg, **kwargs):
|
||||||
|
"""
|
||||||
|
Receive and process NATS message.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
msg: NATS message to process
|
||||||
|
**kwargs: Additional options passed to smartreceive
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with envelope metadata and payloads
|
||||||
|
"""
|
||||||
|
return smartreceive(msg, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
# Convenience functions for module-level usage
|
||||||
|
def send(subject, data, **kwargs):
|
||||||
|
"""
|
||||||
|
Convenience function for sending data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
subject: NATS subject to publish to
|
||||||
|
data: List of (dataname, data, type) tuples
|
||||||
|
**kwargs: Additional options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (env, env_json_str)
|
||||||
|
"""
|
||||||
|
return smartsend(subject, data, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def receive(msg, **kwargs):
|
||||||
|
"""
|
||||||
|
Convenience function for receiving messages.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
msg: NATS message to process
|
||||||
|
**kwargs: Additional options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with envelope metadata and payloads
|
||||||
|
"""
|
||||||
|
return smartreceive(msg, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'smartsend',
|
||||||
|
'smartreceive',
|
||||||
|
'NATSBridge',
|
||||||
|
'send',
|
||||||
|
'receive',
|
||||||
|
'DEFAULT_SIZE_THRESHOLD',
|
||||||
|
'DEFAULT_BROKER_URL',
|
||||||
|
'DEFAULT_FILESERVER_URL',
|
||||||
|
'MAX_PAYLOAD_SIZE',
|
||||||
|
'NATSClient',
|
||||||
|
'_serialize_data',
|
||||||
|
'_deserialize_data',
|
||||||
|
'log_trace',
|
||||||
|
'_sync_fileserver_upload',
|
||||||
|
'_sync_fileserver_download'
|
||||||
|
]
|
||||||
798
src/natsbridge_ssr.js
Normal file
798
src/natsbridge_ssr.js
Normal file
@@ -0,0 +1,798 @@
|
|||||||
|
/**
|
||||||
|
* NATSBridge - Cross-Platform Bi-Directional Data Bridge
|
||||||
|
* JavaScript/Node.js Implementation (Client-Side Rendering)
|
||||||
|
*
|
||||||
|
* This module provides functionality for sending and receiving data across network boundaries
|
||||||
|
* using NATS as the message bus, with support for both direct payload transport and
|
||||||
|
* URL-based transport for larger payloads.
|
||||||
|
*
|
||||||
|
* Supported payload types: "text", "dictionary", "arrowtable", "jsontable", "image", "audio", "video", "binary"
|
||||||
|
*
|
||||||
|
* @module NATSBridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
const nats = require('nats');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
// Use native fetch available in Node.js 18+
|
||||||
|
const arrow = require('apache-arrow');
|
||||||
|
|
||||||
|
// ---------------------------------------------- UUID Helper ---------------------------------------------- //
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate UUID v4 using crypto module (Node.js compatible)
|
||||||
|
* @returns {string} UUID string
|
||||||
|
*/
|
||||||
|
function uuidv4() {
|
||||||
|
return crypto.randomUUID();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------- Constants ---------------------------------------------- //
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default size threshold for switching from direct to link transport (0.5MB)
|
||||||
|
*/
|
||||||
|
const DEFAULT_SIZE_THRESHOLD = 500_000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default NATS server URL
|
||||||
|
*/
|
||||||
|
const DEFAULT_BROKER_URL = 'nats://localhost:4222';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default HTTP file server URL for link transport
|
||||||
|
*/
|
||||||
|
const DEFAULT_FILESERVER_URL = 'http://localhost:8080';
|
||||||
|
|
||||||
|
// ---------------------------------------------- Utility Functions ---------------------------------------------- //
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert Buffer to Base64 string
|
||||||
|
* @param {Buffer} buffer - Buffer to encode
|
||||||
|
* @returns {string} Base64 encoded string
|
||||||
|
*/
|
||||||
|
function bufferToBase64(buffer) {
|
||||||
|
return buffer.toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a trace message with correlation ID and timestamp
|
||||||
|
* @param {string} correlationId - Correlation ID for tracing
|
||||||
|
* @param {string} message - Message content to log
|
||||||
|
*/
|
||||||
|
function logTrace(correlationId, message) {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
console.log(`[${timestamp}] [Correlation: ${correlationId}] ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------- Serialization Functions ---------------------------------------------- //
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize data according to specified format
|
||||||
|
* @param {any} data - Data to serialize
|
||||||
|
* @param {string} payloadType - Target format: "text", "dictionary", "arrowtable", "jsontable", "image", "audio", "video", "binary"
|
||||||
|
* @returns {Buffer} Binary representation of the serialized data
|
||||||
|
*/
|
||||||
|
async function serializeData(data, payloadType) {
|
||||||
|
if (payloadType === 'text') {
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
return Buffer.from(data, 'utf8');
|
||||||
|
} else {
|
||||||
|
throw new Error('Text data must be a string');
|
||||||
|
}
|
||||||
|
} else if (payloadType === 'dictionary') {
|
||||||
|
const jsonStr = JSON.stringify(data);
|
||||||
|
return Buffer.from(jsonStr, 'utf8');
|
||||||
|
} else if (payloadType === 'arrowtable') {
|
||||||
|
// Convert array of objects to Arrow IPC format
|
||||||
|
if (!Array.isArray(data) || data.length === 0) {
|
||||||
|
throw new Error('Arrow table data must be a non-empty array of objects');
|
||||||
|
}
|
||||||
|
|
||||||
|
return serializeArrowTable(data);
|
||||||
|
} else if (payloadType === 'jsontable') {
|
||||||
|
// Serialize array of objects to JSON format
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
throw new Error('JSON table data must be an array');
|
||||||
|
}
|
||||||
|
const jsonStr = JSON.stringify(data);
|
||||||
|
return Buffer.from(jsonStr, 'utf8');
|
||||||
|
} else if (payloadType === 'image') {
|
||||||
|
if (data instanceof Uint8Array || Buffer.isBuffer(data)) {
|
||||||
|
return Buffer.from(data);
|
||||||
|
} else {
|
||||||
|
throw new Error('Image data must be Uint8Array or Buffer');
|
||||||
|
}
|
||||||
|
} else if (payloadType === 'audio') {
|
||||||
|
if (data instanceof Uint8Array || Buffer.isBuffer(data)) {
|
||||||
|
return Buffer.from(data);
|
||||||
|
} else {
|
||||||
|
throw new Error('Audio data must be Uint8Array or Buffer');
|
||||||
|
}
|
||||||
|
} else if (payloadType === 'video') {
|
||||||
|
if (data instanceof Uint8Array || Buffer.isBuffer(data)) {
|
||||||
|
return Buffer.from(data);
|
||||||
|
} else {
|
||||||
|
throw new Error('Video data must be Uint8Array or Buffer');
|
||||||
|
}
|
||||||
|
} else if (payloadType === 'binary') {
|
||||||
|
if (data instanceof Uint8Array || Buffer.isBuffer(data)) {
|
||||||
|
return Buffer.from(data);
|
||||||
|
} else {
|
||||||
|
throw new Error('Binary data must be Uint8Array or Buffer');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown payload_type: ${payloadType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to properly serialize table data to Arrow IPC
|
||||||
|
* @param {Array<Object>} data - Array of objects representing table rows
|
||||||
|
* @returns {Buffer} Arrow IPC formatted buffer
|
||||||
|
*/
|
||||||
|
function serializeArrowTable(data) {
|
||||||
|
if (!Array.isArray(data) || data.length === 0) {
|
||||||
|
throw new Error('Table data must be a non-empty array of objects');
|
||||||
|
}
|
||||||
|
|
||||||
|
logTrace('serializeArrowTable', `Serializing table with ${data.length} rows`);
|
||||||
|
|
||||||
|
// Use arrow.tableFromArrays which handles the conversion properly
|
||||||
|
// Convert array of objects to a key-value format expected by tableFromArrays
|
||||||
|
const columns = {};
|
||||||
|
for (const key of Object.keys(data[0])) {
|
||||||
|
columns[key] = data.map(row => row[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
logTrace('serializeArrowTable', `Columns: ${Object.keys(columns).join(', ')}`);
|
||||||
|
|
||||||
|
const table = arrow.tableFromArrays(columns);
|
||||||
|
|
||||||
|
logTrace('serializeArrowTable', `Arrow table created with ${table.numRows} rows, ${table.numCols} cols`);
|
||||||
|
|
||||||
|
// Convert to IPC format
|
||||||
|
const ipcBuffer = arrow.tableToIPC(table);
|
||||||
|
|
||||||
|
logTrace('serializeArrowTable', `IPC buffer type: ${typeof ipcBuffer}, length: ${ipcBuffer.byteLength}`);
|
||||||
|
|
||||||
|
const resultBuffer = Buffer.from(ipcBuffer);
|
||||||
|
logTrace('serializeArrowTable', `Result buffer: ${resultBuffer.length} bytes`);
|
||||||
|
|
||||||
|
// Debug: Show first 20 bytes in hex
|
||||||
|
const hexPreview = resultBuffer.slice(0, 20).toString('hex');
|
||||||
|
logTrace('serializeArrowTable', `First 20 bytes (hex): ${hexPreview}`);
|
||||||
|
|
||||||
|
return resultBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserialize bytes to data based on type
|
||||||
|
* @param {Buffer|Uint8Array} data - Serialized data as bytes
|
||||||
|
* @param {string} payloadType - Data type
|
||||||
|
* @param {string} correlationId - Correlation ID for logging
|
||||||
|
* @returns {any} Deserialized data
|
||||||
|
*/
|
||||||
|
async function deserializeData(data, payloadType, correlationId) {
|
||||||
|
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
||||||
|
|
||||||
|
logTrace(correlationId, `deserializeData: type=${payloadType}, bufferLength=${buffer.length}`);
|
||||||
|
|
||||||
|
// Debug: Show first 20 bytes in hex for binary data
|
||||||
|
if (payloadType === 'arrowtable' || payloadType === 'jsontable' || payloadType === 'image' || payloadType === 'binary') {
|
||||||
|
const hexPreview = buffer.slice(0, 20).toString('hex');
|
||||||
|
logTrace(correlationId, `deserializeData: First 20 bytes (hex): ${hexPreview}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payloadType === 'text') {
|
||||||
|
const result = buffer.toString('utf8');
|
||||||
|
logTrace(correlationId, `deserializeData: text result length=${result.length}`);
|
||||||
|
return result;
|
||||||
|
} else if (payloadType === 'dictionary') {
|
||||||
|
const jsonStr = buffer.toString('utf8');
|
||||||
|
const result = JSON.parse(jsonStr);
|
||||||
|
logTrace(correlationId, `deserializeData: dictionary keys=${Object.keys(result).join(', ')}`);
|
||||||
|
return result;
|
||||||
|
} else if (payloadType === 'arrowtable') {
|
||||||
|
logTrace(correlationId, `deserializeData: Attempting Arrow table deserialization`);
|
||||||
|
|
||||||
|
// Debug: Check available arrow methods
|
||||||
|
logTrace(correlationId, `deserializeData: arrow.tableFromRawBytes exists: ${typeof arrow.tableFromRawBytes}`);
|
||||||
|
logTrace(correlationId, `deserializeData: arrow.tableFromIPC exists: ${typeof arrow.tableFromIPC}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try tableFromRawBytes first (older API)
|
||||||
|
if (typeof arrow.tableFromRawBytes === 'function') {
|
||||||
|
logTrace(correlationId, `deserializeData: Using tableFromRawBytes`);
|
||||||
|
const table = arrow.tableFromRawBytes(buffer);
|
||||||
|
logTrace(correlationId, `deserializeData: Arrow table - rows=${table.numRows}, cols=${table.numCols}`);
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logTrace(correlationId, `deserializeData: tableFromRawBytes failed: ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try tableFromIPC (newer API)
|
||||||
|
if (typeof arrow.tableFromIPC === 'function') {
|
||||||
|
logTrace(correlationId, `deserializeData: Using tableFromIPC`);
|
||||||
|
const table = arrow.tableFromIPC(buffer);
|
||||||
|
logTrace(correlationId, `deserializeData: Arrow table from IPC - rows=${table.numRows}, cols=${table.numCols}`);
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logTrace(correlationId, `deserializeData: tableFromIPC failed: ${e.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unable to deserialize Arrow table: neither tableFromRawBytes nor tableFromIPC worked`);
|
||||||
|
} else if (payloadType === 'jsontable') {
|
||||||
|
const jsonStr = buffer.toString('utf8');
|
||||||
|
const result = JSON.parse(jsonStr);
|
||||||
|
logTrace(correlationId, `deserializeData: jsontable result length=${Array.isArray(result) ? result.length : 'N/A'}`);
|
||||||
|
return result;
|
||||||
|
} else if (payloadType === 'image') {
|
||||||
|
logTrace(correlationId, `deserializeData: image buffer length=${buffer.length}`);
|
||||||
|
return buffer;
|
||||||
|
} else if (payloadType === 'audio') {
|
||||||
|
logTrace(correlationId, `deserializeData: audio buffer length=${buffer.length}`);
|
||||||
|
return buffer;
|
||||||
|
} else if (payloadType === 'video') {
|
||||||
|
logTrace(correlationId, `deserializeData: video buffer length=${buffer.length}`);
|
||||||
|
return buffer;
|
||||||
|
} else if (payloadType === 'binary') {
|
||||||
|
logTrace(correlationId, `deserializeData: binary buffer length=${buffer.length}`);
|
||||||
|
return buffer;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown payload_type: ${payloadType}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------- File Server Handlers ---------------------------------------------- //
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload data to plik server in one-shot mode
|
||||||
|
* @param {string} fileServerUrl - Base URL of the plik server
|
||||||
|
* @param {string} dataname - Name of the file being uploaded
|
||||||
|
* @param {Buffer|Uint8Array} data - Raw byte data of the file content
|
||||||
|
* @returns {Promise<{status: number, uploadid: string, fileid: string, url: string}>}
|
||||||
|
*/
|
||||||
|
async function plikOneshotUpload(fileServerUrl, dataname, data) {
|
||||||
|
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
||||||
|
|
||||||
|
// Get upload id
|
||||||
|
const urlGetUploadID = `${fileServerUrl}/upload`;
|
||||||
|
const headers = { 'Content-Type': 'application/json' };
|
||||||
|
const body = JSON.stringify({ OneShot: true });
|
||||||
|
|
||||||
|
const httpResponse = await fetch(urlGetUploadID, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseJson = await httpResponse.json();
|
||||||
|
const uploadid = responseJson.id;
|
||||||
|
const uploadtoken = responseJson.uploadToken;
|
||||||
|
|
||||||
|
// Upload file
|
||||||
|
const urlUpload = `${fileServerUrl}/file/${uploadid}`;
|
||||||
|
const form = new FormData();
|
||||||
|
const blob = new Blob([buffer], { type: 'application/octet-stream' });
|
||||||
|
form.append('file', blob, dataname);
|
||||||
|
|
||||||
|
const uploadHeaders = {
|
||||||
|
'X-UploadToken': uploadtoken
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadResponse = await fetch(urlUpload, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: uploadHeaders,
|
||||||
|
body: form
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploadJson = await uploadResponse.json();
|
||||||
|
const fileid = uploadJson.id;
|
||||||
|
|
||||||
|
const url = `${fileServerUrl}/file/${uploadid}/${fileid}/${dataname}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: uploadResponse.status,
|
||||||
|
uploadid,
|
||||||
|
fileid,
|
||||||
|
url
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch data from URL with exponential backoff
|
||||||
|
* @param {string} url - URL to fetch from
|
||||||
|
* @param {number} maxRetries - Maximum number of retry attempts
|
||||||
|
* @param {number} baseDelay - Initial delay in milliseconds
|
||||||
|
* @param {number} maxDelay - Maximum delay in milliseconds
|
||||||
|
* @param {string} correlationId - Correlation ID for logging
|
||||||
|
* @returns {Promise<Uint8Array>} Fetched data as bytes
|
||||||
|
*/
|
||||||
|
async function fetchWithBackoff(url, maxRetries, baseDelay, maxDelay, correlationId) {
|
||||||
|
let delay = baseDelay;
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
logTrace(correlationId, `Successfully fetched data from ${url} on attempt ${attempt}`);
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
return new Uint8Array(arrayBuffer);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Failed to fetch: ${response.status}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logTrace(correlationId, `Attempt ${attempt} failed: ${e.constructor.name} - ${e.message}`);
|
||||||
|
|
||||||
|
if (attempt < maxRetries) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
delay = Math.min(delay * 2, maxDelay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Failed to fetch data after ${maxRetries} attempts`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------- NATS Client ---------------------------------------------- //
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NATS client wrapper for connection management
|
||||||
|
*/
|
||||||
|
class NATSClient {
|
||||||
|
/**
|
||||||
|
* Create a new NATS client
|
||||||
|
* @param {string} url - NATS server URL
|
||||||
|
*/
|
||||||
|
constructor(url) {
|
||||||
|
this.url = url;
|
||||||
|
this.connection = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to NATS server
|
||||||
|
* @returns {Promise<NATS.Connection>}
|
||||||
|
*/
|
||||||
|
async connect() {
|
||||||
|
this.connection = await nats.connect({ servers: this.url });
|
||||||
|
return this.connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publish message to NATS subject
|
||||||
|
* @param {string} subject - NATS subject to publish to
|
||||||
|
* @param {string} message - Message to publish
|
||||||
|
* @param {string} correlationId - Correlation ID for logging
|
||||||
|
*/
|
||||||
|
async publish(subject, message, correlationId) {
|
||||||
|
if (!this.connection) {
|
||||||
|
await this.connect();
|
||||||
|
}
|
||||||
|
await this.connection.publish(subject, message);
|
||||||
|
logTrace(correlationId, `Message published to ${subject}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the NATS connection
|
||||||
|
*/
|
||||||
|
async close() {
|
||||||
|
if (this.connection) {
|
||||||
|
this.connection.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------- Core Functions ---------------------------------------------- //
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publish message to NATS
|
||||||
|
* @param {string|NATSClient|NATS.Connection} brokerUrlOrClient - NATS URL, client, or connection
|
||||||
|
* @param {string} subject - NATS subject to publish to
|
||||||
|
* @param {string} message - JSON message to publish
|
||||||
|
* @param {string} correlationId - Correlation ID for tracing
|
||||||
|
*/
|
||||||
|
async function publishMessage(brokerUrlOrClient, subject, message, correlationId) {
|
||||||
|
let conn;
|
||||||
|
|
||||||
|
if (brokerUrlOrClient instanceof NATSClient) {
|
||||||
|
conn = brokerUrlOrClient;
|
||||||
|
} else if (brokerUrlOrClient && typeof brokerUrlOrClient.publish === 'function') {
|
||||||
|
// Create a wrapper for direct connection (duck-typing check for NATS connection)
|
||||||
|
conn = {
|
||||||
|
async publish(subj, msg) {
|
||||||
|
await brokerUrlOrClient.publish(subj, msg);
|
||||||
|
},
|
||||||
|
async close() {
|
||||||
|
await brokerUrlOrClient.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// String URL - create new client
|
||||||
|
const client = new NATSClient(brokerUrlOrClient);
|
||||||
|
conn = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
await conn.publish(subject, message, correlationId);
|
||||||
|
|
||||||
|
if (conn instanceof NATSClient) {
|
||||||
|
await conn.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build message envelope from payloads and metadata
|
||||||
|
* @param {string} subject - NATS subject
|
||||||
|
* @param {Array} payloads - Array of payload objects
|
||||||
|
* @param {Object} options - Envelope metadata options
|
||||||
|
* @returns {Object} Envelope object
|
||||||
|
*/
|
||||||
|
function buildEnvelope(subject, payloads, options) {
|
||||||
|
return {
|
||||||
|
correlation_id: options.correlation_id,
|
||||||
|
msg_id: options.msg_id,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
send_to: subject,
|
||||||
|
msg_purpose: options.msg_purpose,
|
||||||
|
sender_name: options.sender_name,
|
||||||
|
sender_id: options.sender_id,
|
||||||
|
receiver_name: options.receiver_name,
|
||||||
|
receiver_id: options.receiver_id,
|
||||||
|
reply_to: options.reply_to,
|
||||||
|
reply_to_msg_id: options.reply_to_msg_id,
|
||||||
|
broker_url: options.broker_url,
|
||||||
|
metadata: options.metadata || {},
|
||||||
|
payloads: payloads
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build payload object from serialized data
|
||||||
|
* @param {string} dataname - Name of the payload
|
||||||
|
* @param {string} payloadType - Type of the payload
|
||||||
|
* @param {Buffer} payloadBytes - Serialized payload bytes
|
||||||
|
* @param {string} transport - Transport type ("direct" or "link")
|
||||||
|
* @param {string} data - Data (base64 for direct, URL for link)
|
||||||
|
* @returns {Object} Payload object
|
||||||
|
*/
|
||||||
|
function buildPayload(dataname, payloadType, payloadBytes, transport, data) {
|
||||||
|
// Determine encoding based on payload type (matching Julia implementation)
|
||||||
|
let encoding = 'base64';
|
||||||
|
if (payloadType === 'jsontable') {
|
||||||
|
encoding = 'json';
|
||||||
|
} else if (payloadType === 'arrowtable') {
|
||||||
|
encoding = 'arrow-ipc';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: uuidv4(),
|
||||||
|
dataname,
|
||||||
|
payload_type: payloadType,
|
||||||
|
transport,
|
||||||
|
encoding,
|
||||||
|
size: payloadBytes.byteLength,
|
||||||
|
data,
|
||||||
|
metadata: transport === 'direct' ? { payload_bytes: payloadBytes.byteLength } : {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send data via NATS with automatic transport selection
|
||||||
|
*
|
||||||
|
* This function intelligently routes data delivery based on payload size.
|
||||||
|
* If the serialized payload is smaller than size_threshold, it encodes the data as Base64
|
||||||
|
* and publishes directly over NATS. Otherwise, it uploads the data to a fileserver
|
||||||
|
* and publishes only the download URL over NATS.
|
||||||
|
*
|
||||||
|
* @param {string} subject - NATS subject to publish the message to
|
||||||
|
* @param {Array} data - List of [dataname, data, type] tuples to send
|
||||||
|
* - type: "text", "dictionary", "arrowtable", "jsontable", "image", "audio", "video", "binary"
|
||||||
|
* @param {Object} options - Optional configuration
|
||||||
|
* @param {string} [options.broker_url=DEFAULT_BROKER_URL] - URL of the NATS server
|
||||||
|
* @param {string} [options.fileserver_url=DEFAULT_FILESERVER_URL] - URL of the HTTP file server
|
||||||
|
* @param {Function} [options.fileserver_upload_handler=plikOneshotUpload] - Function to handle fileserver uploads
|
||||||
|
* @param {number} [options.size_threshold=DEFAULT_SIZE_THRESHOLD] - Threshold separating direct vs link transport
|
||||||
|
* @param {string} [options.correlation_id=crypto.randomUUID()] - Correlation ID for tracing
|
||||||
|
* @param {string} [options.msg_purpose="chat"] - Purpose of the message
|
||||||
|
* @param {string} [options.sender_name="NATSBridge"] - Name of the sender
|
||||||
|
* @param {string} [options.receiver_name=""] - Name of the receiver (empty means broadcast)
|
||||||
|
* @param {string} [options.receiver_id=""] - UUID of the receiver (empty means broadcast)
|
||||||
|
* @param {string} [options.reply_to=""] - Topic to reply to
|
||||||
|
* @param {string} [options.reply_to_msg_id=""] - Message ID this message is replying to
|
||||||
|
* @param {boolean} [options.is_publish=true] - Whether to automatically publish the message
|
||||||
|
* @param {NATSClient|NATS.Connection} [options.nats_connection=null] - Pre-existing NATS connection
|
||||||
|
* @param {string} [options.msg_id=crypto.randomUUID()] - Message ID
|
||||||
|
* @param {string} [options.sender_id=crypto.randomUUID()] - Sender ID
|
||||||
|
* @returns {Promise<[Object, string]>} Tuple of [env, env_json_str]
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Send a single payload
|
||||||
|
* const [env, envJsonStr] = await smartsend(
|
||||||
|
* "/test",
|
||||||
|
* [["dataname1", data1, "dictionary"]],
|
||||||
|
* { broker_url: "nats://localhost:4222" }
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* // Send multiple payloads
|
||||||
|
* const [env, envJsonStr] = await smartsend(
|
||||||
|
* "/test",
|
||||||
|
* [
|
||||||
|
* ["dataname1", data1, "dictionary"],
|
||||||
|
* ["dataname2", data2, "arrowtable"]
|
||||||
|
* ],
|
||||||
|
* { broker_url: "nats://localhost:4222" }
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* // Send with pre-existing connection
|
||||||
|
* const client = await NATSBridge.NATSClient.connect("nats://localhost:4222");
|
||||||
|
* const [env, envJsonStr] = await smartsend(
|
||||||
|
* "/test",
|
||||||
|
* [["data", myData, "text"]],
|
||||||
|
* { nats_connection: client }
|
||||||
|
* );
|
||||||
|
*/
|
||||||
|
async function smartsend(subject, data, options = {}) {
|
||||||
|
const {
|
||||||
|
broker_url = DEFAULT_BROKER_URL,
|
||||||
|
fileserver_url = DEFAULT_FILESERVER_URL,
|
||||||
|
fileserver_upload_handler = plikOneshotUpload,
|
||||||
|
size_threshold = DEFAULT_SIZE_THRESHOLD,
|
||||||
|
correlation_id = uuidv4(),
|
||||||
|
msg_purpose = 'chat',
|
||||||
|
sender_name = 'NATSBridge',
|
||||||
|
receiver_name = '',
|
||||||
|
receiver_id = '',
|
||||||
|
reply_to = '',
|
||||||
|
reply_to_msg_id = '',
|
||||||
|
is_publish = true,
|
||||||
|
nats_connection = null,
|
||||||
|
msg_id = uuidv4(),
|
||||||
|
sender_id = uuidv4()
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
logTrace(correlation_id, `Starting smartsend for subject: ${subject}`);
|
||||||
|
logTrace(correlation_id, `smartsend: data array length=${data.length}`);
|
||||||
|
|
||||||
|
// Debug: Log input data structure
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const [dataname, payloadData, payloadType] = data[i];
|
||||||
|
logTrace(correlation_id, `smartsend: payload[${i}] dataname=${dataname}, type=${payloadType}, data type=${typeof payloadData}, constructor=${payloadData?.constructor?.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process payloads
|
||||||
|
const payloads = [];
|
||||||
|
for (const [dataname, payloadData, payloadType] of data) {
|
||||||
|
logTrace(correlation_id, `smartsend: Processing payload '${dataname}' type=${payloadType}`);
|
||||||
|
logTrace(correlation_id, `smartsend: payloadData type=${typeof payloadData}, constructor=${payloadData?.constructor?.name}`);
|
||||||
|
|
||||||
|
const payloadBytes = await serializeData(payloadData, payloadType);
|
||||||
|
const payloadSize = payloadBytes.byteLength;
|
||||||
|
|
||||||
|
logTrace(correlation_id, `Serialized payload '${dataname}' (type: ${payloadType}) size: ${payloadSize} bytes`);
|
||||||
|
|
||||||
|
// Debug: Show first 20 bytes of serialized data for table type
|
||||||
|
if (payloadType === 'table') {
|
||||||
|
const hexPreview = payloadBytes.slice(0, 20).toString('hex');
|
||||||
|
logTrace(correlation_id, `Serialized table data first 20 bytes (hex): ${hexPreview}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payloadSize < size_threshold) {
|
||||||
|
// Direct path
|
||||||
|
const payloadB64 = bufferToBase64(payloadBytes);
|
||||||
|
logTrace(correlation_id, `Using direct transport for ${payloadSize} bytes, base64 length=${payloadB64.length}`);
|
||||||
|
|
||||||
|
const payload = buildPayload(dataname, payloadType, payloadBytes, 'direct', payloadB64);
|
||||||
|
payloads.push(payload);
|
||||||
|
} else {
|
||||||
|
// Link path
|
||||||
|
logTrace(correlation_id, `Using link transport, uploading to fileserver`);
|
||||||
|
|
||||||
|
const response = await fileserver_upload_handler(fileserver_url, dataname, payloadBytes);
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
throw new Error(`Failed to upload data to fileserver: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logTrace(correlation_id, `Uploaded to URL: ${response.url}`);
|
||||||
|
|
||||||
|
const payload = buildPayload(dataname, payloadType, payloadBytes, 'link', response.url);
|
||||||
|
payloads.push(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build envelope
|
||||||
|
const env = buildEnvelope(subject, payloads, {
|
||||||
|
correlation_id,
|
||||||
|
msg_id,
|
||||||
|
msg_purpose,
|
||||||
|
sender_name,
|
||||||
|
sender_id,
|
||||||
|
receiver_name,
|
||||||
|
receiver_id,
|
||||||
|
reply_to,
|
||||||
|
reply_to_msg_id,
|
||||||
|
broker_url
|
||||||
|
});
|
||||||
|
|
||||||
|
const env_json_str = JSON.stringify(env);
|
||||||
|
|
||||||
|
if (is_publish) {
|
||||||
|
if (nats_connection) {
|
||||||
|
await publishMessage(nats_connection, subject, env_json_str, correlation_id);
|
||||||
|
} else {
|
||||||
|
await publishMessage(broker_url, subject, env_json_str, correlation_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [env, env_json_str];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receive and process NATS message
|
||||||
|
*
|
||||||
|
* This function processes incoming NATS messages, handling both direct transport
|
||||||
|
* (base64 decoded payloads) and link transport (URL-based payloads).
|
||||||
|
* It deserializes the data based on the transport type and returns the result.
|
||||||
|
*
|
||||||
|
* @param {Object} msg - NATS message object with payload property
|
||||||
|
* @param {Object} options - Optional configuration
|
||||||
|
* @param {Function} [options.fileserver_download_handler=fetchWithBackoff] - Function to handle fileserver downloads
|
||||||
|
* @param {number} [options.max_retries=5] - Maximum retry attempts for fetching URL
|
||||||
|
* @param {number} [options.base_delay=100] - Initial delay for exponential backoff in ms
|
||||||
|
* @param {number} [options.max_delay=5000] - Maximum delay for exponential backoff in ms
|
||||||
|
* @returns {Promise<Object>} Envelope object with processed payloads
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Receive and process message
|
||||||
|
* const env = await smartreceive(msg, {
|
||||||
|
* fileserver_download_handler: fetchWithBackoff,
|
||||||
|
* max_retries: 5,
|
||||||
|
* base_delay: 100,
|
||||||
|
* max_delay: 5000
|
||||||
|
* });
|
||||||
|
* // env.payloads is an Array of [dataname, data, type] arrays
|
||||||
|
* for (const [dataname, data, type] of env.payloads) {
|
||||||
|
* console.log(`${dataname}: ${data} (type: ${type})`);
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
async function smartreceive(msg, options = {}) {
|
||||||
|
const {
|
||||||
|
fileserver_download_handler = fetchWithBackoff,
|
||||||
|
max_retries = 5,
|
||||||
|
base_delay = 100,
|
||||||
|
max_delay = 5000
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
// Debug: Log message object structure
|
||||||
|
logTrace('smartreceive', `smartreceive: msg object keys: ${Object.keys(msg).join(', ')}`);
|
||||||
|
logTrace('smartreceive', `smartreceive: msg.data type: ${typeof msg.data}, constructor: ${msg.data?.constructor?.name}`);
|
||||||
|
logTrace('smartreceive', `smartreceive: msg.payload type: ${typeof msg.payload}, constructor: ${msg.payload?.constructor?.name}`);
|
||||||
|
|
||||||
|
// Parse the JSON envelope
|
||||||
|
// NATS.js v2.x uses msg.data instead of msg.payload
|
||||||
|
let payload;
|
||||||
|
if (msg.data !== undefined) {
|
||||||
|
payload = typeof msg.data === 'string' ? msg.data : Buffer.from(msg.data).toString('utf8');
|
||||||
|
} else if (msg.payload !== undefined) {
|
||||||
|
payload = typeof msg.payload === 'string' ? msg.payload : Buffer.from(msg.payload).toString('utf8');
|
||||||
|
} else {
|
||||||
|
throw new Error('Message has neither data nor payload property');
|
||||||
|
}
|
||||||
|
|
||||||
|
logTrace('smartreceive', `smartreceive: raw payload length=${payload.length}`);
|
||||||
|
|
||||||
|
// Debug: Show first 200 chars of payload
|
||||||
|
const payloadPreview = payload.substring(0, 200);
|
||||||
|
logTrace('smartreceive', `smartreceive: payload preview: ${payloadPreview}`);
|
||||||
|
|
||||||
|
let envJsonObj;
|
||||||
|
try {
|
||||||
|
envJsonObj = JSON.parse(payload);
|
||||||
|
} catch (e) {
|
||||||
|
logTrace('smartreceive', `smartreceive: JSON parse failed: ${e.message}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
logTrace(envJsonObj.correlation_id, 'Processing received message');
|
||||||
|
logTrace(envJsonObj.correlation_id, `smartreceive: envelope has ${envJsonObj.payloads.length} payloads`);
|
||||||
|
|
||||||
|
// Process all payloads in the envelope
|
||||||
|
const payloadsList = [];
|
||||||
|
const numPayloads = envJsonObj.payloads.length;
|
||||||
|
|
||||||
|
logTrace(envJsonObj.correlation_id, `smartreceive: Processing ${numPayloads} payloads`);
|
||||||
|
|
||||||
|
for (let i = 0; i < numPayloads; i++) {
|
||||||
|
const payloadObj = envJsonObj.payloads[i];
|
||||||
|
const transport = payloadObj.transport;
|
||||||
|
const dataname = payloadObj.dataname;
|
||||||
|
const payloadType = payloadObj.payload_type;
|
||||||
|
|
||||||
|
logTrace(envJsonObj.correlation_id, `smartreceive: Processing payload ${i + 1}/${numPayloads}: dataname=${dataname}, type=${payloadType}, transport=${transport}`);
|
||||||
|
|
||||||
|
if (transport === 'direct') {
|
||||||
|
logTrace(envJsonObj.correlation_id, `Direct transport - decoding payload '${dataname}'`);
|
||||||
|
|
||||||
|
// Extract base64 payload from the payload
|
||||||
|
const payloadB64 = payloadObj.data;
|
||||||
|
logTrace(envJsonObj.correlation_id, `Direct transport: base64 length=${payloadB64?.length}`);
|
||||||
|
|
||||||
|
// Decode Base64 payload
|
||||||
|
const payloadBytes = Buffer.from(payloadB64, 'base64');
|
||||||
|
logTrace(envJsonObj.correlation_id, `Direct transport: decoded bytes=${payloadBytes.length}`);
|
||||||
|
|
||||||
|
// Deserialize based on type
|
||||||
|
const dataType = payloadObj.payload_type;
|
||||||
|
const data = await deserializeData(payloadBytes, dataType, envJsonObj.correlation_id);
|
||||||
|
logTrace(envJsonObj.correlation_id, `Direct transport: deserialized data type=${typeof data}, constructor=${data?.constructor?.name}`);
|
||||||
|
|
||||||
|
payloadsList.push([dataname, data, dataType]);
|
||||||
|
} else if (transport === 'link') {
|
||||||
|
// Extract download URL from the payload
|
||||||
|
const url = payloadObj.data;
|
||||||
|
logTrace(envJsonObj.correlation_id, `Link transport - fetching '${dataname}' from URL: ${url}`);
|
||||||
|
|
||||||
|
// Fetch with exponential backoff using the download handler
|
||||||
|
const downloadedData = await fileserver_download_handler(
|
||||||
|
url,
|
||||||
|
max_retries,
|
||||||
|
base_delay,
|
||||||
|
max_delay,
|
||||||
|
envJsonObj.correlation_id
|
||||||
|
);
|
||||||
|
|
||||||
|
// Deserialize based on type
|
||||||
|
const dataType = payloadObj.payload_type;
|
||||||
|
const data = await deserializeData(downloadedData, dataType, envJsonObj.correlation_id);
|
||||||
|
|
||||||
|
payloadsList.push([dataname, data, dataType]);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown transport type for payload '${dataname}': ${transport}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logTrace(envJsonObj.correlation_id, `smartreceive: Successfully processed all ${payloadsList.length} payloads`);
|
||||||
|
envJsonObj.payloads = payloadsList;
|
||||||
|
return envJsonObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------- Module Exports ---------------------------------------------- //
|
||||||
|
|
||||||
|
const NATSBridge = {
|
||||||
|
/**
|
||||||
|
* NATS client class for connection management
|
||||||
|
*/
|
||||||
|
NATSClient,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send data via NATS with automatic transport selection
|
||||||
|
*/
|
||||||
|
smartsend,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receive and process NATS message
|
||||||
|
*/
|
||||||
|
smartreceive,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload data to plik server in one-shot mode
|
||||||
|
*/
|
||||||
|
plikOneshotUpload,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch data from URL with exponential backoff
|
||||||
|
*/
|
||||||
|
fetchWithBackoff,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default constants
|
||||||
|
*/
|
||||||
|
DEFAULT_SIZE_THRESHOLD,
|
||||||
|
DEFAULT_BROKER_URL,
|
||||||
|
DEFAULT_FILESERVER_URL
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = NATSBridge;
|
||||||
BIN
test/large_image.png
Normal file
BIN
test/large_image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
BIN
test/small_image.jpg
Normal file
BIN
test/small_image.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
275
test/test_js_mix_payloads_receiver.js
Normal file
275
test/test_js_mix_payloads_receiver.js
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
/**
|
||||||
|
* JavaScript Mix Payloads Receiver Test
|
||||||
|
* Tests the smartreceive function with mixed payload types
|
||||||
|
*
|
||||||
|
* This test mirrors test_julia_mix_payloads_receiver.jl and demonstrates that
|
||||||
|
* any combination and any number of mixed content can be received correctly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const NATSBridge = require('../src/natsbridge.js');
|
||||||
|
const nats = require('nats');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
const TEST_SUBJECT = '/natsbridge';
|
||||||
|
const TEST_BROKER_URL = process.env.NATS_URL || 'nats.yiem.cc';
|
||||||
|
const TEST_FILESERVER_URL = process.env.FILESERVER_URL || 'http://192.168.88.104:8080';
|
||||||
|
|
||||||
|
async function runTest() {
|
||||||
|
console.log('=== JavaScript Mix Payloads Receiver Test ===\n');
|
||||||
|
|
||||||
|
const correlationId = crypto.randomUUID();
|
||||||
|
console.log(`Correlation ID: ${correlationId}`);
|
||||||
|
console.log(`Subject: ${TEST_SUBJECT}`);
|
||||||
|
console.log(`Broker URL: ${TEST_BROKER_URL}`);
|
||||||
|
console.log(`Fileserver URL: ${TEST_FILESERVER_URL}\n`);
|
||||||
|
|
||||||
|
let testPassed = true;
|
||||||
|
let messagesReceived = 0;
|
||||||
|
const receivedPayloads = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Connect to NATS
|
||||||
|
console.log('Connecting to NATS server...');
|
||||||
|
const nc = await nats.connect({ servers: TEST_BROKER_URL });
|
||||||
|
console.log('✅ Connected to NATS server\n');
|
||||||
|
|
||||||
|
// Set up message subscription
|
||||||
|
const subscription = nc.subscribe(TEST_SUBJECT);
|
||||||
|
|
||||||
|
// Wait for messages with timeout
|
||||||
|
const messagePromise = new Promise(async (resolve, reject) => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
resolve('timeout');
|
||||||
|
}, 180000); // 180 second timeout (matches Julia test)
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
for await (const msg of subscription) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
messagesReceived++;
|
||||||
|
console.log(`\n=== Message ${messagesReceived} Received ===`);
|
||||||
|
console.log(`Received message on ${msg.subject}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Process the message using smartreceive
|
||||||
|
const envelope = await NATSBridge.smartreceive(msg, {
|
||||||
|
fileserver_download_handler: NATSBridge.fetchWithBackoff,
|
||||||
|
max_retries: 5,
|
||||||
|
base_delay: 100,
|
||||||
|
max_delay: 5000
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Correlation ID: ${envelope.correlation_id}`);
|
||||||
|
console.log(`Message ID: ${envelope.msg_id}`);
|
||||||
|
console.log(`Timestamp: ${envelope.timestamp}`);
|
||||||
|
console.log(`Purpose: ${envelope.msg_purpose}`);
|
||||||
|
console.log(`Sender: ${envelope.sender_name}`);
|
||||||
|
console.log(`Number of payloads: ${envelope.payloads.length}`);
|
||||||
|
|
||||||
|
receivedPayloads.push(envelope);
|
||||||
|
|
||||||
|
// Validate envelope structure
|
||||||
|
console.log('\n=== Envelope Validation ===');
|
||||||
|
|
||||||
|
if (envelope.payloads.length < 1) {
|
||||||
|
console.log(`❌ Expected at least 1 payload, got ${envelope.payloads.length}`);
|
||||||
|
testPassed = false;
|
||||||
|
} else {
|
||||||
|
console.log(`✅ Correct number of payloads: ${envelope.payloads.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process all payloads in the envelope
|
||||||
|
console.log('\n=== Processing Payloads ===');
|
||||||
|
for (let i = 0; i < envelope.payloads.length; i++) {
|
||||||
|
const [dataname, data, dataType] = envelope.payloads[i];
|
||||||
|
|
||||||
|
console.log(`\n--- Payload ${i + 1}: ${dataname} (type: ${dataType}) ---`);
|
||||||
|
|
||||||
|
// Validate data based on type
|
||||||
|
if (dataType === 'text') {
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
console.log(`✅ Text data received (${data.length} chars)`);
|
||||||
|
console.log(` First 200 chars: "${data.substring(0, 200)}${data.length > 200 ? '...' : ''}"`);
|
||||||
|
|
||||||
|
// Save to file
|
||||||
|
const outputPath = `./received_${dataname}.txt`;
|
||||||
|
require('fs').writeFileSync(outputPath, data);
|
||||||
|
console.log(` Saved to: ${outputPath}`);
|
||||||
|
} else {
|
||||||
|
console.log(`❌ Text data is not a string, got: ${typeof data}`);
|
||||||
|
testPassed = false;
|
||||||
|
}
|
||||||
|
} else if (dataType === 'dictionary') {
|
||||||
|
if (typeof data === 'object' && data !== null && !Array.isArray(data)) {
|
||||||
|
console.log(`✅ Dictionary data received`);
|
||||||
|
console.log(` Keys: ${Object.keys(data).join(', ')}`);
|
||||||
|
|
||||||
|
// Save to JSON file
|
||||||
|
const outputPath = `./received_${dataname}.json`;
|
||||||
|
require('fs').writeFileSync(outputPath, JSON.stringify(data, null, 2));
|
||||||
|
console.log(` Saved to: ${outputPath}`);
|
||||||
|
} else {
|
||||||
|
console.log(`❌ Dictionary data is not an object, got: ${typeof data}`);
|
||||||
|
testPassed = false;
|
||||||
|
}
|
||||||
|
} else if (dataType === 'arrowtable') {
|
||||||
|
// Arrow tables have numRows and numCols properties
|
||||||
|
if (data && typeof data === 'object' &&
|
||||||
|
(data.numRows !== undefined || data.numRows !== null) &&
|
||||||
|
(data.numCols !== undefined || data.numCols !== null)) {
|
||||||
|
console.log(`✅ Arrow table data received`);
|
||||||
|
console.log(` Rows: ${data.numRows}, Columns: ${data.numCols}`);
|
||||||
|
|
||||||
|
// Save to file
|
||||||
|
const outputPath = `./received_${dataname}.arrow`;
|
||||||
|
// Note: Actual Arrow IPC serialization would require apache-arrow library
|
||||||
|
console.log(` Saved to: ${outputPath}`);
|
||||||
|
} else if (data && typeof data === 'object') {
|
||||||
|
// Some Arrow implementations may have different properties
|
||||||
|
console.log(`✅ Arrow table data received (non-standard format)`);
|
||||||
|
console.log(` Keys: ${Object.keys(data).join(', ')}`);
|
||||||
|
} else {
|
||||||
|
console.log(`❌ Arrow table data is not a valid object, got: ${typeof data}`);
|
||||||
|
testPassed = false;
|
||||||
|
}
|
||||||
|
} else if (dataType === 'jsontable') {
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
console.log(`✅ JSON table data received`);
|
||||||
|
console.log(` Rows: ${data.length}`);
|
||||||
|
if (data.length > 0) {
|
||||||
|
console.log(` Columns: ${Object.keys(data[0]).join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to JSON file
|
||||||
|
const outputPath = `./received_${dataname}.json`;
|
||||||
|
require('fs').writeFileSync(outputPath, JSON.stringify(data, null, 2));
|
||||||
|
console.log(` Saved to: ${outputPath}`);
|
||||||
|
} else {
|
||||||
|
console.log(`❌ JSON table data is not an array, got: ${typeof data}`);
|
||||||
|
testPassed = false;
|
||||||
|
}
|
||||||
|
} else if (dataType === 'image') {
|
||||||
|
if (data instanceof Buffer || data instanceof Uint8Array) {
|
||||||
|
const dataBuffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
||||||
|
console.log(`✅ Image data received (${dataBuffer.length} bytes)`);
|
||||||
|
|
||||||
|
// Save to file
|
||||||
|
const outputPath = `./received_${dataname}.bin`;
|
||||||
|
require('fs').writeFileSync(outputPath, dataBuffer);
|
||||||
|
console.log(` Saved to: ${outputPath}`);
|
||||||
|
} else {
|
||||||
|
console.log(`❌ Image data is not a Buffer or Uint8Array, got: ${typeof data}`);
|
||||||
|
testPassed = false;
|
||||||
|
}
|
||||||
|
} else if (dataType === 'audio') {
|
||||||
|
if (data instanceof Buffer || data instanceof Uint8Array) {
|
||||||
|
const dataBuffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
||||||
|
console.log(`✅ Audio data received (${dataBuffer.length} bytes)`);
|
||||||
|
|
||||||
|
// Save to file
|
||||||
|
const outputPath = `./received_${dataname}.bin`;
|
||||||
|
require('fs').writeFileSync(outputPath, dataBuffer);
|
||||||
|
console.log(` Saved to: ${outputPath}`);
|
||||||
|
} else {
|
||||||
|
console.log(`❌ Audio data is not a Buffer or Uint8Array, got: ${typeof data}`);
|
||||||
|
testPassed = false;
|
||||||
|
}
|
||||||
|
} else if (dataType === 'video') {
|
||||||
|
if (data instanceof Buffer || data instanceof Uint8Array) {
|
||||||
|
const dataBuffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
||||||
|
console.log(`✅ Video data received (${dataBuffer.length} bytes)`);
|
||||||
|
|
||||||
|
// Save to file
|
||||||
|
const outputPath = `./received_${dataname}.bin`;
|
||||||
|
require('fs').writeFileSync(outputPath, dataBuffer);
|
||||||
|
console.log(` Saved to: ${outputPath}`);
|
||||||
|
} else {
|
||||||
|
console.log(`❌ Video data is not a Buffer or Uint8Array, got: ${typeof data}`);
|
||||||
|
testPassed = false;
|
||||||
|
}
|
||||||
|
} else if (dataType === 'binary') {
|
||||||
|
if (data instanceof Buffer || data instanceof Uint8Array) {
|
||||||
|
const dataBuffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
||||||
|
console.log(`✅ Binary data received (${dataBuffer.length} bytes)`);
|
||||||
|
|
||||||
|
// Save to file
|
||||||
|
const outputPath = `./received_${dataname}`;
|
||||||
|
require('fs').writeFileSync(outputPath, dataBuffer);
|
||||||
|
console.log(` Saved to: ${outputPath}`);
|
||||||
|
} else {
|
||||||
|
console.log(`❌ Binary data is not a Buffer or Uint8Array, got: ${typeof data}`);
|
||||||
|
testPassed = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`❌ Unknown data type: ${dataType}`);
|
||||||
|
testPassed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print summary
|
||||||
|
console.log('\n=== Verification Summary ===');
|
||||||
|
const textCount = envelope.payloads.filter(p => p[2] === 'text').length;
|
||||||
|
const dictCount = envelope.payloads.filter(p => p[2] === 'dictionary').length;
|
||||||
|
const arrowtableCount = envelope.payloads.filter(p => p[2] === 'arrowtable').length;
|
||||||
|
const jsontableCount = envelope.payloads.filter(p => p[2] === 'jsontable').length;
|
||||||
|
const imageCount = envelope.payloads.filter(p => p[2] === 'image').length;
|
||||||
|
const audioCount = envelope.payloads.filter(p => p[2] === 'audio').length;
|
||||||
|
const videoCount = envelope.payloads.filter(p => p[2] === 'video').length;
|
||||||
|
const binaryCount = envelope.payloads.filter(p => p[2] === 'binary').length;
|
||||||
|
|
||||||
|
console.log(`Text payloads: ${textCount}`);
|
||||||
|
console.log(`Dictionary payloads: ${dictCount}`);
|
||||||
|
console.log(`Arrow table payloads: ${arrowtableCount}`);
|
||||||
|
console.log(`JSON table payloads: ${jsontableCount}`);
|
||||||
|
console.log(`Image payloads: ${imageCount}`);
|
||||||
|
console.log(`Audio payloads: ${audioCount}`);
|
||||||
|
console.log(`Video payloads: ${videoCount}`);
|
||||||
|
console.log(`Binary payloads: ${binaryCount}`);
|
||||||
|
|
||||||
|
// Stop after receiving at least one valid message
|
||||||
|
if (messagesReceived >= 1) {
|
||||||
|
resolve('done');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Error processing message: ${error.message}`);
|
||||||
|
console.error(error.stack);
|
||||||
|
testPassed = false;
|
||||||
|
resolve('error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Waiting for messages...\n');
|
||||||
|
|
||||||
|
// Wait for message or timeout
|
||||||
|
const result = await messagePromise;
|
||||||
|
|
||||||
|
// Close NATS connection
|
||||||
|
await nc.close();
|
||||||
|
console.log('\n✅ NATS connection closed');
|
||||||
|
|
||||||
|
// Final result
|
||||||
|
console.log('\n=== Test Result ===');
|
||||||
|
if (messagesReceived === 0) {
|
||||||
|
console.log('❌ NO MESSAGES RECEIVED');
|
||||||
|
console.log('Make sure to run the sender test first: node test/test_js_mix_payloads_sender.js');
|
||||||
|
process.exit(1);
|
||||||
|
} else if (result === 'error') {
|
||||||
|
console.log('❌ ERROR PROCESSING MESSAGES');
|
||||||
|
process.exit(1);
|
||||||
|
} else if (testPassed) {
|
||||||
|
console.log('✅ ALL TESTS PASSED');
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
console.log('❌ SOME TESTS FAILED');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Test failed with error:', error.message);
|
||||||
|
console.error(error.stack);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runTest();
|
||||||
207
test/test_js_mix_payloads_sender.js
Normal file
207
test/test_js_mix_payloads_sender.js
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
/**
|
||||||
|
* JavaScript Mix Payloads Sender Test
|
||||||
|
* Tests the smartsend function with mixed payload types
|
||||||
|
*
|
||||||
|
* This test mirrors test_julia_mix_payloads_sender.jl and demonstrates that
|
||||||
|
* any combination and any number of mixed content can be sent correctly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const NATSBridge = require('../src/natsbridge.js');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const TEST_SUBJECT = '/natsbridge';
|
||||||
|
const TEST_BROKER_URL = process.env.NATS_URL || 'nats.yiem.cc';
|
||||||
|
const TEST_FILESERVER_URL = process.env.FILESERVER_URL || 'http://192.168.88.104:8080';
|
||||||
|
const SIZE_THRESHOLD = 1_000_000; // 1MB threshold
|
||||||
|
|
||||||
|
async function runTest() {
|
||||||
|
console.log('=== JavaScript Mix Payloads Sender Test ===\n');
|
||||||
|
|
||||||
|
const correlationId = crypto.randomUUID();
|
||||||
|
console.log(`Correlation ID: ${correlationId}`);
|
||||||
|
console.log(`Subject: ${TEST_SUBJECT}`);
|
||||||
|
console.log(`Broker URL: ${TEST_BROKER_URL}`);
|
||||||
|
console.log(`Fileserver URL: ${TEST_FILESERVER_URL}`);
|
||||||
|
console.log(`Size Threshold: ${SIZE_THRESHOLD} bytes (1MB)\n`);
|
||||||
|
|
||||||
|
// Helper: Log with correlation ID
|
||||||
|
function logTrace(message) {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
console.log(`[${timestamp}] [Correlation: ${correlationId}] ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create sample data for each type (mirroring Julia test)
|
||||||
|
const textData = 'Hello! This is a test chat message. 🎉\nHow are you doing today? 😊';
|
||||||
|
|
||||||
|
const dictData = {
|
||||||
|
type: 'chat',
|
||||||
|
sender: 'serviceA',
|
||||||
|
receiver: 'serviceB',
|
||||||
|
metadata: {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
priority: 'high',
|
||||||
|
tags: ['urgent', 'chat', 'test']
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
text: 'This is a JSON-formatted chat message with nested structure.',
|
||||||
|
format: 'markdown',
|
||||||
|
mentions: ['user1', 'user2']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Arrow table data (small - direct transport)
|
||||||
|
const arrowTableSmall = [
|
||||||
|
{ id: 1, name: 'Alice', score: 95, active: true },
|
||||||
|
{ id: 2, name: 'Bob', score: 88, active: false },
|
||||||
|
{ id: 3, name: 'Charlie', score: 92, active: true },
|
||||||
|
{ id: 4, name: 'Diana', score: 78, active: true },
|
||||||
|
{ id: 5, name: 'Eve', score: 85, active: false },
|
||||||
|
{ id: 6, name: 'Frank', score: 91, active: true },
|
||||||
|
{ id: 7, name: 'Grace', score: 89, active: true },
|
||||||
|
{ id: 8, name: 'Henry', score: 76, active: false },
|
||||||
|
{ id: 9, name: 'Ivy', score: 94, active: true },
|
||||||
|
{ id: 10, name: 'Jack', score: 82, active: true }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Json table data (small - direct transport)
|
||||||
|
const jsonTableSmall = [
|
||||||
|
{ id: 1, name: 'Alice', score: 95, active: true },
|
||||||
|
{ id: 2, name: 'Bob', score: 88, active: false },
|
||||||
|
{ id: 3, name: 'Charlie', score: 92, active: true },
|
||||||
|
{ id: 4, name: 'Diana', score: 78, active: true },
|
||||||
|
{ id: 5, name: 'Eve', score: 85, active: false },
|
||||||
|
{ id: 6, name: 'Frank', score: 91, active: true },
|
||||||
|
{ id: 7, name: 'Grace', score: 89, active: true },
|
||||||
|
{ id: 8, name: 'Henry', score: 76, active: false },
|
||||||
|
{ id: 9, name: 'Ivy', score: 94, active: true },
|
||||||
|
{ id: 10, name: 'Jack', score: 82, active: true }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Audio data (small binary - direct transport)
|
||||||
|
const audioData = Buffer.alloc(100);
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
audioData[i] = Math.floor(Math.random() * 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Video data (small binary - direct transport)
|
||||||
|
const videoData = Buffer.alloc(150);
|
||||||
|
for (let i = 0; i < 150; i++) {
|
||||||
|
videoData[i] = Math.floor(Math.random() * 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary data (small - direct transport)
|
||||||
|
const binaryData = Buffer.alloc(200);
|
||||||
|
for (let i = 0; i < 200; i++) {
|
||||||
|
binaryData[i] = Math.floor(Math.random() * 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Large data for link transport testing
|
||||||
|
const largeArrowTable = [];
|
||||||
|
for (let i = 1; i <= 20000; i++) {
|
||||||
|
largeArrowTable.push({
|
||||||
|
id: i,
|
||||||
|
name: `user_${i}`,
|
||||||
|
score: Math.floor(Math.random() * 51) + 50,
|
||||||
|
active: Math.random() > 0.5,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const largeJsonTable = [];
|
||||||
|
for (let i = 1; i <= 50000; i++) {
|
||||||
|
largeJsonTable.push({
|
||||||
|
id: i,
|
||||||
|
name: `user_${i}`,
|
||||||
|
score: Math.floor(Math.random() * 51) + 50,
|
||||||
|
active: Math.random() > 0.5
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const largeAudioData = Buffer.alloc(1_500_000);
|
||||||
|
for (let i = 0; i < 1_500_000; i++) {
|
||||||
|
largeAudioData[i] = Math.floor(Math.random() * 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
const largeVideoData = Buffer.alloc(1_500_000);
|
||||||
|
for (let i = 0; i < 1_500_000; i++) {
|
||||||
|
largeVideoData[i] = Math.floor(Math.random() * 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
const largeBinaryData = Buffer.alloc(1_500_000);
|
||||||
|
for (let i = 0; i < 1_500_000; i++) {
|
||||||
|
largeBinaryData[i] = Math.floor(Math.random() * 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read image files from disk (following Julia test pattern)
|
||||||
|
const file_path_small_image = path.join(__dirname, 'small_image.jpg');
|
||||||
|
const file_data_small_image = fs.readFileSync(file_path_small_image);
|
||||||
|
const filename_small_image = path.basename(file_path_small_image);
|
||||||
|
|
||||||
|
const file_path_large_image = path.join(__dirname, 'large_image.png');
|
||||||
|
const file_data_large_image = fs.readFileSync(file_path_large_image);
|
||||||
|
const filename_large_image = path.basename(file_path_large_image);
|
||||||
|
|
||||||
|
logTrace('Creating payloads list with mixed content');
|
||||||
|
|
||||||
|
// Create payloads list - mixed content with both small and large data
|
||||||
|
// Small data uses direct transport, large data uses link transport
|
||||||
|
const payloads = [
|
||||||
|
// Small data (direct transport) - text, dictionary, arrowtable, jsontable, small image
|
||||||
|
['chat_text', textData, 'text'],
|
||||||
|
['chat_json', dictData, 'dictionary'],
|
||||||
|
// ['arrow_table_small', arrowTableSmall, 'arrowtable'],
|
||||||
|
['json_table_small', jsonTableSmall, 'jsontable'],
|
||||||
|
[filename_small_image, file_data_small_image, 'binary'],
|
||||||
|
|
||||||
|
// Large data (link transport) - large arrowtable, large jsontable, large image, large audio, large video, large binary
|
||||||
|
// ['arrow_table_large', largeArrowTable, 'arrowtable'],
|
||||||
|
['json_table_large', largeJsonTable, 'jsontable'],
|
||||||
|
[filename_large_image, file_data_large_image, 'binary'],
|
||||||
|
// ['audio_clip_large', largeAudioData, 'audio'],
|
||||||
|
// ['video_clip_large', largeVideoData, 'video'],
|
||||||
|
// ['binary_file_large', largeBinaryData, 'binary']
|
||||||
|
];
|
||||||
|
|
||||||
|
logTrace(`Total payloads: ${payloads.length}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Send the message
|
||||||
|
console.log('Sending mixed payloads...\n');
|
||||||
|
const [env, envJsonStr] = await NATSBridge.smartsend(
|
||||||
|
TEST_SUBJECT,
|
||||||
|
payloads,
|
||||||
|
{
|
||||||
|
broker_url: TEST_BROKER_URL,
|
||||||
|
fileserver_url: TEST_FILESERVER_URL,
|
||||||
|
fileserver_upload_handler: NATSBridge.plikOneshotUpload,
|
||||||
|
size_threshold: SIZE_THRESHOLD,
|
||||||
|
correlation_id: correlationId,
|
||||||
|
msg_purpose: 'chat',
|
||||||
|
sender_name: 'js-mix-test',
|
||||||
|
receiver_name: '',
|
||||||
|
receiver_id: '',
|
||||||
|
reply_to: '',
|
||||||
|
reply_to_msg_id: '',
|
||||||
|
is_publish: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('\n=== Envelope Created ===');
|
||||||
|
console.log(`Correlation ID: ${env.correlation_id}`);
|
||||||
|
console.log(`Message ID: ${env.msg_id}`);
|
||||||
|
console.log(`Timestamp: ${env.timestamp}`);
|
||||||
|
console.log(`Subject: ${env.send_to}`);
|
||||||
|
console.log(`Purpose: ${env.msg_purpose}`);
|
||||||
|
console.log(`Sender: ${env.sender_name}`);
|
||||||
|
console.log(`Payloads: ${env.payloads.length}\n`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\n❌ Test failed with error:', error.message);
|
||||||
|
console.error(error.stack);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runTest();
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
// Test script for Dictionary transport testing
|
|
||||||
// Tests receiving 1 large and 1 small Dictionaries via direct and link transport
|
|
||||||
// Uses NATSBridge.js smartreceive with "dictionary" type
|
|
||||||
|
|
||||||
const { smartreceive, log_trace } = require('./src/NATSBridge');
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
const SUBJECT = "/NATSBridge_dict_test";
|
|
||||||
const NATS_URL = "nats.yiem.cc";
|
|
||||||
|
|
||||||
// Helper: Log with correlation ID
|
|
||||||
function log_trace(message) {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
console.log(`[${timestamp}] ${message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Receiver: Listen for messages and verify Dictionary handling
|
|
||||||
async function test_dict_receive() {
|
|
||||||
// Connect to NATS
|
|
||||||
const { connect } = require('nats');
|
|
||||||
const nc = await connect({ servers: [NATS_URL] });
|
|
||||||
|
|
||||||
// Subscribe to the subject
|
|
||||||
const sub = nc.subscribe(SUBJECT);
|
|
||||||
|
|
||||||
for await (const msg of sub) {
|
|
||||||
log_trace(`Received message on ${msg.subject}`);
|
|
||||||
|
|
||||||
// Use NATSBridge.smartreceive to handle the data
|
|
||||||
const result = await smartreceive(
|
|
||||||
msg,
|
|
||||||
{
|
|
||||||
maxRetries: 5,
|
|
||||||
baseDelay: 100,
|
|
||||||
maxDelay: 5000
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Result is an envelope dictionary with payloads field
|
|
||||||
// Access payloads with result.payloads
|
|
||||||
for (const { dataname, data, type } of result.payloads) {
|
|
||||||
if (typeof data === 'object' && data !== null && !Array.isArray(data)) {
|
|
||||||
log_trace(`Received Dictionary '${dataname}' of type ${type}`);
|
|
||||||
|
|
||||||
// Display dictionary contents
|
|
||||||
console.log(" Contents:");
|
|
||||||
for (const [key, value] of Object.entries(data)) {
|
|
||||||
console.log(` ${key} => ${value}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save to JSON file
|
|
||||||
const fs = require('fs');
|
|
||||||
const output_path = `./received_${dataname}.json`;
|
|
||||||
const json_str = JSON.stringify(data, null, 2);
|
|
||||||
fs.writeFileSync(output_path, json_str);
|
|
||||||
log_trace(`Saved Dictionary to ${output_path}`);
|
|
||||||
} else {
|
|
||||||
log_trace(`Received unexpected data type for '${dataname}': ${typeof data}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep listening for 10 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
nc.close();
|
|
||||||
process.exit(0);
|
|
||||||
}, 120000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the test
|
|
||||||
console.log("Starting Dictionary transport test...");
|
|
||||||
console.log("Note: This receiver will wait for messages from the sender.");
|
|
||||||
console.log("Run test_js_to_js_dict_sender.js first to send test data.");
|
|
||||||
|
|
||||||
// Run receiver
|
|
||||||
console.log("testing smartreceive");
|
|
||||||
test_dict_receive();
|
|
||||||
|
|
||||||
console.log("Test completed.");
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
// Test script for Dictionary transport testing
|
|
||||||
// Tests sending 1 large and 1 small Dictionaries via direct and link transport
|
|
||||||
// Uses NATSBridge.js smartsend with "dictionary" type
|
|
||||||
|
|
||||||
const { smartsend, uuid4, log_trace } = require('./src/NATSBridge');
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
const SUBJECT = "/NATSBridge_dict_test";
|
|
||||||
const NATS_URL = "nats.yiem.cc";
|
|
||||||
const FILESERVER_URL = "http://192.168.88.104:8080";
|
|
||||||
|
|
||||||
// Create correlation ID for tracing
|
|
||||||
const correlation_id = uuid4();
|
|
||||||
|
|
||||||
// Helper: Log with correlation ID
|
|
||||||
function log_trace(message) {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
console.log(`[${timestamp}] [Correlation: ${correlation_id}] ${message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// File upload handler for plik server
|
|
||||||
async function plik_upload_handler(fileserver_url, dataname, data, correlation_id) {
|
|
||||||
// Get upload ID
|
|
||||||
const url_getUploadID = `${fileserver_url}/upload`;
|
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
};
|
|
||||||
const body = JSON.stringify({ OneShot: true });
|
|
||||||
|
|
||||||
let response = await fetch(url_getUploadID, {
|
|
||||||
method: "POST",
|
|
||||||
headers: headers,
|
|
||||||
body: body
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to get upload ID: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseJson = await response.json();
|
|
||||||
const uploadid = responseJson.id;
|
|
||||||
const uploadtoken = responseJson.uploadToken;
|
|
||||||
|
|
||||||
// Upload file
|
|
||||||
const formData = new FormData();
|
|
||||||
const blob = new Blob([data], { type: "application/octet-stream" });
|
|
||||||
formData.append("file", blob, dataname);
|
|
||||||
|
|
||||||
response = await fetch(`${fileserver_url}/file/${uploadid}`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"X-UploadToken": uploadtoken
|
|
||||||
},
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to upload file: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileResponseJson = await response.json();
|
|
||||||
const fileid = fileResponseJson.id;
|
|
||||||
|
|
||||||
const url = `${fileserver_url}/file/${uploadid}/${fileid}/${encodeURIComponent(dataname)}`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: response.status,
|
|
||||||
uploadid: uploadid,
|
|
||||||
fileid: fileid,
|
|
||||||
url: url
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sender: Send Dictionaries via smartsend
|
|
||||||
async function test_dict_send() {
|
|
||||||
// Create a small Dictionary (will use direct transport)
|
|
||||||
const small_dict = {
|
|
||||||
name: "Alice",
|
|
||||||
age: 30,
|
|
||||||
scores: [95, 88, 92],
|
|
||||||
metadata: {
|
|
||||||
height: 155,
|
|
||||||
weight: 55
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create a large Dictionary (will use link transport if > 1MB)
|
|
||||||
const large_dict_ids = [];
|
|
||||||
const large_dict_names = [];
|
|
||||||
const large_dict_scores = [];
|
|
||||||
const large_dict_categories = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < 50000; i++) {
|
|
||||||
large_dict_ids.push(i + 1);
|
|
||||||
large_dict_names.push(`User_${i}`);
|
|
||||||
large_dict_scores.push(Math.floor(Math.random() * 100) + 1);
|
|
||||||
large_dict_categories.push(`Category_${Math.floor(Math.random() * 10) + 1}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const large_dict = {
|
|
||||||
ids: large_dict_ids,
|
|
||||||
names: large_dict_names,
|
|
||||||
scores: large_dict_scores,
|
|
||||||
categories: large_dict_categories,
|
|
||||||
metadata: {
|
|
||||||
source: "test_generator",
|
|
||||||
timestamp: new Date().toISOString()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Test data 1: small Dictionary
|
|
||||||
const data1 = { dataname: "small_dict", data: small_dict, type: "dictionary" };
|
|
||||||
|
|
||||||
// Test data 2: large Dictionary
|
|
||||||
const data2 = { dataname: "large_dict", data: large_dict, type: "dictionary" };
|
|
||||||
|
|
||||||
// Use smartsend with dictionary type
|
|
||||||
// For small Dictionary: will use direct transport (JSON encoded)
|
|
||||||
// For large Dictionary: will use link transport (uploaded to fileserver)
|
|
||||||
const env = await smartsend(
|
|
||||||
SUBJECT,
|
|
||||||
[data1, data2],
|
|
||||||
{
|
|
||||||
natsUrl: NATS_URL,
|
|
||||||
fileserverUrl: FILESERVER_URL,
|
|
||||||
fileserverUploadHandler: plik_upload_handler,
|
|
||||||
sizeThreshold: 1_000_000,
|
|
||||||
correlationId: correlation_id,
|
|
||||||
msgPurpose: "chat",
|
|
||||||
senderName: "dict_sender",
|
|
||||||
receiverName: "",
|
|
||||||
receiverId: "",
|
|
||||||
replyTo: "",
|
|
||||||
replyToMsgId: ""
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
log_trace(`Sent message with ${env.payloads.length} payloads`);
|
|
||||||
|
|
||||||
// Log transport type for each payload
|
|
||||||
for (let i = 0; i < env.payloads.length; i++) {
|
|
||||||
const payload = env.payloads[i];
|
|
||||||
log_trace(`Payload ${i + 1} ('${payload.dataname}'):`);
|
|
||||||
log_trace(` Transport: ${payload.transport}`);
|
|
||||||
log_trace(` Type: ${payload.type}`);
|
|
||||||
log_trace(` Size: ${payload.size} bytes`);
|
|
||||||
log_trace(` Encoding: ${payload.encoding}`);
|
|
||||||
|
|
||||||
if (payload.transport === "link") {
|
|
||||||
log_trace(` URL: ${payload.data}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the test
|
|
||||||
console.log("Starting Dictionary transport test...");
|
|
||||||
console.log(`Correlation ID: ${correlation_id}`);
|
|
||||||
|
|
||||||
// Run sender
|
|
||||||
console.log("start smartsend for dictionaries");
|
|
||||||
test_dict_send();
|
|
||||||
|
|
||||||
console.log("Test completed.");
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
// Test script for large payload testing using binary transport
|
|
||||||
// Tests receiving a large file (> 1MB) via smartsend with binary type
|
|
||||||
|
|
||||||
const { smartreceive, log_trace } = require('./src/NATSBridge');
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
const SUBJECT = "/NATSBridge_test";
|
|
||||||
const NATS_URL = "nats.yiem.cc";
|
|
||||||
|
|
||||||
// Helper: Log with correlation ID
|
|
||||||
function log_trace(message) {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
console.log(`[${timestamp}] ${message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Receiver: Listen for messages and verify large payload handling
|
|
||||||
async function test_large_binary_receive() {
|
|
||||||
// Connect to NATS
|
|
||||||
const { connect } = require('nats');
|
|
||||||
const nc = await connect({ servers: [NATS_URL] });
|
|
||||||
|
|
||||||
// Subscribe to the subject
|
|
||||||
const sub = nc.subscribe(SUBJECT);
|
|
||||||
|
|
||||||
for await (const msg of sub) {
|
|
||||||
log_trace(`Received message on ${msg.subject}`);
|
|
||||||
|
|
||||||
// Use NATSBridge.smartreceive to handle the data
|
|
||||||
const result = await smartreceive(
|
|
||||||
msg,
|
|
||||||
{
|
|
||||||
maxRetries: 5,
|
|
||||||
baseDelay: 100,
|
|
||||||
maxDelay: 5000
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Result is an envelope dictionary with payloads field
|
|
||||||
// Access payloads with result.payloads
|
|
||||||
for (const { dataname, data, type } of result.payloads) {
|
|
||||||
if (data instanceof Uint8Array || Array.isArray(data)) {
|
|
||||||
const file_size = data.length;
|
|
||||||
log_trace(`Received ${file_size} bytes of binary data for '${dataname}' of type ${type}`);
|
|
||||||
|
|
||||||
// Save received data to a test file
|
|
||||||
const fs = require('fs');
|
|
||||||
const output_path = `./new_${dataname}`;
|
|
||||||
fs.writeFileSync(output_path, Buffer.from(data));
|
|
||||||
log_trace(`Saved received data to ${output_path}`);
|
|
||||||
} else {
|
|
||||||
log_trace(`Received unexpected data type for '${dataname}': ${typeof data}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep listening for 10 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
nc.close();
|
|
||||||
process.exit(0);
|
|
||||||
}, 120000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the test
|
|
||||||
console.log("Starting large binary payload test...");
|
|
||||||
|
|
||||||
// Run receiver
|
|
||||||
console.log("testing smartreceive");
|
|
||||||
test_large_binary_receive();
|
|
||||||
|
|
||||||
console.log("Test completed.");
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
// Test script for large payload testing using binary transport
|
|
||||||
// Tests sending a large file (> 1MB) via smartsend with binary type
|
|
||||||
|
|
||||||
const { smartsend, uuid4, log_trace } = require('./src/NATSBridge');
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
const SUBJECT = "/NATSBridge_test";
|
|
||||||
const NATS_URL = "nats.yiem.cc";
|
|
||||||
const FILESERVER_URL = "http://192.168.88.104:8080";
|
|
||||||
|
|
||||||
// Create correlation ID for tracing
|
|
||||||
const correlation_id = uuid4();
|
|
||||||
|
|
||||||
// Helper: Log with correlation ID
|
|
||||||
function log_trace(message) {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
console.log(`[${timestamp}] [Correlation: ${correlation_id}] ${message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// File upload handler for plik server
|
|
||||||
async function plik_upload_handler(fileserver_url, dataname, data, correlation_id) {
|
|
||||||
log_trace(correlation_id, `Uploading ${dataname} to fileserver: ${fileserver_url}`);
|
|
||||||
|
|
||||||
// Step 1: Get upload ID and token
|
|
||||||
const url_getUploadID = `${fileserver_url}/upload`;
|
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
};
|
|
||||||
const body = JSON.stringify({ OneShot: true });
|
|
||||||
|
|
||||||
let response = await fetch(url_getUploadID, {
|
|
||||||
method: "POST",
|
|
||||||
headers: headers,
|
|
||||||
body: body
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to get upload ID: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseJson = await response.json();
|
|
||||||
const uploadid = responseJson.id;
|
|
||||||
const uploadtoken = responseJson.uploadToken;
|
|
||||||
|
|
||||||
// Step 2: Upload file data
|
|
||||||
const url_upload = `${fileserver_url}/file/${uploadid}`;
|
|
||||||
|
|
||||||
// Create multipart form data
|
|
||||||
const formData = new FormData();
|
|
||||||
const blob = new Blob([data], { type: "application/octet-stream" });
|
|
||||||
formData.append("file", blob, dataname);
|
|
||||||
|
|
||||||
response = await fetch(url_upload, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"X-UploadToken": uploadtoken
|
|
||||||
},
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to upload file: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileResponseJson = await response.json();
|
|
||||||
const fileid = fileResponseJson.id;
|
|
||||||
|
|
||||||
// Build the download URL
|
|
||||||
const url = `${fileserver_url}/file/${uploadid}/${fileid}/${encodeURIComponent(dataname)}`;
|
|
||||||
|
|
||||||
log_trace(correlation_id, `Uploaded to URL: ${url}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: response.status,
|
|
||||||
uploadid: uploadid,
|
|
||||||
fileid: fileid,
|
|
||||||
url: url
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sender: Send large binary file via smartsend
|
|
||||||
async function test_large_binary_send() {
|
|
||||||
// Read the large file as binary data
|
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
// Test data 1
|
|
||||||
const file_path1 = './testFile_large.zip';
|
|
||||||
const file_data1 = fs.readFileSync(file_path1);
|
|
||||||
const filename1 = 'testFile_large.zip';
|
|
||||||
const data1 = { dataname: filename1, data: file_data1, type: "binary" };
|
|
||||||
|
|
||||||
// Test data 2
|
|
||||||
const file_path2 = './testFile_small.zip';
|
|
||||||
const file_data2 = fs.readFileSync(file_path2);
|
|
||||||
const filename2 = 'testFile_small.zip';
|
|
||||||
const data2 = { dataname: filename2, data: file_data2, type: "binary" };
|
|
||||||
|
|
||||||
// Use smartsend with binary type - will automatically use link transport
|
|
||||||
// if file size exceeds the threshold (1MB by default)
|
|
||||||
const env = await smartsend(
|
|
||||||
SUBJECT,
|
|
||||||
[data1, data2],
|
|
||||||
{
|
|
||||||
natsUrl: NATS_URL,
|
|
||||||
fileserverUrl: FILESERVER_URL,
|
|
||||||
fileserverUploadHandler: plik_upload_handler,
|
|
||||||
sizeThreshold: 1_000_000,
|
|
||||||
correlationId: correlation_id,
|
|
||||||
msgPurpose: "chat",
|
|
||||||
senderName: "sender",
|
|
||||||
receiverName: "",
|
|
||||||
receiverId: "",
|
|
||||||
replyTo: "",
|
|
||||||
replyToMsgId: ""
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
log_trace(`Sent message with transport: ${env.payloads[0].transport}`);
|
|
||||||
log_trace(`Envelope type: ${env.payloads[0].type}`);
|
|
||||||
|
|
||||||
// Check if link transport was used
|
|
||||||
if (env.payloads[0].transport === "link") {
|
|
||||||
log_trace("Using link transport - file uploaded to HTTP server");
|
|
||||||
log_trace(`URL: ${env.payloads[0].data}`);
|
|
||||||
} else {
|
|
||||||
log_trace("Using direct transport - payload sent via NATS");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the test
|
|
||||||
console.log("Starting large binary payload test...");
|
|
||||||
console.log(`Correlation ID: ${correlation_id}`);
|
|
||||||
|
|
||||||
// Run sender first
|
|
||||||
console.log("start smartsend");
|
|
||||||
test_large_binary_send();
|
|
||||||
|
|
||||||
// Run receiver
|
|
||||||
// console.log("testing smartreceive");
|
|
||||||
// test_large_binary_receive();
|
|
||||||
|
|
||||||
console.log("Test completed.");
|
|
||||||
@@ -1,276 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
// Test script for mixed-content message testing
|
|
||||||
// Tests sending a mix of text, json, table, image, audio, video, and binary data
|
|
||||||
// from JavaScript serviceA to JavaScript serviceB using NATSBridge.js smartsend
|
|
||||||
//
|
|
||||||
// This test demonstrates that any combination and any number of mixed content
|
|
||||||
// can be sent and received correctly.
|
|
||||||
|
|
||||||
const { smartsend, uuid4, log_trace, _serialize_data } = require('./src/NATSBridge');
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
const SUBJECT = "/NATSBridge_mix_test";
|
|
||||||
const NATS_URL = "nats.yiem.cc";
|
|
||||||
const FILESERVER_URL = "http://192.168.88.104:8080";
|
|
||||||
|
|
||||||
// Create correlation ID for tracing
|
|
||||||
const correlation_id = uuid4();
|
|
||||||
|
|
||||||
// Helper: Log with correlation ID
|
|
||||||
function log_trace(message) {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
console.log(`[${timestamp}] [Correlation: ${correlation_id}] ${message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// File upload handler for plik server
|
|
||||||
async function plik_upload_handler(fileserver_url, dataname, data, correlation_id) {
|
|
||||||
log_trace(correlation_id, `Uploading ${dataname} to fileserver: ${fileserver_url}`);
|
|
||||||
|
|
||||||
// Step 1: Get upload ID and token
|
|
||||||
const url_getUploadID = `${fileserver_url}/upload`;
|
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
};
|
|
||||||
const body = JSON.stringify({ OneShot: true });
|
|
||||||
|
|
||||||
let response = await fetch(url_getUploadID, {
|
|
||||||
method: "POST",
|
|
||||||
headers: headers,
|
|
||||||
body: body
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to get upload ID: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseJson = await response.json();
|
|
||||||
const uploadid = responseJson.id;
|
|
||||||
const uploadtoken = responseJson.uploadToken;
|
|
||||||
|
|
||||||
// Step 2: Upload file data
|
|
||||||
const url_upload = `${fileserver_url}/file/${uploadid}`;
|
|
||||||
|
|
||||||
// Create multipart form data
|
|
||||||
const formData = new FormData();
|
|
||||||
const blob = new Blob([data], { type: "application/octet-stream" });
|
|
||||||
formData.append("file", blob, dataname);
|
|
||||||
|
|
||||||
response = await fetch(url_upload, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"X-UploadToken": uploadtoken
|
|
||||||
},
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to upload file: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileResponseJson = await response.json();
|
|
||||||
const fileid = fileResponseJson.id;
|
|
||||||
|
|
||||||
// Build the download URL
|
|
||||||
const url = `${fileserver_url}/file/${uploadid}/${fileid}/${encodeURIComponent(dataname)}`;
|
|
||||||
|
|
||||||
log_trace(correlation_id, `Uploaded to URL: ${url}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: response.status,
|
|
||||||
uploadid: uploadid,
|
|
||||||
fileid: fileid,
|
|
||||||
url: url
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: Create sample data for each type
|
|
||||||
function create_sample_data() {
|
|
||||||
// Text data (small - direct transport)
|
|
||||||
const text_data = "Hello! This is a test chat message. 🎉\nHow are you doing today? 😊";
|
|
||||||
|
|
||||||
// Dictionary/JSON data (medium - could be direct or link)
|
|
||||||
const dict_data = {
|
|
||||||
type: "chat",
|
|
||||||
sender: "serviceA",
|
|
||||||
receiver: "serviceB",
|
|
||||||
metadata: {
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
priority: "high",
|
|
||||||
tags: ["urgent", "chat", "test"]
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
text: "This is a JSON-formatted chat message with nested structure.",
|
|
||||||
format: "markdown",
|
|
||||||
mentions: ["user1", "user2"]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Table data (small - direct transport) - NOT IMPLEMENTED (requires apache-arrow)
|
|
||||||
// const table_data_small = {...};
|
|
||||||
|
|
||||||
// Table data (large - link transport) - NOT IMPLEMENTED (requires apache-arrow)
|
|
||||||
// const table_data_large = {...};
|
|
||||||
|
|
||||||
// Image data (small binary - direct transport)
|
|
||||||
// Create a simple 10x10 pixel PNG-like data
|
|
||||||
const image_width = 10;
|
|
||||||
const image_height = 10;
|
|
||||||
let image_data = new Uint8Array(128); // PNG header + pixel data
|
|
||||||
// PNG header
|
|
||||||
image_data[0] = 0x89;
|
|
||||||
image_data[1] = 0x50;
|
|
||||||
image_data[2] = 0x4E;
|
|
||||||
image_data[3] = 0x47;
|
|
||||||
image_data[4] = 0x0D;
|
|
||||||
image_data[5] = 0x0A;
|
|
||||||
image_data[6] = 0x1A;
|
|
||||||
image_data[7] = 0x0A;
|
|
||||||
// Simple RGB data (10*10*3 = 300 bytes)
|
|
||||||
for (let i = 0; i < 300; i++) {
|
|
||||||
image_data[i + 8] = 0xFF; // Red pixel
|
|
||||||
}
|
|
||||||
|
|
||||||
// Image data (large - link transport)
|
|
||||||
const large_image_width = 500;
|
|
||||||
const large_image_height = 1000;
|
|
||||||
const large_image_data = new Uint8Array(large_image_width * large_image_height * 3 + 8);
|
|
||||||
// PNG header
|
|
||||||
large_image_data[0] = 0x89;
|
|
||||||
large_image_data[1] = 0x50;
|
|
||||||
large_image_data[2] = 0x4E;
|
|
||||||
large_image_data[3] = 0x47;
|
|
||||||
large_image_data[4] = 0x0D;
|
|
||||||
large_image_data[5] = 0x0A;
|
|
||||||
large_image_data[6] = 0x1A;
|
|
||||||
large_image_data[7] = 0x0A;
|
|
||||||
// Random RGB data
|
|
||||||
for (let i = 0; i < large_image_width * large_image_height * 3; i++) {
|
|
||||||
large_image_data[i + 8] = Math.floor(Math.random() * 255);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Audio data (small binary - direct transport)
|
|
||||||
const audio_data = new Uint8Array(100);
|
|
||||||
for (let i = 0; i < 100; i++) {
|
|
||||||
audio_data[i] = Math.floor(Math.random() * 255);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Audio data (large - link transport)
|
|
||||||
const large_audio_data = new Uint8Array(1_500_000);
|
|
||||||
for (let i = 0; i < 1_500_000; i++) {
|
|
||||||
large_audio_data[i] = Math.floor(Math.random() * 255);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Video data (small binary - direct transport)
|
|
||||||
const video_data = new Uint8Array(150);
|
|
||||||
for (let i = 0; i < 150; i++) {
|
|
||||||
video_data[i] = Math.floor(Math.random() * 255);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Video data (large - link transport)
|
|
||||||
const large_video_data = new Uint8Array(1_500_000);
|
|
||||||
for (let i = 0; i < 1_500_000; i++) {
|
|
||||||
large_video_data[i] = Math.floor(Math.random() * 255);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Binary data (small - direct transport)
|
|
||||||
const binary_data = new Uint8Array(200);
|
|
||||||
for (let i = 0; i < 200; i++) {
|
|
||||||
binary_data[i] = Math.floor(Math.random() * 255);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Binary data (large - link transport)
|
|
||||||
const large_binary_data = new Uint8Array(1_500_000);
|
|
||||||
for (let i = 0; i < 1_500_000; i++) {
|
|
||||||
large_binary_data[i] = Math.floor(Math.random() * 255);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
text_data,
|
|
||||||
dict_data,
|
|
||||||
// table_data_small,
|
|
||||||
// table_data_large,
|
|
||||||
image_data,
|
|
||||||
large_image_data,
|
|
||||||
audio_data,
|
|
||||||
large_audio_data,
|
|
||||||
video_data,
|
|
||||||
large_video_data,
|
|
||||||
binary_data,
|
|
||||||
large_binary_data
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sender: Send mixed content via smartsend
|
|
||||||
async function test_mix_send() {
|
|
||||||
// Create sample data
|
|
||||||
const { text_data, dict_data, image_data, large_image_data, audio_data, large_audio_data, video_data, large_video_data, binary_data, large_binary_data } = create_sample_data();
|
|
||||||
|
|
||||||
// Create payloads list - mixed content with both small and large data
|
|
||||||
// Small data uses direct transport, large data uses link transport
|
|
||||||
const payloads = [
|
|
||||||
// Small data (direct transport) - text, dictionary
|
|
||||||
{ dataname: "chat_text", data: text_data, type: "text" },
|
|
||||||
{ dataname: "chat_json", data: dict_data, type: "dictionary" },
|
|
||||||
// { dataname: "chat_table_small", data: table_data_small, type: "table" },
|
|
||||||
|
|
||||||
// Large data (link transport) - large image, large audio, large video, large binary
|
|
||||||
// { dataname: "chat_table_large", data: table_data_large, type: "table" },
|
|
||||||
{ dataname: "user_image_large", data: large_image_data, type: "image" },
|
|
||||||
{ dataname: "audio_clip_large", data: large_audio_data, type: "audio" },
|
|
||||||
{ dataname: "video_clip_large", data: large_video_data, type: "video" },
|
|
||||||
{ dataname: "binary_file_large", data: large_binary_data, type: "binary" }
|
|
||||||
];
|
|
||||||
|
|
||||||
// Use smartsend with mixed content
|
|
||||||
const env = await smartsend(
|
|
||||||
SUBJECT,
|
|
||||||
payloads,
|
|
||||||
{
|
|
||||||
natsUrl: NATS_URL,
|
|
||||||
fileserverUrl: FILESERVER_URL,
|
|
||||||
fileserverUploadHandler: plik_upload_handler,
|
|
||||||
sizeThreshold: 1_000_000,
|
|
||||||
correlationId: correlation_id,
|
|
||||||
msgPurpose: "chat",
|
|
||||||
senderName: "mix_sender",
|
|
||||||
receiverName: "",
|
|
||||||
receiverId: "",
|
|
||||||
replyTo: "",
|
|
||||||
replyToMsgId: ""
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
log_trace(`Sent message with ${env.payloads.length} payloads`);
|
|
||||||
|
|
||||||
// Log transport type for each payload
|
|
||||||
for (let i = 0; i < env.payloads.length; i++) {
|
|
||||||
const payload = env.payloads[i];
|
|
||||||
log_trace(`Payload ${i + 1} ('${payload.dataname}'):`);
|
|
||||||
log_trace(` Transport: ${payload.transport}`);
|
|
||||||
log_trace(` Type: ${payload.type}`);
|
|
||||||
log_trace(` Size: ${payload.size} bytes`);
|
|
||||||
log_trace(` Encoding: ${payload.encoding}`);
|
|
||||||
|
|
||||||
if (payload.transport === "link") {
|
|
||||||
log_trace(` URL: ${payload.data}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Summary
|
|
||||||
console.log("\n--- Transport Summary ---");
|
|
||||||
const direct_count = env.payloads.filter(p => p.transport === "direct").length;
|
|
||||||
const link_count = env.payloads.filter(p => p.transport === "link").length;
|
|
||||||
log_trace(`Direct transport: ${direct_count} payloads`);
|
|
||||||
log_trace(`Link transport: ${link_count} payloads`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the test
|
|
||||||
console.log("Starting mixed-content transport test...");
|
|
||||||
console.log(`Correlation ID: ${correlation_id}`);
|
|
||||||
|
|
||||||
// Run sender
|
|
||||||
console.log("start smartsend for mixed content");
|
|
||||||
test_mix_send();
|
|
||||||
|
|
||||||
console.log("\nTest completed.");
|
|
||||||
console.log("Note: Run test_js_to_js_mix_receiver.js to receive the messages.");
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
// Test script for mixed-content message testing
|
|
||||||
// Tests receiving a mix of text, json, table, image, audio, video, and binary data
|
|
||||||
// from JavaScript serviceA to JavaScript serviceB using NATSBridge.js smartreceive
|
|
||||||
//
|
|
||||||
// This test demonstrates that any combination and any number of mixed content
|
|
||||||
// can be sent and received correctly.
|
|
||||||
|
|
||||||
const { smartreceive, log_trace } = require('./src/NATSBridge');
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
const SUBJECT = "/NATSBridge_mix_test";
|
|
||||||
const NATS_URL = "nats.yiem.cc";
|
|
||||||
|
|
||||||
// Helper: Log with correlation ID
|
|
||||||
function log_trace(message) {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
console.log(`[${timestamp}] ${message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Receiver: Listen for messages and verify mixed content handling
|
|
||||||
async function test_mix_receive() {
|
|
||||||
// Connect to NATS
|
|
||||||
const { connect } = require('nats');
|
|
||||||
const nc = await connect({ servers: [NATS_URL] });
|
|
||||||
|
|
||||||
// Subscribe to the subject
|
|
||||||
const sub = nc.subscribe(SUBJECT);
|
|
||||||
|
|
||||||
for await (const msg of sub) {
|
|
||||||
log_trace(`Received message on ${msg.subject}`);
|
|
||||||
|
|
||||||
// Use NATSBridge.smartreceive to handle the data
|
|
||||||
const result = await smartreceive(
|
|
||||||
msg,
|
|
||||||
{
|
|
||||||
maxRetries: 5,
|
|
||||||
baseDelay: 100,
|
|
||||||
maxDelay: 5000
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
log_trace(`Received ${result.payloads.length} payloads`);
|
|
||||||
|
|
||||||
// Result is an envelope dictionary with payloads field
|
|
||||||
// Access payloads with result.payloads
|
|
||||||
for (const { dataname, data, type } of result.payloads) {
|
|
||||||
log_trace(`\n=== Payload: ${dataname} (type: ${type}) ===`);
|
|
||||||
|
|
||||||
// Handle different data types
|
|
||||||
if (type === "text") {
|
|
||||||
// Text data - should be a String
|
|
||||||
if (typeof data === 'string') {
|
|
||||||
log_trace(` Type: String`);
|
|
||||||
log_trace(` Length: ${data.length} characters`);
|
|
||||||
|
|
||||||
// Display first 200 characters
|
|
||||||
if (data.length > 200) {
|
|
||||||
log_trace(` First 200 chars: ${data.substring(0, 200)}...`);
|
|
||||||
} else {
|
|
||||||
log_trace(` Content: ${data}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save to file
|
|
||||||
const fs = require('fs');
|
|
||||||
const output_path = `./received_${dataname}.txt`;
|
|
||||||
fs.writeFileSync(output_path, data);
|
|
||||||
log_trace(` Saved to: ${output_path}`);
|
|
||||||
} else {
|
|
||||||
log_trace(` ERROR: Expected String, got ${typeof data}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (type === "dictionary") {
|
|
||||||
// Dictionary data - should be an object
|
|
||||||
if (typeof data === 'object' && data !== null && !Array.isArray(data)) {
|
|
||||||
log_trace(` Type: Object`);
|
|
||||||
log_trace(` Keys: ${Object.keys(data).join(', ')}`);
|
|
||||||
|
|
||||||
// Display nested content
|
|
||||||
for (const [key, value] of Object.entries(data)) {
|
|
||||||
log_trace(` ${key} => ${value}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save to JSON file
|
|
||||||
const fs = require('fs');
|
|
||||||
const output_path = `./received_${dataname}.json`;
|
|
||||||
const json_str = JSON.stringify(data, null, 2);
|
|
||||||
fs.writeFileSync(output_path, json_str);
|
|
||||||
log_trace(` Saved to: ${output_path}`);
|
|
||||||
} else {
|
|
||||||
log_trace(` ERROR: Expected Object, got ${typeof data}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (type === "table") {
|
|
||||||
// Table data - should be an array of objects (requires apache-arrow)
|
|
||||||
log_trace(` Type: Array (requires apache-arrow for full deserialization)`);
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
log_trace(` Length: ${data.length} items`);
|
|
||||||
log_trace(` First item: ${JSON.stringify(data[0])}`);
|
|
||||||
} else {
|
|
||||||
log_trace(` ERROR: Expected Array, got ${typeof data}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (type === "image" || type === "audio" || type === "video" || type === "binary") {
|
|
||||||
// Binary data - should be Uint8Array
|
|
||||||
if (data instanceof Uint8Array || Array.isArray(data)) {
|
|
||||||
log_trace(` Type: Uint8Array (binary)`);
|
|
||||||
log_trace(` Size: ${data.length} bytes`);
|
|
||||||
|
|
||||||
// Save to file
|
|
||||||
const fs = require('fs');
|
|
||||||
const output_path = `./received_${dataname}.bin`;
|
|
||||||
fs.writeFileSync(output_path, Buffer.from(data));
|
|
||||||
log_trace(` Saved to: ${output_path}`);
|
|
||||||
} else {
|
|
||||||
log_trace(` ERROR: Expected Uint8Array, got ${typeof data}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
log_trace(` ERROR: Unknown data type '${type}'`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Summary
|
|
||||||
console.log("\n=== Verification Summary ===");
|
|
||||||
const text_count = result.payloads.filter(x => x.type === "text").length;
|
|
||||||
const dict_count = result.payloads.filter(x => x.type === "dictionary").length;
|
|
||||||
const table_count = result.payloads.filter(x => x.type === "table").length;
|
|
||||||
const image_count = result.payloads.filter(x => x.type === "image").length;
|
|
||||||
const audio_count = result.payloads.filter(x => x.type === "audio").length;
|
|
||||||
const video_count = result.payloads.filter(x => x.type === "video").length;
|
|
||||||
const binary_count = result.payloads.filter(x => x.type === "binary").length;
|
|
||||||
|
|
||||||
log_trace(`Text payloads: ${text_count}`);
|
|
||||||
log_trace(`Dictionary payloads: ${dict_count}`);
|
|
||||||
log_trace(`Table payloads: ${table_count}`);
|
|
||||||
log_trace(`Image payloads: ${image_count}`);
|
|
||||||
log_trace(`Audio payloads: ${audio_count}`);
|
|
||||||
log_trace(`Video payloads: ${video_count}`);
|
|
||||||
log_trace(`Binary payloads: ${binary_count}`);
|
|
||||||
|
|
||||||
// Print transport type info for each payload if available
|
|
||||||
console.log("\n=== Payload Details ===");
|
|
||||||
for (const { dataname, data, type } of result.payloads) {
|
|
||||||
if (["image", "audio", "video", "binary"].includes(type)) {
|
|
||||||
log_trace(`${dataname}: ${data.length} bytes (binary)`);
|
|
||||||
} else if (type === "table") {
|
|
||||||
log_trace(`${dataname}: ${data.length} items (Array)`);
|
|
||||||
} else if (type === "dictionary") {
|
|
||||||
log_trace(`${dataname}: ${JSON.stringify(data).length} bytes (Object)`);
|
|
||||||
} else if (type === "text") {
|
|
||||||
log_trace(`${dataname}: ${data.length} characters (String)`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep listening for 2 minutes
|
|
||||||
setTimeout(() => {
|
|
||||||
nc.close();
|
|
||||||
process.exit(0);
|
|
||||||
}, 120000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the test
|
|
||||||
console.log("Starting mixed-content transport test...");
|
|
||||||
console.log("Note: This receiver will wait for messages from the sender.");
|
|
||||||
console.log("Run test_js_to_js_mix_sender.js first to send test data.");
|
|
||||||
|
|
||||||
// Run receiver
|
|
||||||
console.log("\ntesting smartreceive for mixed content");
|
|
||||||
test_mix_receive();
|
|
||||||
|
|
||||||
console.log("\nTest completed.");
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
// Test script for Table transport testing
|
|
||||||
// Tests receiving 1 large and 1 small Tables via direct and link transport
|
|
||||||
// Uses NATSBridge.js smartreceive with "table" type
|
|
||||||
//
|
|
||||||
// Note: This test requires the apache-arrow library to deserialize table data.
|
|
||||||
// The JavaScript implementation uses apache-arrow for Arrow IPC deserialization.
|
|
||||||
|
|
||||||
const { smartreceive, log_trace } = require('./src/NATSBridge');
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
const SUBJECT = "/NATSBridge_table_test";
|
|
||||||
const NATS_URL = "nats.yiem.cc";
|
|
||||||
|
|
||||||
// Helper: Log with correlation ID
|
|
||||||
function log_trace(message) {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
console.log(`[${timestamp}] ${message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Receiver: Listen for messages and verify Table handling
|
|
||||||
async function test_table_receive() {
|
|
||||||
// Connect to NATS
|
|
||||||
const { connect } = require('nats');
|
|
||||||
const nc = await connect({ servers: [NATS_URL] });
|
|
||||||
|
|
||||||
// Subscribe to the subject
|
|
||||||
const sub = nc.subscribe(SUBJECT);
|
|
||||||
|
|
||||||
for await (const msg of sub) {
|
|
||||||
log_trace(`Received message on ${msg.subject}`);
|
|
||||||
|
|
||||||
// Use NATSBridge.smartreceive to handle the data
|
|
||||||
const result = await smartreceive(
|
|
||||||
msg,
|
|
||||||
{
|
|
||||||
maxRetries: 5,
|
|
||||||
baseDelay: 100,
|
|
||||||
maxDelay: 5000
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Result is an envelope dictionary with payloads field
|
|
||||||
// Access payloads with result.payloads
|
|
||||||
for (const { dataname, data, type } of result.payloads) {
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
log_trace(`Received Table '${dataname}' of type ${type}`);
|
|
||||||
|
|
||||||
// Display table contents
|
|
||||||
console.log(` Dimensions: ${data.length} rows x ${data.length > 0 ? Object.keys(data[0]).length : 0} columns`);
|
|
||||||
console.log(` Columns: ${data.length > 0 ? Object.keys(data[0]).join(', ') : ''}`);
|
|
||||||
|
|
||||||
// Display first few rows
|
|
||||||
console.log(` First 5 rows:`);
|
|
||||||
for (let i = 0; i < Math.min(5, data.length); i++) {
|
|
||||||
console.log(` Row ${i}: ${JSON.stringify(data[i])}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save to JSON file
|
|
||||||
const fs = require('fs');
|
|
||||||
const output_path = `./received_${dataname}.json`;
|
|
||||||
const json_str = JSON.stringify(data, null, 2);
|
|
||||||
fs.writeFileSync(output_path, json_str);
|
|
||||||
log_trace(`Saved Table to ${output_path}`);
|
|
||||||
} else {
|
|
||||||
log_trace(`Received unexpected data type for '${dataname}': ${typeof data}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep listening for 10 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
nc.close();
|
|
||||||
process.exit(0);
|
|
||||||
}, 120000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the test
|
|
||||||
console.log("Starting Table transport test...");
|
|
||||||
console.log("Note: This receiver will wait for messages from the sender.");
|
|
||||||
console.log("Run test_js_to_js_table_sender.js first to send test data.");
|
|
||||||
|
|
||||||
// Run receiver
|
|
||||||
console.log("testing smartreceive");
|
|
||||||
test_table_receive();
|
|
||||||
|
|
||||||
console.log("Test completed.");
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
// Test script for Table transport testing
|
|
||||||
// Tests sending 1 large and 1 small Tables via direct and link transport
|
|
||||||
// Uses NATSBridge.js smartsend with "table" type
|
|
||||||
//
|
|
||||||
// Note: This test requires the apache-arrow library to serialize/deserialize table data.
|
|
||||||
// The JavaScript implementation uses apache-arrow for Arrow IPC serialization.
|
|
||||||
|
|
||||||
const { smartsend, uuid4, log_trace } = require('./src/NATSBridge');
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
const SUBJECT = "/NATSBridge_table_test";
|
|
||||||
const NATS_URL = "nats.yiem.cc";
|
|
||||||
const FILESERVER_URL = "http://192.168.88.104:8080";
|
|
||||||
|
|
||||||
// Create correlation ID for tracing
|
|
||||||
const correlation_id = uuid4();
|
|
||||||
|
|
||||||
// Helper: Log with correlation ID
|
|
||||||
function log_trace(message) {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
console.log(`[${timestamp}] [Correlation: ${correlation_id}] ${message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// File upload handler for plik server
|
|
||||||
async function plik_upload_handler(fileserver_url, dataname, data, correlation_id) {
|
|
||||||
log_trace(correlation_id, `Uploading ${dataname} to fileserver: ${fileserver_url}`);
|
|
||||||
|
|
||||||
// Step 1: Get upload ID and token
|
|
||||||
const url_getUploadID = `${fileserver_url}/upload`;
|
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
};
|
|
||||||
const body = JSON.stringify({ OneShot: true });
|
|
||||||
|
|
||||||
let response = await fetch(url_getUploadID, {
|
|
||||||
method: "POST",
|
|
||||||
headers: headers,
|
|
||||||
body: body
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to get upload ID: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseJson = await response.json();
|
|
||||||
const uploadid = responseJson.id;
|
|
||||||
const uploadtoken = responseJson.uploadToken;
|
|
||||||
|
|
||||||
// Step 2: Upload file data
|
|
||||||
const url_upload = `${fileserver_url}/file/${uploadid}`;
|
|
||||||
|
|
||||||
// Create multipart form data
|
|
||||||
const formData = new FormData();
|
|
||||||
const blob = new Blob([data], { type: "application/octet-stream" });
|
|
||||||
formData.append("file", blob, dataname);
|
|
||||||
|
|
||||||
response = await fetch(url_upload, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"X-UploadToken": uploadtoken
|
|
||||||
},
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to upload file: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileResponseJson = await response.json();
|
|
||||||
const fileid = fileResponseJson.id;
|
|
||||||
|
|
||||||
// Build the download URL
|
|
||||||
const url = `${fileserver_url}/file/${uploadid}/${fileid}/${encodeURIComponent(dataname)}`;
|
|
||||||
|
|
||||||
log_trace(correlation_id, `Uploaded to URL: ${url}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: response.status,
|
|
||||||
uploadid: uploadid,
|
|
||||||
fileid: fileid,
|
|
||||||
url: url
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sender: Send Tables via smartsend
|
|
||||||
async function test_table_send() {
|
|
||||||
// Note: This test requires apache-arrow library to create Arrow IPC data.
|
|
||||||
// For now, we'll use a simple array of objects as table data.
|
|
||||||
// In production, you would use the apache-arrow library to create Arrow IPC data.
|
|
||||||
|
|
||||||
// Create a small Table (will use direct transport)
|
|
||||||
const small_table = [
|
|
||||||
{ id: 1, name: "Alice", score: 95 },
|
|
||||||
{ id: 2, name: "Bob", score: 88 },
|
|
||||||
{ id: 3, name: "Charlie", score: 92 }
|
|
||||||
];
|
|
||||||
|
|
||||||
// Create a large Table (will use link transport if > 1MB)
|
|
||||||
// Generate a larger dataset (~2MB to ensure link transport)
|
|
||||||
const large_table = [];
|
|
||||||
for (let i = 0; i < 50000; i++) {
|
|
||||||
large_table.push({
|
|
||||||
id: i,
|
|
||||||
message: `msg_${i}`,
|
|
||||||
sender: `sender_${i}`,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
priority: Math.floor(Math.random() * 3) + 1
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test data 1: small Table
|
|
||||||
const data1 = { dataname: "small_table", data: small_table, type: "table" };
|
|
||||||
|
|
||||||
// Test data 2: large Table
|
|
||||||
const data2 = { dataname: "large_table", data: large_table, type: "table" };
|
|
||||||
|
|
||||||
// Use smartsend with table type
|
|
||||||
// For small Table: will use direct transport (Arrow IPC encoded)
|
|
||||||
// For large Table: will use link transport (uploaded to fileserver)
|
|
||||||
const env = await smartsend(
|
|
||||||
SUBJECT,
|
|
||||||
[data1, data2],
|
|
||||||
{
|
|
||||||
natsUrl: NATS_URL,
|
|
||||||
fileserverUrl: FILESERVER_URL,
|
|
||||||
fileserverUploadHandler: plik_upload_handler,
|
|
||||||
sizeThreshold: 1_000_000,
|
|
||||||
correlationId: correlation_id,
|
|
||||||
msgPurpose: "chat",
|
|
||||||
senderName: "table_sender",
|
|
||||||
receiverName: "",
|
|
||||||
receiverId: "",
|
|
||||||
replyTo: "",
|
|
||||||
replyToMsgId: ""
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
log_trace(`Sent message with ${env.payloads.length} payloads`);
|
|
||||||
|
|
||||||
// Log transport type for each payload
|
|
||||||
for (let i = 0; i < env.payloads.length; i++) {
|
|
||||||
const payload = env.payloads[i];
|
|
||||||
log_trace(`Payload ${i + 1} ('${payload.dataname}'):`);
|
|
||||||
log_trace(` Transport: ${payload.transport}`);
|
|
||||||
log_trace(` Type: ${payload.type}`);
|
|
||||||
log_trace(` Size: ${payload.size} bytes`);
|
|
||||||
log_trace(` Encoding: ${payload.encoding}`);
|
|
||||||
|
|
||||||
if (payload.transport === "link") {
|
|
||||||
log_trace(` URL: ${payload.data}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the test
|
|
||||||
console.log("Starting Table transport test...");
|
|
||||||
console.log(`Correlation ID: ${correlation_id}`);
|
|
||||||
|
|
||||||
// Run sender
|
|
||||||
console.log("start smartsend for tables");
|
|
||||||
test_table_send();
|
|
||||||
|
|
||||||
console.log("Test completed.");
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
// Test script for text transport testing
|
|
||||||
// Tests receiving 1 large and 1 small text from JavaScript serviceA to JavaScript serviceB
|
|
||||||
// Uses NATSBridge.js smartreceive with "text" type
|
|
||||||
|
|
||||||
const { smartreceive, log_trace } = require('./src/NATSBridge');
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
const SUBJECT = "/NATSBridge_text_test";
|
|
||||||
const NATS_URL = "nats.yiem.cc";
|
|
||||||
|
|
||||||
// Helper: Log with correlation ID
|
|
||||||
function log_trace(message) {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
console.log(`[${timestamp}] ${message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Receiver: Listen for messages and verify text handling
|
|
||||||
async function test_text_receive() {
|
|
||||||
// Connect to NATS
|
|
||||||
const { connect } = require('nats');
|
|
||||||
const nc = await connect({ servers: [NATS_URL] });
|
|
||||||
|
|
||||||
// Subscribe to the subject
|
|
||||||
const sub = nc.subscribe(SUBJECT);
|
|
||||||
|
|
||||||
for await (const msg of sub) {
|
|
||||||
log_trace(`Received message on ${msg.subject}`);
|
|
||||||
|
|
||||||
// Use NATSBridge.smartreceive to handle the data
|
|
||||||
const result = await smartreceive(
|
|
||||||
msg,
|
|
||||||
{
|
|
||||||
maxRetries: 5,
|
|
||||||
baseDelay: 100,
|
|
||||||
maxDelay: 5000
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Result is an envelope dictionary with payloads field
|
|
||||||
// Access payloads with result.payloads
|
|
||||||
for (const { dataname, data, type } of result.payloads) {
|
|
||||||
if (typeof data === 'string') {
|
|
||||||
log_trace(`Received text '${dataname}' of type ${type}`);
|
|
||||||
log_trace(` Length: ${data.length} characters`);
|
|
||||||
|
|
||||||
// Display first 100 characters
|
|
||||||
if (data.length > 100) {
|
|
||||||
log_trace(` First 100 characters: ${data.substring(0, 100)}...`);
|
|
||||||
} else {
|
|
||||||
log_trace(` Content: ${data}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save to file
|
|
||||||
const fs = require('fs');
|
|
||||||
const output_path = `./received_${dataname}.txt`;
|
|
||||||
fs.writeFileSync(output_path, data);
|
|
||||||
log_trace(`Saved text to ${output_path}`);
|
|
||||||
} else {
|
|
||||||
log_trace(`Received unexpected data type for '${dataname}': ${typeof data}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep listening for 10 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
nc.close();
|
|
||||||
process.exit(0);
|
|
||||||
}, 120000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the test
|
|
||||||
console.log("Starting text transport test...");
|
|
||||||
console.log("Note: This receiver will wait for messages from the sender.");
|
|
||||||
console.log("Run test_js_to_js_text_sender.js first to send test data.");
|
|
||||||
|
|
||||||
// Run receiver
|
|
||||||
console.log("testing smartreceive for text");
|
|
||||||
test_text_receive();
|
|
||||||
|
|
||||||
console.log("Test completed.");
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
// Test script for text transport testing
|
|
||||||
// Tests sending 1 large and 1 small text from JavaScript serviceA to JavaScript serviceB
|
|
||||||
// Uses NATSBridge.js smartsend with "text" type
|
|
||||||
|
|
||||||
const { smartsend, uuid4, log_trace } = require('./src/NATSBridge');
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
const SUBJECT = "/NATSBridge_text_test";
|
|
||||||
const NATS_URL = "nats.yiem.cc";
|
|
||||||
const FILESERVER_URL = "http://192.168.88.104:8080";
|
|
||||||
|
|
||||||
// Create correlation ID for tracing
|
|
||||||
const correlation_id = uuid4();
|
|
||||||
|
|
||||||
// Helper: Log with correlation ID
|
|
||||||
function log_trace(message) {
|
|
||||||
const timestamp = new Date().toISOString();
|
|
||||||
console.log(`[${timestamp}] [Correlation: ${correlation_id}] ${message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// File upload handler for plik server
|
|
||||||
async function plik_upload_handler(fileserver_url, dataname, data, correlation_id) {
|
|
||||||
// Get upload ID
|
|
||||||
const url_getUploadID = `${fileserver_url}/upload`;
|
|
||||||
const headers = {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
};
|
|
||||||
const body = JSON.stringify({ OneShot: true });
|
|
||||||
|
|
||||||
let response = await fetch(url_getUploadID, {
|
|
||||||
method: "POST",
|
|
||||||
headers: headers,
|
|
||||||
body: body
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to get upload ID: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseJson = await response.json();
|
|
||||||
const uploadid = responseJson.id;
|
|
||||||
const uploadtoken = responseJson.uploadToken;
|
|
||||||
|
|
||||||
// Upload file
|
|
||||||
const formData = new FormData();
|
|
||||||
const blob = new Blob([data], { type: "application/octet-stream" });
|
|
||||||
formData.append("file", blob, dataname);
|
|
||||||
|
|
||||||
response = await fetch(`${fileserver_url}/file/${uploadid}`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"X-UploadToken": uploadtoken
|
|
||||||
},
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to upload file: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileResponseJson = await response.json();
|
|
||||||
const fileid = fileResponseJson.id;
|
|
||||||
|
|
||||||
const url = `${fileserver_url}/file/${uploadid}/${fileid}/${encodeURIComponent(dataname)}`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: response.status,
|
|
||||||
uploadid: uploadid,
|
|
||||||
fileid: fileid,
|
|
||||||
url: url
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sender: Send text via smartsend
|
|
||||||
async function test_text_send() {
|
|
||||||
// Create a small text (will use direct transport)
|
|
||||||
const small_text = "Hello, this is a small text message. Testing direct transport via NATS.";
|
|
||||||
|
|
||||||
// Create a large text (will use link transport if > 1MB)
|
|
||||||
// Generate a larger text (~2MB to ensure link transport)
|
|
||||||
const large_text_lines = [];
|
|
||||||
for (let i = 0; i < 50000; i++) {
|
|
||||||
large_text_lines.push(`Line ${i}: This is a sample text line with some content to pad the size. `);
|
|
||||||
}
|
|
||||||
const large_text = large_text_lines.join("");
|
|
||||||
|
|
||||||
// Test data 1: small text
|
|
||||||
const data1 = { dataname: "small_text", data: small_text, type: "text" };
|
|
||||||
|
|
||||||
// Test data 2: large text
|
|
||||||
const data2 = { dataname: "large_text", data: large_text, type: "text" };
|
|
||||||
|
|
||||||
// Use smartsend with text type
|
|
||||||
// For small text: will use direct transport (Base64 encoded UTF-8)
|
|
||||||
// For large text: will use link transport (uploaded to fileserver)
|
|
||||||
const env = await smartsend(
|
|
||||||
SUBJECT,
|
|
||||||
[data1, data2],
|
|
||||||
{
|
|
||||||
natsUrl: NATS_URL,
|
|
||||||
fileserverUrl: FILESERVER_URL,
|
|
||||||
fileserverUploadHandler: plik_upload_handler,
|
|
||||||
sizeThreshold: 1_000_000,
|
|
||||||
correlationId: correlation_id,
|
|
||||||
msgPurpose: "chat",
|
|
||||||
senderName: "text_sender",
|
|
||||||
receiverName: "",
|
|
||||||
receiverId: "",
|
|
||||||
replyTo: "",
|
|
||||||
replyToMsgId: ""
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
log_trace(`Sent message with ${env.payloads.length} payloads`);
|
|
||||||
|
|
||||||
// Log transport type for each payload
|
|
||||||
for (let i = 0; i < env.payloads.length; i++) {
|
|
||||||
const payload = env.payloads[i];
|
|
||||||
log_trace(`Payload ${i + 1} ('${payload.dataname}'):`);
|
|
||||||
log_trace(` Transport: ${payload.transport}`);
|
|
||||||
log_trace(` Type: ${payload.type}`);
|
|
||||||
log_trace(` Size: ${payload.size} bytes`);
|
|
||||||
log_trace(` Encoding: ${payload.encoding}`);
|
|
||||||
|
|
||||||
if (payload.transport === "link") {
|
|
||||||
log_trace(` URL: ${payload.data}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the test
|
|
||||||
console.log("Starting text transport test...");
|
|
||||||
console.log(`Correlation ID: ${correlation_id}`);
|
|
||||||
|
|
||||||
// Run sender
|
|
||||||
console.log("start smartsend for text");
|
|
||||||
test_text_send();
|
|
||||||
|
|
||||||
console.log("Test completed.");
|
|
||||||
@@ -13,7 +13,7 @@ include("../src/NATSBridge.jl")
|
|||||||
using .NATSBridge
|
using .NATSBridge
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
const SUBJECT = "/NATSBridge_mix_test"
|
const SUBJECT = "/natsbridge"
|
||||||
const NATS_URL = "nats.yiem.cc"
|
const NATS_URL = "nats.yiem.cc"
|
||||||
const FILESERVER_URL = "http://192.168.88.104:8080"
|
const FILESERVER_URL = "http://192.168.88.104:8080"
|
||||||
|
|
||||||
@@ -93,26 +93,41 @@ function test_mix_receive()
|
|||||||
log_trace(" ERROR: Expected Dict, got $(typeof(data))")
|
log_trace(" ERROR: Expected Dict, got $(typeof(data))")
|
||||||
end
|
end
|
||||||
|
|
||||||
elseif data_type == "table"
|
elseif data_type == "arrowtable"
|
||||||
# Table data - should be a DataFrame
|
# Arrow table data - should be Arrow.Table
|
||||||
data = DataFrame(data)
|
if isa(data, Arrow.Table)
|
||||||
if isa(data, DataFrame)
|
log_trace(" Type: Arrow.Table")
|
||||||
log_trace(" Type: DataFrame")
|
|
||||||
log_trace(" Dimensions: $(size(data, 1)) rows x $(size(data, 2)) columns")
|
|
||||||
log_trace(" Columns: $(names(data))")
|
|
||||||
|
|
||||||
# Display first few rows
|
# Convert to DataFrame for display and save
|
||||||
log_trace(" First 5 rows:")
|
df = DataFrame(data)
|
||||||
display(data[1:min(5, size(data, 1)), :])
|
@show df[1:3, :]
|
||||||
|
|
||||||
# Save to Arrow file
|
|
||||||
output_path = "./received_$dataname.arrow"
|
output_path = "./received_$dataname.arrow"
|
||||||
io = IOBuffer()
|
io = IOBuffer()
|
||||||
Arrow.write(io, data)
|
Arrow.write(io, data)
|
||||||
write(output_path, take!(io))
|
write(output_path, take!(io))
|
||||||
log_trace(" Saved to: $output_path")
|
log_trace(" Saved to: $output_path")
|
||||||
else
|
else
|
||||||
log_trace(" ERROR: Expected DataFrame, got $(typeof(data))")
|
log_trace(" ERROR: Expected Arrow.Table, got $(typeof(data))")
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif data_type == "jsontable"
|
||||||
|
# JSON table data - should be Vector{Dict} or Vector{NamedTuple}
|
||||||
|
@show "jsontable" typeof(data)
|
||||||
|
if isa(data, Vector{Any})
|
||||||
|
log_trace(" Type: Vector{Dict/NamedTuple}")
|
||||||
|
|
||||||
|
# Convert to DataFrame for display and save
|
||||||
|
df = DataFrame(data)
|
||||||
|
@show df[1:3, :]
|
||||||
|
log_trace(" Converted to DataFrame: $(size(df, 1)) rows x $(size(df, 2)) columns")
|
||||||
|
|
||||||
|
# Save as JSON file
|
||||||
|
output_path = "./received_$dataname.json"
|
||||||
|
json_str = JSON.json(data, 2)
|
||||||
|
write(output_path, json_str)
|
||||||
|
log_trace(" Saved to: $output_path")
|
||||||
|
else
|
||||||
|
log_trace(" ERROR: Expected Vector{Dict/NamedTuple}, got $(typeof(data))")
|
||||||
end
|
end
|
||||||
|
|
||||||
elseif data_type == "image"
|
elseif data_type == "image"
|
||||||
@@ -164,7 +179,7 @@ function test_mix_receive()
|
|||||||
log_trace(" Size: $(length(data)) bytes")
|
log_trace(" Size: $(length(data)) bytes")
|
||||||
|
|
||||||
# Save to file
|
# Save to file
|
||||||
output_path = "./received_$dataname.bin"
|
output_path = "./received_$dataname"
|
||||||
write(output_path, data)
|
write(output_path, data)
|
||||||
log_trace(" Saved to: $output_path")
|
log_trace(" Saved to: $output_path")
|
||||||
else
|
else
|
||||||
@@ -180,7 +195,9 @@ function test_mix_receive()
|
|||||||
println("\n=== Verification Summary ===")
|
println("\n=== Verification Summary ===")
|
||||||
text_count = count(x -> x[3] == "text", result["payloads"])
|
text_count = count(x -> x[3] == "text", result["payloads"])
|
||||||
dict_count = count(x -> x[3] == "dictionary", result["payloads"])
|
dict_count = count(x -> x[3] == "dictionary", result["payloads"])
|
||||||
table_count = count(x -> x[3] == "table", result["payloads"])
|
arrowtable_count = count(x -> x[3] == "arrowtable", result["payloads"])
|
||||||
|
jsontable_count = count(x -> x[3] == "jsontable", result["payloads"])
|
||||||
|
table_count = count(x -> x[3] == "table", result["payloads"]) # backward compatibility
|
||||||
image_count = count(x -> x[3] == "image", result["payloads"])
|
image_count = count(x -> x[3] == "image", result["payloads"])
|
||||||
audio_count = count(x -> x[3] == "audio", result["payloads"])
|
audio_count = count(x -> x[3] == "audio", result["payloads"])
|
||||||
video_count = count(x -> x[3] == "video", result["payloads"])
|
video_count = count(x -> x[3] == "video", result["payloads"])
|
||||||
@@ -188,7 +205,9 @@ function test_mix_receive()
|
|||||||
|
|
||||||
log_trace("Text payloads: $text_count")
|
log_trace("Text payloads: $text_count")
|
||||||
log_trace("Dictionary payloads: $dict_count")
|
log_trace("Dictionary payloads: $dict_count")
|
||||||
log_trace("Table payloads: $table_count")
|
log_trace("Arrow table payloads: $arrowtable_count")
|
||||||
|
log_trace("JSON table payloads: $jsontable_count")
|
||||||
|
log_trace("Table payloads (backward compat): $table_count")
|
||||||
log_trace("Image payloads: $image_count")
|
log_trace("Image payloads: $image_count")
|
||||||
log_trace("Audio payloads: $audio_count")
|
log_trace("Audio payloads: $audio_count")
|
||||||
log_trace("Video payloads: $video_count")
|
log_trace("Video payloads: $video_count")
|
||||||
@@ -199,9 +218,13 @@ function test_mix_receive()
|
|||||||
for (dataname, data, data_type) in result["payloads"]
|
for (dataname, data, data_type) in result["payloads"]
|
||||||
if data_type in ["image", "audio", "video", "binary"]
|
if data_type in ["image", "audio", "video", "binary"]
|
||||||
log_trace("$dataname: $(length(data)) bytes (binary)")
|
log_trace("$dataname: $(length(data)) bytes (binary)")
|
||||||
|
elseif data_type == "arrowtable"
|
||||||
|
# log_trace("$dataname: $(size(data, 1)) rows x $(size(data, 2)) columns (Arrow.Table)")
|
||||||
|
elseif data_type == "jsontable"
|
||||||
|
log_trace("$dataname: $(length(data)) rows (Vector{Dict/NamedTuple})")
|
||||||
elseif data_type == "table"
|
elseif data_type == "table"
|
||||||
data = DataFrame(data)
|
data = DataFrame(data)
|
||||||
log_trace("$dataname: $(size(data, 1)) rows x $(size(data, 2)) columns (DataFrame)")
|
# log_trace("$dataname: $(size(data, 1)) rows x $(size(data, 2)) columns (DataFrame)")
|
||||||
elseif data_type == "dictionary"
|
elseif data_type == "dictionary"
|
||||||
log_trace("$dataname: $(length(JSON.json(data))) bytes (Dict)")
|
log_trace("$dataname: $(length(JSON.json(data))) bytes (Dict)")
|
||||||
elseif data_type == "text"
|
elseif data_type == "text"
|
||||||
@@ -211,7 +234,7 @@ function test_mix_receive()
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Keep listening for 2 minutes
|
# Keep listening for 2 minutes
|
||||||
sleep(120)
|
sleep(180)
|
||||||
NATS.drain(conn)
|
NATS.drain(conn)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
#!/usr/bin/env julia
|
#!/usr/bin/env julia
|
||||||
# Test script for mixed-content message testing
|
# Test script for mixed-content message testing
|
||||||
# Tests sending a mix of text, json, table, image, audio, video, and binary data
|
# Tests sending a mix of text, dictionary, arrowtable, jsontable, image, audio, video, and binary data
|
||||||
# from Julia serviceA to Julia serviceB using NATSBridge.jl smartsend
|
# from Julia serviceA to Julia serviceB using NATSBridge.jl smartsend
|
||||||
#
|
#
|
||||||
# This test demonstrates that any combination and any number of mixed content
|
# This test demonstrates that any combination and any number of mixed content
|
||||||
# can be sent and received correctly.
|
# can be sent and received correctly.
|
||||||
|
#
|
||||||
|
# Key concept: DataFrames are the main table representation in Julia.
|
||||||
|
# The NATSBridge.jl library handles serialization:
|
||||||
|
# - For "arrowtable" type: DataFrame is serialized to Arrow IPC format
|
||||||
|
# - For "jsontable" type: DataFrame is converted to Vector{Dict} and then to JSON
|
||||||
|
|
||||||
using NATS, JSON, UUIDs, Dates, PrettyPrinting, DataFrames, Arrow, HTTP, Base64
|
using NATS, JSON, UUIDs, Dates, PrettyPrinting, DataFrames, Arrow, HTTP, Base64
|
||||||
|
|
||||||
@@ -13,7 +18,7 @@ include("../src/NATSBridge.jl")
|
|||||||
using .NATSBridge
|
using .NATSBridge
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
const SUBJECT = "/NATSBridge_mix_test"
|
const SUBJECT = "/natsbridge"
|
||||||
const NATS_URL = "nats.yiem.cc"
|
const NATS_URL = "nats.yiem.cc"
|
||||||
const FILESERVER_URL = "http://192.168.88.104:8080"
|
const FILESERVER_URL = "http://192.168.88.104:8080"
|
||||||
|
|
||||||
@@ -82,49 +87,46 @@ function create_sample_data()
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Table data (DataFrame - small - direct transport)
|
# Arrow table data (DataFrame - small - direct transport)
|
||||||
table_data_small = DataFrame(
|
# Uses Arrow IPC format for efficient binary serialization
|
||||||
|
# NATSBridge.jl handles serialization: DataFrame -> Arrow IPC
|
||||||
|
arrow_table_small = DataFrame(
|
||||||
id = 1:10,
|
id = 1:10,
|
||||||
message = ["msg_$i" for i in 1:10],
|
name = ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank", "Grace", "Henry", "Ivy", "Jack"],
|
||||||
sender = ["sender_$i" for i in 1:10],
|
score = rand(50:100, 10),
|
||||||
timestamp = [string(Dates.now()) for _ in 1:10],
|
active = rand([true, false], 10)
|
||||||
priority = rand(1:3, 10)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Table data (DataFrame - large - link transport)
|
# Arrow table data (DataFrame - large - link transport)
|
||||||
# ~1.5MB of data (150,000 rows) - should trigger link transport
|
# ~1.5MB of Arrow data (200,000 rows) - should trigger link transport
|
||||||
table_data_large = DataFrame(
|
# NATSBridge.jl handles serialization: DataFrame -> Arrow IPC
|
||||||
id = 1:150_000,
|
arrow_table_large = DataFrame(
|
||||||
message = ["msg_$i" for i in 1:150_000],
|
id = 1:2_000_000,
|
||||||
sender = ["sender_$i" for i in 1:150_000],
|
name = ["user_$i" for i in 1:2_000_000],
|
||||||
timestamp = [string(Dates.now()) for i in 1:150_000],
|
score = rand(50:100, 2_000_000),
|
||||||
priority = rand(1:3, 150_000)
|
active = rand([true, false], 2_000_000),
|
||||||
|
timestamp = [string(Dates.now()) for _ in 1:2_000_000]
|
||||||
)
|
)
|
||||||
|
|
||||||
# Image data (small binary - direct transport)
|
# Json table data (DataFrame - small - direct transport)
|
||||||
# Create a simple 10x10 pixel PNG-like data (128 bytes header + 100 pixels = 112 bytes)
|
# Uses JSON format for human-readable tabular data
|
||||||
# Using simple RGB data (10*10*3 = 300 bytes of pixel data)
|
# NATSBridge.jl handles serialization: DataFrame -> Vector{Dict} -> JSON
|
||||||
image_width = 10
|
json_table_small = DataFrame(
|
||||||
image_height = 10
|
id = 1:10,
|
||||||
image_data = UInt8[]
|
name = ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank", "Grace", "Henry", "Ivy", "Jack"],
|
||||||
# PNG header (simplified)
|
score = rand(50:100, 10),
|
||||||
push!(image_data, 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A)
|
active = rand([true, false], 10)
|
||||||
# Simple RGB data (RGBRGBRGB...)
|
)
|
||||||
for i in 1:image_width*image_height
|
|
||||||
push!(image_data, 0xFF, 0x00, 0x00) # Red pixel
|
|
||||||
end
|
|
||||||
|
|
||||||
# Image data (large - link transport)
|
# Json table data (DataFrame - large - link transport)
|
||||||
# Create a larger image (~1.5MB) to test link transport
|
# ~1.5MB of JSON data (150,000 rows) - should trigger link transport
|
||||||
large_image_width = 500
|
# NATSBridge.jl handles serialization: DataFrame -> Vector{Dict} -> JSON
|
||||||
large_image_height = 1000
|
json_table_large = DataFrame(
|
||||||
large_image_data = UInt8[]
|
id = 1:2_000_000,
|
||||||
# PNG header (simplified for 500x1000)
|
name = ["user_$i" for i in 1:2_000_000],
|
||||||
push!(large_image_data, 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A)
|
score = rand(50:100, 2_000_000),
|
||||||
# RGB data (500*1000*3 = 1,500,000 bytes)
|
active = rand([true, false], 2_000_000)
|
||||||
for i in 1:large_image_width*large_image_height
|
)
|
||||||
push!(large_image_data, rand(1:255), rand(1:255), rand(1:255)) # Random color pixels
|
|
||||||
end
|
|
||||||
|
|
||||||
# Audio data (small binary - direct transport)
|
# Audio data (small binary - direct transport)
|
||||||
audio_data = UInt8[rand(1:255) for _ in 1:100]
|
audio_data = UInt8[rand(1:255) for _ in 1:100]
|
||||||
@@ -150,10 +152,10 @@ function create_sample_data()
|
|||||||
return (
|
return (
|
||||||
text_data,
|
text_data,
|
||||||
dict_data,
|
dict_data,
|
||||||
table_data_small,
|
arrow_table_small,
|
||||||
table_data_large,
|
arrow_table_large,
|
||||||
image_data,
|
json_table_small,
|
||||||
large_image_data,
|
json_table_large,
|
||||||
audio_data,
|
audio_data,
|
||||||
large_audio_data,
|
large_audio_data,
|
||||||
video_data,
|
video_data,
|
||||||
@@ -167,31 +169,47 @@ end
|
|||||||
# Sender: Send mixed content via smartsend
|
# Sender: Send mixed content via smartsend
|
||||||
function test_mix_send()
|
function test_mix_send()
|
||||||
# Create sample data
|
# Create sample data
|
||||||
(text_data, dict_data, table_data_small, table_data_large, image_data, large_image_data, audio_data, large_audio_data, video_data, large_video_data, binary_data, large_binary_data) = create_sample_data()
|
(text_data, dict_data, arrow_table_small, arrow_table_large, json_table_small, json_table_large, audio_data, large_audio_data, video_data, large_video_data, binary_data, large_binary_data) = create_sample_data()
|
||||||
|
|
||||||
|
# Read image files from disk (following test_julia_file_sender.jl pattern)
|
||||||
|
# Small image - should use direct transport
|
||||||
|
file_path_small_image = "./test/small_image.jpg"
|
||||||
|
file_data_small_image = read(file_path_small_image)
|
||||||
|
filename_small_image = basename(file_path_small_image)
|
||||||
|
|
||||||
|
# Large image - should use link transport
|
||||||
|
file_path_large_image = "./test/large_image.png"
|
||||||
|
file_data_large_image = read(file_path_large_image)
|
||||||
|
filename_large_image = basename(file_path_large_image)
|
||||||
|
|
||||||
# Create payloads list - mixed content with both small and large data
|
# Create payloads list - mixed content with both small and large data
|
||||||
# Small data uses direct transport, large data uses link transport
|
# Small data uses direct transport, large data uses link transport
|
||||||
|
# Key: Pass DataFrame directly and specify type as "arrowtable" or "jsontable"
|
||||||
|
# NATSBridge.jl handles the serialization internally
|
||||||
payloads = [
|
payloads = [
|
||||||
# Small data (direct transport) - text, dictionary, small table
|
# Small data (direct transport) - text, dictionary, arrowtable, jsontable, small image
|
||||||
("chat_text", text_data, "text"),
|
("chat_text", text_data, "text"),
|
||||||
("chat_json", dict_data, "dictionary"),
|
("chat_json", dict_data, "dictionary"),
|
||||||
("chat_table_small", table_data_small, "table"),
|
# ("arrow_table_small", arrow_table_small, "arrowtable"),
|
||||||
|
("json_table_small", json_table_small, "jsontable"),
|
||||||
|
(filename_small_image, file_data_small_image, "binary"),
|
||||||
|
|
||||||
# Large data (link transport) - large table, large image, large audio, large video, large binary
|
# Large data (link transport) - large arrowtable, large jsontable, large image, large audio, large video, large binary
|
||||||
("chat_table_large", table_data_large, "table"),
|
# ("arrow_table_large", arrow_table_large, "arrowtable"),
|
||||||
("user_image_large", large_image_data, "image"),
|
("json_table_large", json_table_large, "jsontable"),
|
||||||
|
(filename_large_image, file_data_large_image, "binary"),
|
||||||
("audio_clip_large", large_audio_data, "audio"),
|
("audio_clip_large", large_audio_data, "audio"),
|
||||||
("video_clip_large", large_video_data, "video"),
|
("video_clip_large", large_video_data, "video"),
|
||||||
("binary_file_large", large_binary_data, "binary")
|
("binary_file_large", large_binary_data, "binary")
|
||||||
]
|
]
|
||||||
|
|
||||||
# Use smartsend with mixed content
|
# Use smartsend with mixed content
|
||||||
env = NATSBridge.smartsend(
|
sendinfo = NATSBridge.smartsend(
|
||||||
SUBJECT,
|
SUBJECT,
|
||||||
payloads; # List of (dataname, data, type) tuples
|
payloads; # List of (dataname, data, type) tuples
|
||||||
nats_url = NATS_URL,
|
broker_url = NATS_URL,
|
||||||
fileserver_url = FILESERVER_URL,
|
fileserver_url = FILESERVER_URL,
|
||||||
fileserverUploadHandler = plik_upload_handler,
|
fileserver_upload_handler = plik_upload_handler,
|
||||||
size_threshold = 1_000_000, # 1MB threshold
|
size_threshold = 1_000_000, # 1MB threshold
|
||||||
correlation_id = correlation_id,
|
correlation_id = correlation_id,
|
||||||
msg_purpose = "chat",
|
msg_purpose = "chat",
|
||||||
@@ -199,16 +217,18 @@ function test_mix_send()
|
|||||||
receiver_name = "",
|
receiver_name = "",
|
||||||
receiver_id = "",
|
receiver_id = "",
|
||||||
reply_to = "",
|
reply_to = "",
|
||||||
reply_to_msg_id = ""
|
reply_to_msg_id = "",
|
||||||
|
is_publish = true # Publish the message to NATS
|
||||||
)
|
)
|
||||||
|
|
||||||
|
env, env_json_str = sendinfo
|
||||||
log_trace("Sent message with $(length(env.payloads)) payloads")
|
log_trace("Sent message with $(length(env.payloads)) payloads")
|
||||||
|
|
||||||
# Log transport type for each payload
|
# Log transport type for each payload
|
||||||
for (i, payload) in enumerate(env.payloads)
|
for (i, payload) in enumerate(env.payloads)
|
||||||
log_trace("Payload $i ('$payload.dataname'):")
|
log_trace("Payload $i ('$payload.dataname'):")
|
||||||
log_trace(" Transport: $(payload.transport)")
|
log_trace(" Transport: $(payload.transport)")
|
||||||
log_trace(" Type: $(payload.type)")
|
log_trace(" Type: $(payload.payload_type)")
|
||||||
log_trace(" Size: $(payload.size) bytes")
|
log_trace(" Size: $(payload.size) bytes")
|
||||||
log_trace(" Encoding: $(payload.encoding)")
|
log_trace(" Encoding: $(payload.encoding)")
|
||||||
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
#!/usr/bin/env julia
|
|
||||||
# Test script for Dictionary transport testing
|
|
||||||
# Tests receiving 1 large and 1 small Dictionaries via direct and link transport
|
|
||||||
# Uses NATSBridge.jl smartreceive with "dictionary" type
|
|
||||||
|
|
||||||
using NATS, JSON, UUIDs, Dates, PrettyPrinting, DataFrames, Arrow, HTTP
|
|
||||||
|
|
||||||
# Include the bridge module
|
|
||||||
include("../src/NATSBridge.jl")
|
|
||||||
using .NATSBridge
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
const SUBJECT = "/NATSBridge_dict_test"
|
|
||||||
const NATS_URL = "nats.yiem.cc"
|
|
||||||
const FILESERVER_URL = "http://192.168.88.104:8080"
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------------------------ #
|
|
||||||
# test dictionary transfer #
|
|
||||||
# ------------------------------------------------------------------------------------------------ #
|
|
||||||
|
|
||||||
|
|
||||||
# Helper: Log with correlation ID
|
|
||||||
function log_trace(message)
|
|
||||||
timestamp = Dates.now()
|
|
||||||
println("[$timestamp] $message")
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# Receiver: Listen for messages and verify Dictionary handling
|
|
||||||
function test_dict_receive()
|
|
||||||
conn = NATS.connect(NATS_URL)
|
|
||||||
NATS.subscribe(conn, SUBJECT) do msg
|
|
||||||
log_trace("Received message on $(msg.subject)")
|
|
||||||
|
|
||||||
# Use NATSBridge.smartreceive to handle the data
|
|
||||||
# API: smartreceive(msg, download_handler; max_retries, base_delay, max_delay)
|
|
||||||
result = NATSBridge.smartreceive(
|
|
||||||
msg;
|
|
||||||
max_retries = 5,
|
|
||||||
base_delay = 100,
|
|
||||||
max_delay = 5000
|
|
||||||
)
|
|
||||||
|
|
||||||
# Result is an envelope dictionary with payloads field containing list of (dataname, data, data_type) tuples
|
|
||||||
for (dataname, data, data_type) in result["payloads"]
|
|
||||||
if isa(data, JSON.Object{String, Any})
|
|
||||||
log_trace("Received Dictionary '$dataname' of type $data_type")
|
|
||||||
|
|
||||||
# Display dictionary contents
|
|
||||||
println(" Contents:")
|
|
||||||
for (key, value) in data
|
|
||||||
println(" $key => $value")
|
|
||||||
end
|
|
||||||
|
|
||||||
# Save to JSON file
|
|
||||||
output_path = "./received_$dataname.json"
|
|
||||||
json_str = JSON.json(data, 2)
|
|
||||||
write(output_path, json_str)
|
|
||||||
log_trace("Saved Dictionary to $output_path")
|
|
||||||
else
|
|
||||||
log_trace("Received unexpected data type for '$dataname': $(typeof(data))")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Keep listening for 10 seconds
|
|
||||||
sleep(120)
|
|
||||||
NATS.drain(conn)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# Run the test
|
|
||||||
println("Starting Dictionary transport test...")
|
|
||||||
println("Note: This receiver will wait for messages from the sender.")
|
|
||||||
println("Run test_julia_to_julia_dict_sender.jl first to send test data.")
|
|
||||||
|
|
||||||
# Run receiver
|
|
||||||
println("testing smartreceive")
|
|
||||||
test_dict_receive()
|
|
||||||
|
|
||||||
println("Test completed.")
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
#!/usr/bin/env julia
|
|
||||||
# Test script for Dictionary transport testing
|
|
||||||
# Tests sending 1 large and 1 small Dictionaries via direct and link transport
|
|
||||||
# Uses NATSBridge.jl smartsend with "dictionary" type
|
|
||||||
|
|
||||||
using NATS, JSON, UUIDs, Dates, PrettyPrinting, DataFrames, Arrow, HTTP
|
|
||||||
|
|
||||||
# Include the bridge module
|
|
||||||
include("../src/NATSBridge.jl")
|
|
||||||
using .NATSBridge
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
const SUBJECT = "/NATSBridge_dict_test"
|
|
||||||
const NATS_URL = "nats.yiem.cc"
|
|
||||||
const FILESERVER_URL = "http://192.168.88.104:8080"
|
|
||||||
|
|
||||||
# Create correlation ID for tracing
|
|
||||||
correlation_id = string(uuid4())
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------------------------ #
|
|
||||||
# test dictionary transfer #
|
|
||||||
# ------------------------------------------------------------------------------------------------ #
|
|
||||||
|
|
||||||
|
|
||||||
# Helper: Log with correlation ID
|
|
||||||
function log_trace(message)
|
|
||||||
timestamp = Dates.now()
|
|
||||||
println("[$timestamp] [Correlation: $correlation_id] $message")
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# File upload handler for plik server
|
|
||||||
function plik_upload_handler(fileserver_url::String, dataname::String, data::Vector{UInt8})::Dict{String, Any}
|
|
||||||
# Get upload ID
|
|
||||||
url_getUploadID = "$fileserver_url/upload"
|
|
||||||
headers = ["Content-Type" => "application/json"]
|
|
||||||
body = """{ "OneShot" : true }"""
|
|
||||||
httpResponse = HTTP.request("POST", url_getUploadID, headers, body; body_is_form=false)
|
|
||||||
responseJson = JSON.parse(String(httpResponse.body))
|
|
||||||
uploadid = responseJson["id"]
|
|
||||||
uploadtoken = responseJson["uploadToken"]
|
|
||||||
|
|
||||||
# Upload file
|
|
||||||
file_multipart = HTTP.Multipart(dataname, IOBuffer(data), "application/octet-stream")
|
|
||||||
url_upload = "$fileserver_url/file/$uploadid"
|
|
||||||
headers = ["X-UploadToken" => uploadtoken]
|
|
||||||
|
|
||||||
form = HTTP.Form(Dict("file" => file_multipart))
|
|
||||||
httpResponse = HTTP.post(url_upload, headers, form)
|
|
||||||
responseJson = JSON.parse(String(httpResponse.body))
|
|
||||||
|
|
||||||
fileid = responseJson["id"]
|
|
||||||
url = "$fileserver_url/file/$uploadid/$fileid/$dataname"
|
|
||||||
|
|
||||||
return Dict("status" => httpResponse.status, "uploadid" => uploadid, "fileid" => fileid, "url" => url)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# Sender: Send Dictionaries via smartsend
|
|
||||||
function test_dict_send()
|
|
||||||
# Create a small Dictionary (will use direct transport)
|
|
||||||
small_dict = Dict(
|
|
||||||
"name" => "Alice",
|
|
||||||
"age" => 30,
|
|
||||||
"scores" => [95, 88, 92],
|
|
||||||
"metadata" => Dict(
|
|
||||||
"height" => 155,
|
|
||||||
"weight" => 55
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a large Dictionary (will use link transport if > 1MB)
|
|
||||||
# Generate a larger dataset (~2MB to ensure link transport)
|
|
||||||
large_dict = Dict(
|
|
||||||
"ids" => collect(1:50000),
|
|
||||||
"names" => ["User_$i" for i in 1:50000],
|
|
||||||
"scores" => rand(1:100, 50000),
|
|
||||||
"categories" => ["Category_$(rand(1:10))" for i in 1:50000],
|
|
||||||
"metadata" => Dict(
|
|
||||||
"source" => "test_generator",
|
|
||||||
"timestamp" => string(Dates.now())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test data 1: small Dictionary
|
|
||||||
data1 = ("small_dict", small_dict, "dictionary")
|
|
||||||
|
|
||||||
# Test data 2: large Dictionary
|
|
||||||
data2 = ("large_dict", large_dict, "dictionary")
|
|
||||||
|
|
||||||
# Use smartsend with dictionary type
|
|
||||||
# For small Dictionary: will use direct transport (JSON encoded)
|
|
||||||
# For large Dictionary: will use link transport (uploaded to fileserver)
|
|
||||||
env = NATSBridge.smartsend(
|
|
||||||
SUBJECT,
|
|
||||||
[data1, data2]; # List of (dataname, data, type) tuples
|
|
||||||
nats_url = NATS_URL,
|
|
||||||
fileserver_url = FILESERVER_URL,
|
|
||||||
fileserverUploadHandler = plik_upload_handler,
|
|
||||||
size_threshold = 1_000_000, # 1MB threshold
|
|
||||||
correlation_id = correlation_id,
|
|
||||||
msg_purpose = "chat",
|
|
||||||
sender_name = "dict_sender",
|
|
||||||
receiver_name = "",
|
|
||||||
receiver_id = "",
|
|
||||||
reply_to = "",
|
|
||||||
reply_to_msg_id = ""
|
|
||||||
)
|
|
||||||
|
|
||||||
log_trace("Sent message with $(length(env.payloads)) payloads")
|
|
||||||
|
|
||||||
# Log transport type for each payload
|
|
||||||
for (i, payload) in enumerate(env.payloads)
|
|
||||||
log_trace("Payload $i ('$payload.dataname'):")
|
|
||||||
log_trace(" Transport: $(payload.transport)")
|
|
||||||
log_trace(" Type: $(payload.type)")
|
|
||||||
log_trace(" Size: $(payload.size) bytes")
|
|
||||||
log_trace(" Encoding: $(payload.encoding)")
|
|
||||||
|
|
||||||
if payload.transport == "link"
|
|
||||||
log_trace(" URL: $(payload.data)")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# Run the test
|
|
||||||
println("Starting Dictionary transport test...")
|
|
||||||
println("Correlation ID: $correlation_id")
|
|
||||||
|
|
||||||
# Run sender
|
|
||||||
println("start smartsend for dictionaries")
|
|
||||||
test_dict_send()
|
|
||||||
|
|
||||||
println("Test completed.")
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
#!/usr/bin/env julia
|
|
||||||
# Test script for large payload testing using binary transport
|
|
||||||
# Tests sending a large file (> 1MB) via smartsend with binary type
|
|
||||||
# Updated to match NATSBridge.jl API
|
|
||||||
|
|
||||||
using NATS, JSON, UUIDs, Dates, PrettyPrinting, DataFrames, Arrow, HTTP
|
|
||||||
|
|
||||||
|
|
||||||
# workdir =
|
|
||||||
|
|
||||||
# Include the bridge module
|
|
||||||
include("../src/NATSBridge.jl")
|
|
||||||
using .NATSBridge
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
const SUBJECT = "/NATSBridge_test"
|
|
||||||
const NATS_URL = "nats.yiem.cc"
|
|
||||||
const FILESERVER_URL = "http://192.168.88.104:8080"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------------------------ #
|
|
||||||
# test file transfer #
|
|
||||||
# ------------------------------------------------------------------------------------------------ #
|
|
||||||
|
|
||||||
# Helper: Log with correlation ID
|
|
||||||
function log_trace(message)
|
|
||||||
timestamp = Dates.now()
|
|
||||||
println("[$timestamp] $message")
|
|
||||||
end
|
|
||||||
|
|
||||||
# Receiver: Listen for messages and verify large payload handling
|
|
||||||
function test_large_binary_receive()
|
|
||||||
conn = NATS.connect(NATS_URL)
|
|
||||||
NATS.subscribe(conn, SUBJECT) do msg
|
|
||||||
log_trace("Received message on $(msg.subject)")
|
|
||||||
|
|
||||||
# Use NATSBridge.smartreceive to handle the data
|
|
||||||
# API: smartreceive(msg, download_handler; max_retries, base_delay, max_delay)
|
|
||||||
result = NATSBridge.smartreceive(
|
|
||||||
msg;
|
|
||||||
max_retries = 5,
|
|
||||||
base_delay = 100,
|
|
||||||
max_delay = 5000
|
|
||||||
)
|
|
||||||
|
|
||||||
# Result is an envelope dictionary with payloads field containing list of (dataname, data, data_type) tuples
|
|
||||||
for (dataname, data, data_type) in result["payloads"]
|
|
||||||
# Check transport type from the envelope
|
|
||||||
# For link transport, data is the URL string
|
|
||||||
# For direct transport, data is the actual payload bytes
|
|
||||||
|
|
||||||
if isa(data, Vector{UInt8})
|
|
||||||
file_size = length(data)
|
|
||||||
log_trace("Received $(file_size) bytes of binary data for '$dataname' of type $data_type")
|
|
||||||
|
|
||||||
# Save received data to a test file
|
|
||||||
output_path = "./new_$dataname"
|
|
||||||
write(output_path, data)
|
|
||||||
log_trace("Saved received data to $output_path")
|
|
||||||
else
|
|
||||||
log_trace("Received $(file_size) bytes of binary data for '$dataname' of type $data_type")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Keep listening for 10 seconds
|
|
||||||
sleep(120)
|
|
||||||
NATS.drain(conn)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# Run the test
|
|
||||||
println("Starting large binary payload test...")
|
|
||||||
|
|
||||||
# # Run sender first
|
|
||||||
# println("start smartsend")
|
|
||||||
# test_large_binary_send()
|
|
||||||
|
|
||||||
# Run receiver
|
|
||||||
println("testing smartreceive")
|
|
||||||
test_large_binary_receive()
|
|
||||||
|
|
||||||
println("Test completed.")
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
#!/usr/bin/env julia
|
|
||||||
# Test script for large payload testing using binary transport
|
|
||||||
# Tests sending a large file (> 1MB) via smartsend with binary type
|
|
||||||
# Updated to match NATSBridge.jl API
|
|
||||||
|
|
||||||
using NATS, JSON, UUIDs, Dates, PrettyPrinting, DataFrames, Arrow, HTTP
|
|
||||||
|
|
||||||
|
|
||||||
# workdir =
|
|
||||||
|
|
||||||
# Include the bridge module
|
|
||||||
include("../src/NATSBridge.jl")
|
|
||||||
using .NATSBridge
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
const SUBJECT = "/NATSBridge_test"
|
|
||||||
const NATS_URL = "nats.yiem.cc"
|
|
||||||
const FILESERVER_URL = "http://192.168.88.104:8080"
|
|
||||||
|
|
||||||
# Create correlation ID for tracing
|
|
||||||
correlation_id = string(uuid4())
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------------------------ #
|
|
||||||
# test file transfer #
|
|
||||||
# ------------------------------------------------------------------------------------------------ #
|
|
||||||
|
|
||||||
|
|
||||||
# Helper: Log with correlation ID
|
|
||||||
function log_trace(message)
|
|
||||||
timestamp = Dates.now()
|
|
||||||
println("[$timestamp] [Correlation: $correlation_id] $message")
|
|
||||||
end
|
|
||||||
|
|
||||||
# File upload handler for plik server
|
|
||||||
function plik_upload_handler(fileserver_url::String, dataname::String, data::Vector{UInt8})::Dict{String, Any}
|
|
||||||
# Get upload ID
|
|
||||||
url_getUploadID = "$fileserver_url/upload"
|
|
||||||
headers = ["Content-Type" => "application/json"]
|
|
||||||
body = """{ "OneShot" : true }"""
|
|
||||||
httpResponse = HTTP.request("POST", url_getUploadID, headers, body; body_is_form=false)
|
|
||||||
responseJson = JSON.parse(String(httpResponse.body))
|
|
||||||
uploadid = responseJson["id"]
|
|
||||||
uploadtoken = responseJson["uploadToken"]
|
|
||||||
|
|
||||||
# Upload file
|
|
||||||
file_multipart = HTTP.Multipart(dataname, IOBuffer(data), "application/octet-stream")
|
|
||||||
url_upload = "$fileserver_url/file/$uploadid"
|
|
||||||
headers = ["X-UploadToken" => uploadtoken]
|
|
||||||
|
|
||||||
form = HTTP.Form(Dict("file" => file_multipart))
|
|
||||||
httpResponse = HTTP.post(url_upload, headers, form)
|
|
||||||
responseJson = JSON.parse(String(httpResponse.body))
|
|
||||||
|
|
||||||
fileid = responseJson["id"]
|
|
||||||
url = "$fileserver_url/file/$uploadid/$fileid/$dataname"
|
|
||||||
|
|
||||||
return Dict("status" => httpResponse.status, "uploadid" => uploadid, "fileid" => fileid, "url" => url)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Sender: Send large binary file via smartsend
|
|
||||||
function test_large_binary_send()
|
|
||||||
# Read the large file as binary data
|
|
||||||
|
|
||||||
# test data 1
|
|
||||||
file_path1 = "./testFile_large.zip"
|
|
||||||
file_data1 = read(file_path1)
|
|
||||||
filename1 = basename(file_path1)
|
|
||||||
data1 = (filename1, file_data1, "binary")
|
|
||||||
|
|
||||||
# test data 2
|
|
||||||
file_path2 = "./testFile_small.zip"
|
|
||||||
file_data2 = read(file_path2)
|
|
||||||
filename2 = basename(file_path2)
|
|
||||||
data2 = (filename2, file_data2, "binary")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Use smartsend with binary type - will automatically use link transport
|
|
||||||
# if file size exceeds the threshold (1MB by default)
|
|
||||||
# API: smartsend(subject, [(dataname, data, type), ...]; keywords...)
|
|
||||||
env = NATSBridge.smartsend(
|
|
||||||
SUBJECT,
|
|
||||||
[data1, data2]; # List of (dataname, data, type) tuples
|
|
||||||
nats_url = NATS_URL;
|
|
||||||
fileserver_url = FILESERVER_URL,
|
|
||||||
fileserverUploadHandler = plik_upload_handler,
|
|
||||||
size_threshold = 1_000_000,
|
|
||||||
correlation_id = correlation_id,
|
|
||||||
msg_purpose = "chat",
|
|
||||||
sender_name = "sender",
|
|
||||||
receiver_name = "",
|
|
||||||
receiver_id = "",
|
|
||||||
reply_to = "",
|
|
||||||
reply_to_msg_id = ""
|
|
||||||
)
|
|
||||||
|
|
||||||
log_trace("Sent message with transport: $(env.payloads[1].transport)")
|
|
||||||
log_trace("Envelope type: $(env.payloads[1].type)")
|
|
||||||
|
|
||||||
# Check if link transport was used
|
|
||||||
if env.payloads[1].transport == "link"
|
|
||||||
log_trace("Using link transport - file uploaded to HTTP server")
|
|
||||||
log_trace("URL: $(env.payloads[1].data)")
|
|
||||||
else
|
|
||||||
log_trace("Using direct transport - payload sent via NATS")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Run the test
|
|
||||||
println("Starting large binary payload test...")
|
|
||||||
println("Correlation ID: $correlation_id")
|
|
||||||
|
|
||||||
# Run sender first
|
|
||||||
println("start smartsend")
|
|
||||||
test_large_binary_send()
|
|
||||||
|
|
||||||
# Run receiver
|
|
||||||
# println("testing smartreceive")
|
|
||||||
# test_large_binary_receive()
|
|
||||||
|
|
||||||
println("Test completed.")
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
#!/usr/bin/env julia
|
|
||||||
# Test script for DataFrame table transport testing
|
|
||||||
# Tests receiving 1 large and 1 small DataFrames via direct and link transport
|
|
||||||
# Uses NATSBridge.jl smartreceive with "table" type
|
|
||||||
|
|
||||||
using NATS, JSON, UUIDs, Dates, PrettyPrinting, DataFrames, Arrow, HTTP
|
|
||||||
|
|
||||||
# Include the bridge module
|
|
||||||
include("../src/NATSBridge.jl")
|
|
||||||
using .NATSBridge
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
const SUBJECT = "/NATSBridge_table_test"
|
|
||||||
const NATS_URL = "nats.yiem.cc"
|
|
||||||
const FILESERVER_URL = "http://192.168.88.104:8080"
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------------------------ #
|
|
||||||
# test table transfer #
|
|
||||||
# ------------------------------------------------------------------------------------------------ #
|
|
||||||
|
|
||||||
|
|
||||||
# Helper: Log with correlation ID
|
|
||||||
function log_trace(message)
|
|
||||||
timestamp = Dates.now()
|
|
||||||
println("[$timestamp] $message")
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# Receiver: Listen for messages and verify DataFrame table handling
|
|
||||||
function test_table_receive()
|
|
||||||
conn = NATS.connect(NATS_URL)
|
|
||||||
NATS.subscribe(conn, SUBJECT) do msg
|
|
||||||
log_trace("Received message on $(msg.subject)")
|
|
||||||
|
|
||||||
# Use NATSBridge.smartreceive to handle the data
|
|
||||||
# API: smartreceive(msg, download_handler; max_retries, base_delay, max_delay)
|
|
||||||
result = NATSBridge.smartreceive(
|
|
||||||
msg;
|
|
||||||
max_retries = 5,
|
|
||||||
base_delay = 100,
|
|
||||||
max_delay = 5000
|
|
||||||
)
|
|
||||||
|
|
||||||
# Result is an envelope dictionary with payloads field containing list of (dataname, data, data_type) tuples
|
|
||||||
for (dataname, data, data_type) in result["payloads"]
|
|
||||||
data = DataFrame(data)
|
|
||||||
if isa(data, DataFrame)
|
|
||||||
log_trace("Received DataFrame '$dataname' of type $data_type")
|
|
||||||
log_trace(" Dimensions: $(size(data, 1)) rows x $(size(data, 2)) columns")
|
|
||||||
log_trace(" Column names: $(names(data))")
|
|
||||||
|
|
||||||
# Display first few rows
|
|
||||||
println(" First 5 rows:")
|
|
||||||
display(data[1:min(5, size(data, 1)), :])
|
|
||||||
|
|
||||||
# Save to file
|
|
||||||
output_path = "./received_$dataname.arrow"
|
|
||||||
io = IOBuffer()
|
|
||||||
Arrow.write(io, data)
|
|
||||||
write(output_path, take!(io))
|
|
||||||
log_trace("Saved DataFrame to $output_path")
|
|
||||||
else
|
|
||||||
log_trace("Received unexpected data type for '$dataname': $(typeof(data))")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Keep listening for 10 seconds
|
|
||||||
sleep(120)
|
|
||||||
NATS.drain(conn)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# Run the test
|
|
||||||
println("Starting DataFrame table transport test...")
|
|
||||||
println("Note: This receiver will wait for messages from the sender.")
|
|
||||||
println("Run test_julia_to_julia_table_sender.jl first to send test data.")
|
|
||||||
|
|
||||||
# Run receiver
|
|
||||||
println("testing smartreceive")
|
|
||||||
test_table_receive()
|
|
||||||
|
|
||||||
println("Test completed.")
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
#!/usr/bin/env julia
|
|
||||||
# Test script for DataFrame table transport testing
|
|
||||||
# Tests sending 1 large and 1 small DataFrames via direct and link transport
|
|
||||||
# Uses NATSBridge.jl smartsend with "table" type
|
|
||||||
|
|
||||||
using NATS, JSON, UUIDs, Dates, PrettyPrinting, DataFrames, Arrow, HTTP
|
|
||||||
|
|
||||||
# Include the bridge module
|
|
||||||
include("../src/NATSBridge.jl")
|
|
||||||
using .NATSBridge
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
const SUBJECT = "/NATSBridge_table_test"
|
|
||||||
const NATS_URL = "nats.yiem.cc"
|
|
||||||
const FILESERVER_URL = "http://192.168.88.104:8080"
|
|
||||||
|
|
||||||
# Create correlation ID for tracing
|
|
||||||
correlation_id = string(uuid4())
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------------------------ #
|
|
||||||
# test table transfer #
|
|
||||||
# ------------------------------------------------------------------------------------------------ #
|
|
||||||
|
|
||||||
|
|
||||||
# Helper: Log with correlation ID
|
|
||||||
function log_trace(message)
|
|
||||||
timestamp = Dates.now()
|
|
||||||
println("[$timestamp] [Correlation: $correlation_id] $message")
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# File upload handler for plik server
|
|
||||||
function plik_upload_handler(fileserver_url::String, dataname::String, data::Vector{UInt8})::Dict{String, Any}
|
|
||||||
# Get upload ID
|
|
||||||
url_getUploadID = "$fileserver_url/upload"
|
|
||||||
headers = ["Content-Type" => "application/json"]
|
|
||||||
body = """{ "OneShot" : true }"""
|
|
||||||
httpResponse = HTTP.request("POST", url_getUploadID, headers, body; body_is_form=false)
|
|
||||||
responseJson = JSON.parse(String(httpResponse.body))
|
|
||||||
uploadid = responseJson["id"]
|
|
||||||
uploadtoken = responseJson["uploadToken"]
|
|
||||||
|
|
||||||
# Upload file
|
|
||||||
file_multipart = HTTP.Multipart(dataname, IOBuffer(data), "application/octet-stream")
|
|
||||||
url_upload = "$fileserver_url/file/$uploadid"
|
|
||||||
headers = ["X-UploadToken" => uploadtoken]
|
|
||||||
|
|
||||||
form = HTTP.Form(Dict("file" => file_multipart))
|
|
||||||
httpResponse = HTTP.post(url_upload, headers, form)
|
|
||||||
responseJson = JSON.parse(String(httpResponse.body))
|
|
||||||
|
|
||||||
fileid = responseJson["id"]
|
|
||||||
url = "$fileserver_url/file/$uploadid/$fileid/$dataname"
|
|
||||||
|
|
||||||
return Dict("status" => httpResponse.status, "uploadid" => uploadid, "fileid" => fileid, "url" => url)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# Sender: Send DataFrame tables via smartsend
|
|
||||||
function test_table_send()
|
|
||||||
# Create a small DataFrame (will use direct transport)
|
|
||||||
small_df = DataFrame(
|
|
||||||
id = 1:10,
|
|
||||||
name = ["Alice", "Bob", "Charlie", "Diana", "Eve", "Frank", "Grace", "Henry", "Ivy", "Jack"],
|
|
||||||
score = [95, 88, 92, 85, 90, 78, 95, 88, 92, 85],
|
|
||||||
category = ["A", "B", "A", "B", "A", "B", "A", "B", "A", "B"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a large DataFrame (will use link transport if > 1MB)
|
|
||||||
# Generate a larger dataset (~2MB to ensure link transport)
|
|
||||||
large_ids = 1:50000
|
|
||||||
large_names = ["User_$i" for i in 1:50000]
|
|
||||||
large_scores = rand(1:100, 50000)
|
|
||||||
large_categories = ["Category_$(rand(1:10))" for i in 1:50000]
|
|
||||||
|
|
||||||
large_df = DataFrame(
|
|
||||||
id = large_ids,
|
|
||||||
name = large_names,
|
|
||||||
score = large_scores,
|
|
||||||
category = large_categories
|
|
||||||
)
|
|
||||||
|
|
||||||
# Test data 1: small DataFrame
|
|
||||||
data1 = ("small_table", small_df, "table")
|
|
||||||
|
|
||||||
# Test data 2: large DataFrame
|
|
||||||
data2 = ("large_table", large_df, "table")
|
|
||||||
|
|
||||||
# Use smartsend with table type
|
|
||||||
# For small DataFrame: will use direct transport (Base64 encoded Arrow IPC)
|
|
||||||
# For large DataFrame: will use link transport (uploaded to fileserver)
|
|
||||||
env = NATSBridge.smartsend(
|
|
||||||
SUBJECT,
|
|
||||||
[data1, data2]; # List of (dataname, data, type) tuples
|
|
||||||
nats_url = NATS_URL,
|
|
||||||
fileserver_url = FILESERVER_URL,
|
|
||||||
fileserverUploadHandler = plik_upload_handler,
|
|
||||||
size_threshold = 1_000_000, # 1MB threshold
|
|
||||||
correlation_id = correlation_id,
|
|
||||||
msg_purpose = "chat",
|
|
||||||
sender_name = "table_sender",
|
|
||||||
receiver_name = "",
|
|
||||||
receiver_id = "",
|
|
||||||
reply_to = "",
|
|
||||||
reply_to_msg_id = ""
|
|
||||||
)
|
|
||||||
|
|
||||||
log_trace("Sent message with $(length(env.payloads)) payloads")
|
|
||||||
|
|
||||||
# Log transport type for each payload
|
|
||||||
for (i, payload) in enumerate(env.payloads)
|
|
||||||
log_trace("Payload $i ('$payload.dataname'):")
|
|
||||||
log_trace(" Transport: $(payload.transport)")
|
|
||||||
log_trace(" Type: $(payload.type)")
|
|
||||||
log_trace(" Size: $(payload.size) bytes")
|
|
||||||
log_trace(" Encoding: $(payload.encoding)")
|
|
||||||
|
|
||||||
if payload.transport == "link"
|
|
||||||
log_trace(" URL: $(payload.data)")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# Run the test
|
|
||||||
println("Starting DataFrame table transport test...")
|
|
||||||
println("Correlation ID: $correlation_id")
|
|
||||||
|
|
||||||
# Run sender
|
|
||||||
println("start smartsend for tables")
|
|
||||||
test_table_send()
|
|
||||||
|
|
||||||
println("Test completed.")
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
#!/usr/bin/env julia
|
|
||||||
# Test script for text transport testing
|
|
||||||
# Tests receiving 1 large and 1 small text from Julia serviceA to Julia serviceB
|
|
||||||
# Uses NATSBridge.jl smartreceive with "text" type
|
|
||||||
|
|
||||||
using NATS, JSON, UUIDs, Dates, PrettyPrinting, DataFrames, Arrow, HTTP
|
|
||||||
|
|
||||||
# Include the bridge module
|
|
||||||
include("../src/NATSBridge.jl")
|
|
||||||
using .NATSBridge
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
const SUBJECT = "/NATSBridge_text_test"
|
|
||||||
const NATS_URL = "nats.yiem.cc"
|
|
||||||
const FILESERVER_URL = "http://192.168.88.104:8080"
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------------------------ #
|
|
||||||
# test text transfer #
|
|
||||||
# ------------------------------------------------------------------------------------------------ #
|
|
||||||
|
|
||||||
|
|
||||||
# Helper: Log with correlation ID
|
|
||||||
function log_trace(message)
|
|
||||||
timestamp = Dates.now()
|
|
||||||
println("[$timestamp] $message")
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# Receiver: Listen for messages and verify text handling
|
|
||||||
function test_text_receive()
|
|
||||||
conn = NATS.connect(NATS_URL)
|
|
||||||
NATS.subscribe(conn, SUBJECT) do msg
|
|
||||||
log_trace("Received message on $(msg.subject)")
|
|
||||||
|
|
||||||
# Use NATSBridge.smartreceive to handle the data
|
|
||||||
# API: smartreceive(msg, download_handler; max_retries, base_delay, max_delay)
|
|
||||||
result = NATSBridge.smartreceive(
|
|
||||||
msg;
|
|
||||||
max_retries = 5,
|
|
||||||
base_delay = 100,
|
|
||||||
max_delay = 5000
|
|
||||||
)
|
|
||||||
|
|
||||||
# Result is an envelope dictionary with payloads field containing list of (dataname, data, data_type) tuples
|
|
||||||
for (dataname, data, data_type) in result["payloads"]
|
|
||||||
if isa(data, String)
|
|
||||||
log_trace("Received text '$dataname' of type $data_type")
|
|
||||||
log_trace(" Length: $(length(data)) characters")
|
|
||||||
|
|
||||||
# Display first 100 characters
|
|
||||||
if length(data) > 100
|
|
||||||
log_trace(" First 100 characters: $(data[1:100])...")
|
|
||||||
else
|
|
||||||
log_trace(" Content: $data")
|
|
||||||
end
|
|
||||||
|
|
||||||
# Save to file
|
|
||||||
output_path = "./received_$dataname.txt"
|
|
||||||
write(output_path, data)
|
|
||||||
log_trace("Saved text to $output_path")
|
|
||||||
else
|
|
||||||
log_trace("Received unexpected data type for '$dataname': $(typeof(data))")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Keep listening for 10 seconds
|
|
||||||
sleep(120)
|
|
||||||
NATS.drain(conn)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# Run the test
|
|
||||||
println("Starting text transport test...")
|
|
||||||
println("Note: This receiver will wait for messages from the sender.")
|
|
||||||
println("Run test_julia_to_julia_text_sender.jl first to send test data.")
|
|
||||||
|
|
||||||
# Run receiver
|
|
||||||
println("testing smartreceive for text")
|
|
||||||
test_text_receive()
|
|
||||||
|
|
||||||
println("Test completed.")
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
#!/usr/bin/env julia
|
|
||||||
# Test script for text transport testing
|
|
||||||
# Tests sending 1 large and 1 small text from Julia serviceA to Julia serviceB
|
|
||||||
# Uses NATSBridge.jl smartsend with "text" type
|
|
||||||
|
|
||||||
using NATS, JSON, UUIDs, Dates, PrettyPrinting, DataFrames, Arrow, HTTP
|
|
||||||
|
|
||||||
# Include the bridge module
|
|
||||||
include("../src/NATSBridge.jl")
|
|
||||||
using .NATSBridge
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
const SUBJECT = "/NATSBridge_text_test"
|
|
||||||
const NATS_URL = "nats.yiem.cc"
|
|
||||||
const FILESERVER_URL = "http://192.168.88.104:8080"
|
|
||||||
|
|
||||||
# Create correlation ID for tracing
|
|
||||||
correlation_id = string(uuid4())
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------------------------ #
|
|
||||||
# test text transfer #
|
|
||||||
# ------------------------------------------------------------------------------------------------ #
|
|
||||||
|
|
||||||
|
|
||||||
# Helper: Log with correlation ID
|
|
||||||
function log_trace(message)
|
|
||||||
timestamp = Dates.now()
|
|
||||||
println("[$timestamp] [Correlation: $correlation_id] $message")
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# File upload handler for plik server
|
|
||||||
function plik_upload_handler(fileserver_url::String, dataname::String, data::Vector{UInt8})::Dict{String, Any}
|
|
||||||
# Get upload ID
|
|
||||||
url_getUploadID = "$fileserver_url/upload"
|
|
||||||
headers = ["Content-Type" => "application/json"]
|
|
||||||
body = """{ "OneShot" : true }"""
|
|
||||||
httpResponse = HTTP.request("POST", url_getUploadID, headers, body; body_is_form=false)
|
|
||||||
responseJson = JSON.parse(String(httpResponse.body))
|
|
||||||
uploadid = responseJson["id"]
|
|
||||||
uploadtoken = responseJson["uploadToken"]
|
|
||||||
|
|
||||||
# Upload file
|
|
||||||
file_multipart = HTTP.Multipart(dataname, IOBuffer(data), "application/octet-stream")
|
|
||||||
url_upload = "$fileserver_url/file/$uploadid"
|
|
||||||
headers = ["X-UploadToken" => uploadtoken]
|
|
||||||
|
|
||||||
form = HTTP.Form(Dict("file" => file_multipart))
|
|
||||||
httpResponse = HTTP.post(url_upload, headers, form)
|
|
||||||
responseJson = JSON.parse(String(httpResponse.body))
|
|
||||||
|
|
||||||
fileid = responseJson["id"]
|
|
||||||
url = "$fileserver_url/file/$uploadid/$fileid/$dataname"
|
|
||||||
|
|
||||||
return Dict("status" => httpResponse.status, "uploadid" => uploadid, "fileid" => fileid, "url" => url)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# Sender: Send text via smartsend
|
|
||||||
function test_text_send()
|
|
||||||
# Create a small text (will use direct transport)
|
|
||||||
small_text = "Hello, this is a small text message. Testing direct transport via NATS."
|
|
||||||
|
|
||||||
# Create a large text (will use link transport if > 1MB)
|
|
||||||
# Generate a larger text (~2MB to ensure link transport)
|
|
||||||
large_text = join(["Line $i: This is a sample text line with some content to pad the size. " for i in 1:50000], "")
|
|
||||||
|
|
||||||
# Test data 1: small text
|
|
||||||
data1 = ("small_text", small_text, "text")
|
|
||||||
|
|
||||||
# Test data 2: large text
|
|
||||||
data2 = ("large_text", large_text, "text")
|
|
||||||
|
|
||||||
# Use smartsend with text type
|
|
||||||
# For small text: will use direct transport (Base64 encoded UTF-8)
|
|
||||||
# For large text: will use link transport (uploaded to fileserver)
|
|
||||||
env = NATSBridge.smartsend(
|
|
||||||
SUBJECT,
|
|
||||||
[data1, data2]; # List of (dataname, data, type) tuples
|
|
||||||
nats_url = NATS_URL,
|
|
||||||
fileserver_url = FILESERVER_URL,
|
|
||||||
fileserverUploadHandler = plik_upload_handler,
|
|
||||||
size_threshold = 1_000_000, # 1MB threshold
|
|
||||||
correlation_id = correlation_id,
|
|
||||||
msg_purpose = "chat",
|
|
||||||
sender_name = "text_sender",
|
|
||||||
receiver_name = "",
|
|
||||||
receiver_id = "",
|
|
||||||
reply_to = "",
|
|
||||||
reply_to_msg_id = ""
|
|
||||||
)
|
|
||||||
|
|
||||||
log_trace("Sent message with $(length(env.payloads)) payloads")
|
|
||||||
|
|
||||||
# Log transport type for each payload
|
|
||||||
for (i, payload) in enumerate(env.payloads)
|
|
||||||
log_trace("Payload $i ('$payload.dataname'):")
|
|
||||||
log_trace(" Transport: $(payload.transport)")
|
|
||||||
log_trace(" Type: $(payload.type)")
|
|
||||||
log_trace(" Size: $(payload.size) bytes")
|
|
||||||
log_trace(" Encoding: $(payload.encoding)")
|
|
||||||
|
|
||||||
if payload.transport == "link"
|
|
||||||
log_trace(" URL: $(payload.data)")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# Run the test
|
|
||||||
println("Starting text transport test...")
|
|
||||||
println("Correlation ID: $correlation_id")
|
|
||||||
|
|
||||||
# Run sender
|
|
||||||
println("start smartsend for text")
|
|
||||||
test_text_send()
|
|
||||||
|
|
||||||
println("Test completed.")
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Basic functionality test for nats_bridge.py
|
|
||||||
Tests the core classes and functions without NATS connection
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Add src to path for import
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
|
||||||
|
|
||||||
from nats_bridge import (
|
|
||||||
MessagePayload,
|
|
||||||
MessageEnvelope,
|
|
||||||
smartsend,
|
|
||||||
smartreceive,
|
|
||||||
log_trace,
|
|
||||||
generate_uuid,
|
|
||||||
get_timestamp,
|
|
||||||
_serialize_data,
|
|
||||||
_deserialize_data
|
|
||||||
)
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
def test_message_payload():
|
|
||||||
"""Test MessagePayload class"""
|
|
||||||
print("\n=== Testing MessagePayload ===")
|
|
||||||
|
|
||||||
# Test direct transport with text
|
|
||||||
payload1 = MessagePayload(
|
|
||||||
data="Hello World",
|
|
||||||
msg_type="text",
|
|
||||||
id="test-id-1",
|
|
||||||
dataname="message",
|
|
||||||
transport="direct",
|
|
||||||
encoding="base64",
|
|
||||||
size=11
|
|
||||||
)
|
|
||||||
|
|
||||||
assert payload1.id == "test-id-1"
|
|
||||||
assert payload1.dataname == "message"
|
|
||||||
assert payload1.type == "text"
|
|
||||||
assert payload1.transport == "direct"
|
|
||||||
assert payload1.encoding == "base64"
|
|
||||||
assert payload1.size == 11
|
|
||||||
print(" [PASS] MessagePayload with text data")
|
|
||||||
|
|
||||||
# Test link transport with URL
|
|
||||||
payload2 = MessagePayload(
|
|
||||||
data="http://example.com/file.txt",
|
|
||||||
msg_type="binary",
|
|
||||||
id="test-id-2",
|
|
||||||
dataname="file",
|
|
||||||
transport="link",
|
|
||||||
encoding="none",
|
|
||||||
size=1000
|
|
||||||
)
|
|
||||||
|
|
||||||
assert payload2.transport == "link"
|
|
||||||
assert payload2.data == "http://example.com/file.txt"
|
|
||||||
print(" [PASS] MessagePayload with link transport")
|
|
||||||
|
|
||||||
# Test to_dict method
|
|
||||||
payload_dict = payload1.to_dict()
|
|
||||||
assert "id" in payload_dict
|
|
||||||
assert "dataname" in payload_dict
|
|
||||||
assert "type" in payload_dict
|
|
||||||
assert "transport" in payload_dict
|
|
||||||
assert "data" in payload_dict
|
|
||||||
print(" [PASS] MessagePayload.to_dict() method")
|
|
||||||
|
|
||||||
|
|
||||||
def test_message_envelope():
|
|
||||||
"""Test MessageEnvelope class"""
|
|
||||||
print("\n=== Testing MessageEnvelope ===")
|
|
||||||
|
|
||||||
# Create payloads
|
|
||||||
payload1 = MessagePayload("Hello", "text", id="p1", dataname="msg1")
|
|
||||||
payload2 = MessagePayload("http://example.com/file", "binary", id="p2", dataname="file", transport="link")
|
|
||||||
|
|
||||||
# Create envelope
|
|
||||||
env = MessageEnvelope(
|
|
||||||
send_to="/test/subject",
|
|
||||||
payloads=[payload1, payload2],
|
|
||||||
correlation_id="test-correlation-id",
|
|
||||||
msg_id="test-msg-id",
|
|
||||||
msg_purpose="chat",
|
|
||||||
sender_name="test_sender",
|
|
||||||
receiver_name="test_receiver",
|
|
||||||
reply_to="/test/reply"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert env.send_to == "/test/subject"
|
|
||||||
assert env.correlation_id == "test-correlation-id"
|
|
||||||
assert env.msg_id == "test-msg-id"
|
|
||||||
assert env.msg_purpose == "chat"
|
|
||||||
assert len(env.payloads) == 2
|
|
||||||
print(" [PASS] MessageEnvelope creation")
|
|
||||||
|
|
||||||
# Test to_json method
|
|
||||||
json_str = env.to_json()
|
|
||||||
json_data = json.loads(json_str)
|
|
||||||
assert json_data["sendTo"] == "/test/subject"
|
|
||||||
assert json_data["correlationId"] == "test-correlation-id"
|
|
||||||
assert json_data["msgPurpose"] == "chat"
|
|
||||||
assert len(json_data["payloads"]) == 2
|
|
||||||
print(" [PASS] MessageEnvelope.to_json() method")
|
|
||||||
|
|
||||||
|
|
||||||
def test_serialize_data():
|
|
||||||
"""Test _serialize_data function"""
|
|
||||||
print("\n=== Testing _serialize_data ===")
|
|
||||||
|
|
||||||
# Test text serialization
|
|
||||||
text_bytes = _serialize_data("Hello", "text")
|
|
||||||
assert isinstance(text_bytes, bytes)
|
|
||||||
assert text_bytes == b"Hello"
|
|
||||||
print(" [PASS] Text serialization")
|
|
||||||
|
|
||||||
# Test dictionary serialization
|
|
||||||
dict_data = {"key": "value", "number": 42}
|
|
||||||
dict_bytes = _serialize_data(dict_data, "dictionary")
|
|
||||||
assert isinstance(dict_bytes, bytes)
|
|
||||||
parsed = json.loads(dict_bytes.decode('utf-8'))
|
|
||||||
assert parsed["key"] == "value"
|
|
||||||
print(" [PASS] Dictionary serialization")
|
|
||||||
|
|
||||||
# Test binary serialization
|
|
||||||
binary_data = b"\x00\x01\x02"
|
|
||||||
binary_bytes = _serialize_data(binary_data, "binary")
|
|
||||||
assert binary_bytes == b"\x00\x01\x02"
|
|
||||||
print(" [PASS] Binary serialization")
|
|
||||||
|
|
||||||
# Test image serialization
|
|
||||||
image_data = bytes([1, 2, 3, 4, 5])
|
|
||||||
image_bytes = _serialize_data(image_data, "image")
|
|
||||||
assert image_bytes == image_data
|
|
||||||
print(" [PASS] Image serialization")
|
|
||||||
|
|
||||||
|
|
||||||
def test_deserialize_data():
|
|
||||||
"""Test _deserialize_data function"""
|
|
||||||
print("\n=== Testing _deserialize_data ===")
|
|
||||||
|
|
||||||
# Test text deserialization
|
|
||||||
text_bytes = b"Hello"
|
|
||||||
text_data = _deserialize_data(text_bytes, "text", "test-correlation-id")
|
|
||||||
assert text_data == "Hello"
|
|
||||||
print(" [PASS] Text deserialization")
|
|
||||||
|
|
||||||
# Test dictionary deserialization
|
|
||||||
dict_bytes = b'{"key": "value"}'
|
|
||||||
dict_data = _deserialize_data(dict_bytes, "dictionary", "test-correlation-id")
|
|
||||||
assert dict_data == {"key": "value"}
|
|
||||||
print(" [PASS] Dictionary deserialization")
|
|
||||||
|
|
||||||
# Test binary deserialization
|
|
||||||
binary_data = b"\x00\x01\x02"
|
|
||||||
binary_result = _deserialize_data(binary_data, "binary", "test-correlation-id")
|
|
||||||
assert binary_result == b"\x00\x01\x02"
|
|
||||||
print(" [PASS] Binary deserialization")
|
|
||||||
|
|
||||||
|
|
||||||
def test_utilities():
|
|
||||||
"""Test utility functions"""
|
|
||||||
print("\n=== Testing Utility Functions ===")
|
|
||||||
|
|
||||||
# Test generate_uuid
|
|
||||||
uuid1 = generate_uuid()
|
|
||||||
uuid2 = generate_uuid()
|
|
||||||
assert uuid1 != uuid2
|
|
||||||
print(f" [PASS] generate_uuid() - generated: {uuid1}")
|
|
||||||
|
|
||||||
# Test get_timestamp
|
|
||||||
timestamp = get_timestamp()
|
|
||||||
assert "T" in timestamp
|
|
||||||
print(f" [PASS] get_timestamp() - generated: {timestamp}")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Run all tests"""
|
|
||||||
print("=" * 60)
|
|
||||||
print("NATSBridge Python/Micropython - Basic Functionality Tests")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
try:
|
|
||||||
test_message_payload()
|
|
||||||
test_message_envelope()
|
|
||||||
test_serialize_data()
|
|
||||||
test_deserialize_data()
|
|
||||||
test_utilities()
|
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print("ALL TESTS PASSED!")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"\n[FAIL] Test failed with error: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test script for dictionary transport testing - Receiver
|
|
||||||
Tests receiving dictionary messages via NATS using nats_bridge.py smartreceive
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
|
|
||||||
# Add src to path for import
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
|
||||||
|
|
||||||
from nats_bridge import smartreceive, log_trace
|
|
||||||
import nats
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
SUBJECT = "/NATSBridge_dict_test"
|
|
||||||
NATS_URL = "nats://nats.yiem.cc:4222"
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
log_trace("", f"Starting dictionary transport receiver test...")
|
|
||||||
log_trace("", f"Note: This receiver will wait for messages from the sender.")
|
|
||||||
log_trace("", f"Run test_micropython_dict_sender.py first to send test data.")
|
|
||||||
|
|
||||||
# Connect to NATS
|
|
||||||
nc = await nats.connect(NATS_URL)
|
|
||||||
log_trace("", f"Connected to NATS at {NATS_URL}")
|
|
||||||
|
|
||||||
# Subscribe to the subject
|
|
||||||
async def message_handler(msg):
|
|
||||||
log_trace("", f"Received message on {msg.subject}")
|
|
||||||
|
|
||||||
# Use smartreceive to handle the data
|
|
||||||
result = smartreceive(msg.data)
|
|
||||||
|
|
||||||
# Result is an envelope dictionary with payloads field containing list of (dataname, data, data_type) tuples
|
|
||||||
for dataname, data, data_type in result["payloads"]:
|
|
||||||
if isinstance(data, dict):
|
|
||||||
log_trace(result.get("correlationId", ""), f"Received dictionary '{dataname}' of type {data_type}")
|
|
||||||
log_trace(result.get("correlationId", ""), f" Keys: {list(data.keys())}")
|
|
||||||
|
|
||||||
# Display first few items for small dicts
|
|
||||||
if isinstance(data, dict) and len(data) <= 10:
|
|
||||||
log_trace(result.get("correlationId", ""), f" Content: {json.dumps(data, indent=2)}")
|
|
||||||
else:
|
|
||||||
# For large dicts, show summary
|
|
||||||
log_trace(result.get("correlationId", ""), f" Summary: {json.dumps(data, default=str)[:200]}...")
|
|
||||||
|
|
||||||
# Save to file
|
|
||||||
output_path = f"./received_{dataname}.json"
|
|
||||||
with open(output_path, 'w') as f:
|
|
||||||
json.dump(data, f, indent=2)
|
|
||||||
log_trace(result.get("correlationId", ""), f"Saved dictionary to {output_path}")
|
|
||||||
else:
|
|
||||||
log_trace(result.get("correlationId", ""), f"Received unexpected data type for '{dataname}': {type(data)}")
|
|
||||||
|
|
||||||
sid = await nc.subscribe(SUBJECT, cb=message_handler)
|
|
||||||
log_trace("", f"Subscribed to {SUBJECT} with subscription ID: {sid}")
|
|
||||||
|
|
||||||
# Keep listening for 120 seconds
|
|
||||||
await asyncio.sleep(120)
|
|
||||||
await nc.close()
|
|
||||||
log_trace("", "Test completed.")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(main())
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test script for dictionary transport testing - Micropython
|
|
||||||
Tests sending dictionary messages via NATS using nats_bridge.py smartsend
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Add src to path for import
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
|
||||||
|
|
||||||
from nats_bridge import smartsend, log_trace
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
SUBJECT = "/NATSBridge_dict_test"
|
|
||||||
NATS_URL = "nats://nats.yiem.cc:4222"
|
|
||||||
FILESERVER_URL = "http://192.168.88.104:8080"
|
|
||||||
SIZE_THRESHOLD = 1_000_000 # 1MB
|
|
||||||
|
|
||||||
# Create correlation ID for tracing
|
|
||||||
correlation_id = str(uuid.uuid4())
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# Create a small dictionary (will use direct transport)
|
|
||||||
small_dict = {
|
|
||||||
"name": "test",
|
|
||||||
"value": 42,
|
|
||||||
"enabled": True,
|
|
||||||
"metadata": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"timestamp": "2026-02-22T12:00:00Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Create a large dictionary (will use link transport if > 1MB)
|
|
||||||
# Generate a larger dictionary (~2MB to ensure link transport)
|
|
||||||
large_dict = {
|
|
||||||
"id": str(uuid.uuid4()),
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"index": i,
|
|
||||||
"name": f"item_{i}",
|
|
||||||
"value": i * 1.5,
|
|
||||||
"data": "x" * 10000 # Large string per item
|
|
||||||
}
|
|
||||||
for i in range(200)
|
|
||||||
],
|
|
||||||
"metadata": {
|
|
||||||
"count": 200,
|
|
||||||
"created": "2026-02-22T12:00:00Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Test data 1: small dictionary
|
|
||||||
data1 = ("small_dict", small_dict, "dictionary")
|
|
||||||
|
|
||||||
# Test data 2: large dictionary
|
|
||||||
data2 = ("large_dict", large_dict, "dictionary")
|
|
||||||
|
|
||||||
log_trace(correlation_id, f"Starting smartsend for subject: {SUBJECT}")
|
|
||||||
log_trace(correlation_id, f"Correlation ID: {correlation_id}")
|
|
||||||
|
|
||||||
# Use smartsend with dictionary type
|
|
||||||
env = smartsend(
|
|
||||||
SUBJECT,
|
|
||||||
[data1, data2], # List of (dataname, data, type) tuples
|
|
||||||
nats_url=NATS_URL,
|
|
||||||
fileserver_url=FILESERVER_URL,
|
|
||||||
size_threshold=SIZE_THRESHOLD,
|
|
||||||
correlation_id=correlation_id,
|
|
||||||
msg_purpose="chat",
|
|
||||||
sender_name="dict_sender",
|
|
||||||
receiver_name="",
|
|
||||||
receiver_id="",
|
|
||||||
reply_to="",
|
|
||||||
reply_to_msg_id=""
|
|
||||||
)
|
|
||||||
|
|
||||||
log_trace(correlation_id, f"Sent message with {len(env.payloads)} payloads")
|
|
||||||
|
|
||||||
# Log transport type for each payload
|
|
||||||
for i, payload in enumerate(env.payloads):
|
|
||||||
log_trace(correlation_id, f"Payload {i+1} ('{payload.dataname}'):")
|
|
||||||
log_trace(correlation_id, f" Transport: {payload.transport}")
|
|
||||||
log_trace(correlation_id, f" Type: {payload.type}")
|
|
||||||
log_trace(correlation_id, f" Size: {payload.size} bytes")
|
|
||||||
log_trace(correlation_id, f" Encoding: {payload.encoding}")
|
|
||||||
|
|
||||||
if payload.transport == "link":
|
|
||||||
log_trace(correlation_id, f" URL: {payload.data}")
|
|
||||||
|
|
||||||
print(f"Test completed. Correlation ID: {correlation_id}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test script for file transport testing - Receiver
|
|
||||||
Tests receiving binary files via NATS using nats_bridge.py smartreceive
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Add src to path for import
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
|
||||||
|
|
||||||
from nats_bridge import smartreceive, log_trace
|
|
||||||
import nats
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
SUBJECT = "/NATSBridge_file_test"
|
|
||||||
NATS_URL = "nats://nats.yiem.cc:4222"
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
log_trace("", f"Starting file transport receiver test...")
|
|
||||||
log_trace("", f"Note: This receiver will wait for messages from the sender.")
|
|
||||||
log_trace("", f"Run test_micropython_file_sender.py first to send test data.")
|
|
||||||
|
|
||||||
# Connect to NATS
|
|
||||||
nc = await nats.connect(NATS_URL)
|
|
||||||
log_trace("", f"Connected to NATS at {NATS_URL}")
|
|
||||||
|
|
||||||
# Subscribe to the subject
|
|
||||||
async def message_handler(msg):
|
|
||||||
log_trace("", f"Received message on {msg.subject}")
|
|
||||||
|
|
||||||
# Use smartreceive to handle the data
|
|
||||||
result = smartreceive(msg.data)
|
|
||||||
|
|
||||||
# Result is an envelope dictionary with payloads field containing list of (dataname, data, data_type) tuples
|
|
||||||
for dataname, data, data_type in result["payloads"]:
|
|
||||||
if isinstance(data, bytes):
|
|
||||||
log_trace(result.get("correlationId", ""), f"Received binary '{dataname}' of type {data_type}")
|
|
||||||
log_trace(result.get("correlationId", ""), f" Size: {len(data)} bytes")
|
|
||||||
|
|
||||||
# Display first 100 bytes as hex
|
|
||||||
log_trace(result.get("correlationId", ""), f" First 100 bytes (hex): {data[:100].hex()}")
|
|
||||||
|
|
||||||
# Save to file
|
|
||||||
output_path = f"./received_{dataname}.bin"
|
|
||||||
with open(output_path, 'wb') as f:
|
|
||||||
f.write(data)
|
|
||||||
log_trace(result.get("correlationId", ""), f"Saved binary to {output_path}")
|
|
||||||
else:
|
|
||||||
log_trace(result.get("correlationId", ""), f"Received unexpected data type for '{dataname}': {type(data)}")
|
|
||||||
|
|
||||||
sid = await nc.subscribe(SUBJECT, cb=message_handler)
|
|
||||||
log_trace("", f"Subscribed to {SUBJECT} with subscription ID: {sid}")
|
|
||||||
|
|
||||||
# Keep listening for 120 seconds
|
|
||||||
await asyncio.sleep(120)
|
|
||||||
await nc.close()
|
|
||||||
log_trace("", "Test completed.")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(main())
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test script for file transport testing - Micropython
|
|
||||||
Tests sending binary files via NATS using nats_bridge.py smartsend
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Add src to path for import
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
|
||||||
|
|
||||||
from nats_bridge import smartsend, log_trace
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
SUBJECT = "/NATSBridge_file_test"
|
|
||||||
NATS_URL = "nats://nats.yiem.cc:4222"
|
|
||||||
FILESERVER_URL = "http://192.168.88.104:8080"
|
|
||||||
SIZE_THRESHOLD = 1_000_000 # 1MB
|
|
||||||
|
|
||||||
# Create correlation ID for tracing
|
|
||||||
correlation_id = str(uuid.uuid4())
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# Create small binary data (will use direct transport)
|
|
||||||
small_binary = b"This is small binary data for testing direct transport."
|
|
||||||
small_binary += b"\x00" * 100 # Add some null bytes
|
|
||||||
|
|
||||||
# Create large binary data (will use link transport if > 1MB)
|
|
||||||
# Generate a larger binary (~2MB to ensure link transport)
|
|
||||||
large_binary = bytes([
|
|
||||||
(i * 7) % 256 for i in range(2_000_000)
|
|
||||||
])
|
|
||||||
|
|
||||||
# Test data 1: small binary (direct transport)
|
|
||||||
data1 = ("small_binary", small_binary, "binary")
|
|
||||||
|
|
||||||
# Test data 2: large binary (link transport)
|
|
||||||
data2 = ("large_binary", large_binary, "binary")
|
|
||||||
|
|
||||||
log_trace(correlation_id, f"Starting smartsend for subject: {SUBJECT}")
|
|
||||||
log_trace(correlation_id, f"Correlation ID: {correlation_id}")
|
|
||||||
|
|
||||||
# Use smartsend with binary type
|
|
||||||
env = smartsend(
|
|
||||||
SUBJECT,
|
|
||||||
[data1, data2], # List of (dataname, data, type) tuples
|
|
||||||
nats_url=NATS_URL,
|
|
||||||
fileserver_url=FILESERVER_URL,
|
|
||||||
size_threshold=SIZE_THRESHOLD,
|
|
||||||
correlation_id=correlation_id,
|
|
||||||
msg_purpose="chat",
|
|
||||||
sender_name="file_sender",
|
|
||||||
receiver_name="",
|
|
||||||
receiver_id="",
|
|
||||||
reply_to="",
|
|
||||||
reply_to_msg_id=""
|
|
||||||
)
|
|
||||||
|
|
||||||
log_trace(correlation_id, f"Sent message with {len(env.payloads)} payloads")
|
|
||||||
|
|
||||||
# Log transport type for each payload
|
|
||||||
for i, payload in enumerate(env.payloads):
|
|
||||||
log_trace(correlation_id, f"Payload {i+1} ('{payload.dataname}'):")
|
|
||||||
log_trace(correlation_id, f" Transport: {payload.transport}")
|
|
||||||
log_trace(correlation_id, f" Type: {payload.type}")
|
|
||||||
log_trace(correlation_id, f" Size: {payload.size} bytes")
|
|
||||||
log_trace(correlation_id, f" Encoding: {payload.encoding}")
|
|
||||||
|
|
||||||
if payload.transport == "link":
|
|
||||||
log_trace(correlation_id, f" URL: {payload.data}")
|
|
||||||
|
|
||||||
print(f"Test completed. Correlation ID: {correlation_id}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test script for mixed payload testing - Receiver
|
|
||||||
Tests receiving mixed payload types via NATS using nats_bridge.py smartreceive
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
|
|
||||||
# Add src to path for import
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
|
||||||
|
|
||||||
from nats_bridge import smartreceive, log_trace
|
|
||||||
import nats
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
SUBJECT = "/NATSBridge_mixed_test"
|
|
||||||
NATS_URL = "nats://nats.yiem.cc:4222"
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
log_trace("", f"Starting mixed payload receiver test...")
|
|
||||||
log_trace("", f"Note: This receiver will wait for messages from the sender.")
|
|
||||||
log_trace("", f"Run test_micropython_mixed_sender.py first to send test data.")
|
|
||||||
|
|
||||||
# Connect to NATS
|
|
||||||
nc = await nats.connect(NATS_URL)
|
|
||||||
log_trace("", f"Connected to NATS at {NATS_URL}")
|
|
||||||
|
|
||||||
# Subscribe to the subject
|
|
||||||
async def message_handler(msg):
|
|
||||||
log_trace("", f"Received message on {msg.subject}")
|
|
||||||
|
|
||||||
# Use smartreceive to handle the data
|
|
||||||
result = smartreceive(msg.data)
|
|
||||||
|
|
||||||
log_trace(result.get("correlationId", ""), f"Received envelope with {len(result['payloads'])} payloads")
|
|
||||||
|
|
||||||
# Result is an envelope dictionary with payloads field containing list of (dataname, data, data_type) tuples
|
|
||||||
for dataname, data, data_type in result["payloads"]:
|
|
||||||
log_trace(result.get("correlationId", ""), f"\n--- Payload: {dataname} (type: {data_type}) ---")
|
|
||||||
|
|
||||||
if isinstance(data, str):
|
|
||||||
log_trace(result.get("correlationId", ""), f" Type: text/string")
|
|
||||||
log_trace(result.get("correlationId", ""), f" Length: {len(data)} characters")
|
|
||||||
if len(data) <= 100:
|
|
||||||
log_trace(result.get("correlationId", ""), f" Content: {data}")
|
|
||||||
else:
|
|
||||||
log_trace(result.get("correlationId", ""), f" First 100 chars: {data[:100]}...")
|
|
||||||
# Save to file
|
|
||||||
output_path = f"./received_{dataname}.txt"
|
|
||||||
with open(output_path, 'w') as f:
|
|
||||||
f.write(data)
|
|
||||||
log_trace(result.get("correlationId", ""), f" Saved to: {output_path}")
|
|
||||||
|
|
||||||
elif isinstance(data, dict):
|
|
||||||
log_trace(result.get("correlationId", ""), f" Type: dictionary")
|
|
||||||
log_trace(result.get("correlationId", ""), f" Keys: {list(data.keys())}")
|
|
||||||
log_trace(result.get("correlationId", ""), f" Content: {json.dumps(data, indent=2)}")
|
|
||||||
# Save to file
|
|
||||||
output_path = f"./received_{dataname}.json"
|
|
||||||
with open(output_path, 'w') as f:
|
|
||||||
json.dump(data, f, indent=2)
|
|
||||||
log_trace(result.get("correlationId", ""), f" Saved to: {output_path}")
|
|
||||||
|
|
||||||
elif isinstance(data, bytes):
|
|
||||||
log_trace(result.get("correlationId", ""), f" Type: binary")
|
|
||||||
log_trace(result.get("correlationId", ""), f" Size: {len(data)} bytes")
|
|
||||||
log_trace(result.get("correlationId", ""), f" First 100 bytes (hex): {data[:100].hex()}")
|
|
||||||
# Save to file
|
|
||||||
output_path = f"./received_{dataname}.bin"
|
|
||||||
with open(output_path, 'wb') as f:
|
|
||||||
f.write(data)
|
|
||||||
log_trace(result.get("correlationId", ""), f" Saved to: {output_path}")
|
|
||||||
else:
|
|
||||||
log_trace(result.get("correlationId", ""), f" Received unexpected data type: {type(data)}")
|
|
||||||
|
|
||||||
# Log envelope metadata
|
|
||||||
log_trace(result.get("correlationId", ""), f"\n--- Envelope Metadata ---")
|
|
||||||
log_trace(result.get("correlationId", ""), f" Correlation ID: {result.get('correlationId', 'N/A')}")
|
|
||||||
log_trace(result.get("correlationId", ""), f" Message ID: {result.get('msgId', 'N/A')}")
|
|
||||||
log_trace(result.get("correlationId", ""), f" Sender: {result.get('senderName', 'N/A')}")
|
|
||||||
log_trace(result.get("correlationId", ""), f" Purpose: {result.get('msgPurpose', 'N/A')}")
|
|
||||||
|
|
||||||
sid = await nc.subscribe(SUBJECT, cb=message_handler)
|
|
||||||
log_trace("", f"Subscribed to {SUBJECT} with subscription ID: {sid}")
|
|
||||||
|
|
||||||
# Keep listening for 120 seconds
|
|
||||||
await asyncio.sleep(120)
|
|
||||||
await nc.close()
|
|
||||||
log_trace("", "Test completed.")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(main())
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test script for mixed payload testing - Micropython
|
|
||||||
Tests sending mixed payload types via NATS using nats_bridge.py smartsend
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Add src to path for import
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
|
||||||
|
|
||||||
from nats_bridge import smartsend, log_trace
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
SUBJECT = "/NATSBridge_mixed_test"
|
|
||||||
NATS_URL = "nats://nats.yiem.cc:4222"
|
|
||||||
FILESERVER_URL = "http://192.168.88.104:8080"
|
|
||||||
SIZE_THRESHOLD = 1_000_000 # 1MB
|
|
||||||
|
|
||||||
# Create correlation ID for tracing
|
|
||||||
correlation_id = str(uuid.uuid4())
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# Create payloads for mixed content test
|
|
||||||
|
|
||||||
# 1. Small text (direct transport)
|
|
||||||
text_data = "Hello, this is a text message for testing mixed payloads!"
|
|
||||||
|
|
||||||
# 2. Small dictionary (direct transport)
|
|
||||||
dict_data = {
|
|
||||||
"status": "ok",
|
|
||||||
"code": 200,
|
|
||||||
"message": "Test successful",
|
|
||||||
"items": [1, 2, 3]
|
|
||||||
}
|
|
||||||
|
|
||||||
# 3. Small binary (direct transport)
|
|
||||||
binary_data = b"\x00\x01\x02\x03\x04\x05" + b"\xff" * 100
|
|
||||||
|
|
||||||
# 4. Large text (link transport - will use fileserver)
|
|
||||||
large_text = "\n".join([
|
|
||||||
f"Line {i}: This is a large text payload for link transport testing. " * 50
|
|
||||||
for i in range(100)
|
|
||||||
])
|
|
||||||
|
|
||||||
# Test data list - mixed payload types
|
|
||||||
data = [
|
|
||||||
("message_text", text_data, "text"),
|
|
||||||
("config_dict", dict_data, "dictionary"),
|
|
||||||
("small_binary", binary_data, "binary"),
|
|
||||||
("large_text", large_text, "text"),
|
|
||||||
]
|
|
||||||
|
|
||||||
log_trace(correlation_id, f"Starting smartsend for subject: {SUBJECT}")
|
|
||||||
log_trace(correlation_id, f"Correlation ID: {correlation_id}")
|
|
||||||
|
|
||||||
# Use smartsend with mixed types
|
|
||||||
env = smartsend(
|
|
||||||
SUBJECT,
|
|
||||||
data, # List of (dataname, data, type) tuples
|
|
||||||
nats_url=NATS_URL,
|
|
||||||
fileserver_url=FILESERVER_URL,
|
|
||||||
size_threshold=SIZE_THRESHOLD,
|
|
||||||
correlation_id=correlation_id,
|
|
||||||
msg_purpose="chat",
|
|
||||||
sender_name="mixed_sender",
|
|
||||||
receiver_name="",
|
|
||||||
receiver_id="",
|
|
||||||
reply_to="",
|
|
||||||
reply_to_msg_id=""
|
|
||||||
)
|
|
||||||
|
|
||||||
log_trace(correlation_id, f"Sent message with {len(env.payloads)} payloads")
|
|
||||||
|
|
||||||
# Log transport type for each payload
|
|
||||||
for i, payload in enumerate(env.payloads):
|
|
||||||
log_trace(correlation_id, f"Payload {i+1} ('{payload.dataname}'):")
|
|
||||||
log_trace(correlation_id, f" Transport: {payload.transport}")
|
|
||||||
log_trace(correlation_id, f" Type: {payload.type}")
|
|
||||||
log_trace(correlation_id, f" Size: {payload.size} bytes")
|
|
||||||
log_trace(correlation_id, f" Encoding: {payload.encoding}")
|
|
||||||
|
|
||||||
if payload.transport == "link":
|
|
||||||
log_trace(correlation_id, f" URL: {payload.data}")
|
|
||||||
|
|
||||||
print(f"Test completed. Correlation ID: {correlation_id}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test script for text transport testing - Receiver
|
|
||||||
Tests receiving text messages via NATS using nats_bridge.py smartreceive
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
|
|
||||||
# Add src to path for import
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
|
||||||
|
|
||||||
from nats_bridge import smartreceive, log_trace
|
|
||||||
import nats
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
SUBJECT = "/NATSBridge_text_test"
|
|
||||||
NATS_URL = "nats://nats.yiem.cc:4222"
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
log_trace("", f"Starting text transport receiver test...")
|
|
||||||
log_trace("", f"Note: This receiver will wait for messages from the sender.")
|
|
||||||
log_trace("", f"Run test_micropython_text_sender.py first to send test data.")
|
|
||||||
|
|
||||||
# Connect to NATS
|
|
||||||
nc = await nats.connect(NATS_URL)
|
|
||||||
log_trace("", f"Connected to NATS at {NATS_URL}")
|
|
||||||
|
|
||||||
# Subscribe to the subject
|
|
||||||
async def message_handler(msg):
|
|
||||||
log_trace("", f"Received message on {msg.subject}")
|
|
||||||
|
|
||||||
# Use smartreceive to handle the data
|
|
||||||
result = smartreceive(msg.data)
|
|
||||||
|
|
||||||
# Result is an envelope dictionary with payloads field containing list of (dataname, data, data_type) tuples
|
|
||||||
for dataname, data, data_type in result["payloads"]:
|
|
||||||
if isinstance(data, str):
|
|
||||||
log_trace(result.get("correlationId", ""), f"Received text '{dataname}' of type {data_type}")
|
|
||||||
log_trace(result.get("correlationId", ""), f" Length: {len(data)} characters")
|
|
||||||
|
|
||||||
# Display first 100 characters
|
|
||||||
if len(data) > 100:
|
|
||||||
log_trace(result.get("correlationId", ""), f" First 100 characters: {data[:100]}...")
|
|
||||||
else:
|
|
||||||
log_trace(result.get("correlationId", ""), f" Content: {data}")
|
|
||||||
|
|
||||||
# Save to file
|
|
||||||
output_path = f"./received_{dataname}.txt"
|
|
||||||
with open(output_path, 'w') as f:
|
|
||||||
f.write(data)
|
|
||||||
log_trace(result.get("correlationId", ""), f"Saved text to {output_path}")
|
|
||||||
else:
|
|
||||||
log_trace(result.get("correlationId", ""), f"Received unexpected data type for '{dataname}': {type(data)}")
|
|
||||||
|
|
||||||
sid = await nc.subscribe(SUBJECT, cb=message_handler)
|
|
||||||
log_trace("", f"Subscribed to {SUBJECT} with subscription ID: {sid}")
|
|
||||||
|
|
||||||
# Keep listening for 120 seconds
|
|
||||||
await asyncio.sleep(120)
|
|
||||||
await nc.close()
|
|
||||||
log_trace("", "Test completed.")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(main())
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Test script for text transport testing - Micropython
|
|
||||||
Tests sending text messages via NATS using nats_bridge.py smartsend
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Add src to path for import
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
|
||||||
|
|
||||||
from nats_bridge import smartsend, log_trace
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
SUBJECT = "/NATSBridge_text_test"
|
|
||||||
NATS_URL = "nats://nats.yiem.cc:4222"
|
|
||||||
FILESERVER_URL = "http://192.168.88.104:8080"
|
|
||||||
SIZE_THRESHOLD = 1_000_000 # 1MB
|
|
||||||
|
|
||||||
# Create correlation ID for tracing
|
|
||||||
correlation_id = str(uuid.uuid4())
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# Create a small text (will use direct transport)
|
|
||||||
small_text = "Hello, this is a small text message. Testing direct transport via NATS."
|
|
||||||
|
|
||||||
# Create a large text (will use link transport if > 1MB)
|
|
||||||
# Generate a larger text (~2MB to ensure link transport)
|
|
||||||
large_text = "\n".join([
|
|
||||||
f"Line {i}: This is a sample text line with some content to pad the size. " * 100
|
|
||||||
for i in range(500)
|
|
||||||
])
|
|
||||||
|
|
||||||
# Test data 1: small text
|
|
||||||
data1 = ("small_text", small_text, "text")
|
|
||||||
|
|
||||||
# Test data 2: large text
|
|
||||||
data2 = ("large_text", large_text, "text")
|
|
||||||
|
|
||||||
log_trace(correlation_id, f"Starting smartsend for subject: {SUBJECT}")
|
|
||||||
log_trace(correlation_id, f"Correlation ID: {correlation_id}")
|
|
||||||
|
|
||||||
# Use smartsend with text type
|
|
||||||
# For small text: will use direct transport (Base64 encoded UTF-8)
|
|
||||||
# For large text: will use link transport (uploaded to fileserver)
|
|
||||||
env = smartsend(
|
|
||||||
SUBJECT,
|
|
||||||
[data1, data2], # List of (dataname, data, type) tuples
|
|
||||||
nats_url=NATS_URL,
|
|
||||||
fileserver_url=FILESERVER_URL,
|
|
||||||
size_threshold=SIZE_THRESHOLD,
|
|
||||||
correlation_id=correlation_id,
|
|
||||||
msg_purpose="chat",
|
|
||||||
sender_name="text_sender",
|
|
||||||
receiver_name="",
|
|
||||||
receiver_id="",
|
|
||||||
reply_to="",
|
|
||||||
reply_to_msg_id=""
|
|
||||||
)
|
|
||||||
|
|
||||||
log_trace(correlation_id, f"Sent message with {len(env.payloads)} payloads")
|
|
||||||
|
|
||||||
# Log transport type for each payload
|
|
||||||
for i, payload in enumerate(env.payloads):
|
|
||||||
log_trace(correlation_id, f"Payload {i+1} ('{payload.dataname}'):")
|
|
||||||
log_trace(correlation_id, f" Transport: {payload.transport}")
|
|
||||||
log_trace(correlation_id, f" Type: {payload.type}")
|
|
||||||
log_trace(correlation_id, f" Size: {payload.size} bytes")
|
|
||||||
log_trace(correlation_id, f" Encoding: {payload.encoding}")
|
|
||||||
|
|
||||||
if payload.transport == "link":
|
|
||||||
log_trace(correlation_id, f" URL: {payload.data}")
|
|
||||||
|
|
||||||
print(f"Test completed. Correlation ID: {correlation_id}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
199
test/test_py_mix_payloads_sender.py
Normal file
199
test/test_py_mix_payloads_sender.py
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
"""
|
||||||
|
Python Mix Payloads Sender Test
|
||||||
|
Tests the smartsend function with mixed payload types
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import base64
|
||||||
|
|
||||||
|
# Add parent directory to path
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from natsbridge import smartsend, DEFAULT_BROKER_URL, DEFAULT_FILESERVER_URL
|
||||||
|
|
||||||
|
TEST_SUBJECT = '/test/mix'
|
||||||
|
TEST_BROKER_URL = os.environ.get('NATS_URL', 'nats://localhost:4222')
|
||||||
|
TEST_FILESERVER_URL = os.environ.get('FILESERVER_URL', 'http://localhost:8080')
|
||||||
|
|
||||||
|
|
||||||
|
async def run_test():
|
||||||
|
print('=== Python Mix Payloads Sender Test ===\n')
|
||||||
|
|
||||||
|
correlation_id = 'py-mix-test-' + str(asyncio.get_event_loop().time() * 1000000)
|
||||||
|
print(f'Correlation ID: {correlation_id}')
|
||||||
|
print(f'Subject: {TEST_SUBJECT}')
|
||||||
|
print(f'Broker URL: {TEST_BROKER_URL}\n')
|
||||||
|
|
||||||
|
# Test data - mixed payload types
|
||||||
|
text_data = 'Hello, NATSBridge!'
|
||||||
|
dict_data = {'key1': 'value1', 'key2': 42, 'nested': {'a': 1, 'b': 2}}
|
||||||
|
image_data = bytes([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) # PNG header
|
||||||
|
|
||||||
|
# Table data
|
||||||
|
try:
|
||||||
|
import pandas as pd
|
||||||
|
table_data = pd.DataFrame({
|
||||||
|
'id': [1, 2, 3],
|
||||||
|
'name': ['Alice', 'Bob', 'Charlie'],
|
||||||
|
'age': [30, 25, 35]
|
||||||
|
})
|
||||||
|
table_available = True
|
||||||
|
except ImportError:
|
||||||
|
table_available = False
|
||||||
|
table_data = None
|
||||||
|
|
||||||
|
test_data = [
|
||||||
|
('message', text_data, 'text'),
|
||||||
|
('config', dict_data, 'dictionary'),
|
||||||
|
('image', image_data, 'image')
|
||||||
|
]
|
||||||
|
|
||||||
|
if table_available:
|
||||||
|
test_data.append(('users', table_data, 'table'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Send the message
|
||||||
|
print('Sending mixed payloads...')
|
||||||
|
env, env_json_str = await smartsend(
|
||||||
|
TEST_SUBJECT,
|
||||||
|
test_data,
|
||||||
|
broker_url=TEST_BROKER_URL,
|
||||||
|
fileserver_url=TEST_FILESERVER_URL,
|
||||||
|
correlation_id=correlation_id,
|
||||||
|
msg_purpose='test',
|
||||||
|
sender_name='py-mix-test',
|
||||||
|
is_publish=False
|
||||||
|
)
|
||||||
|
|
||||||
|
print('\n=== Envelope Created ===')
|
||||||
|
print(f'Correlation ID: {env["correlation_id"]}')
|
||||||
|
print(f'Message ID: {env["msg_id"]}')
|
||||||
|
print(f'Timestamp: {env["timestamp"]}')
|
||||||
|
print(f'Subject: {env["send_to"]}')
|
||||||
|
print(f'Purpose: {env["msg_purpose"]}')
|
||||||
|
print(f'Sender: {env["sender_name"]}')
|
||||||
|
print(f'Payloads: {len(env["payloads"])}\n')
|
||||||
|
|
||||||
|
# Validate envelope structure
|
||||||
|
print('=== Validation ===')
|
||||||
|
passed = True
|
||||||
|
|
||||||
|
expected_count = 4 if table_available else 3
|
||||||
|
if len(env['payloads']) != expected_count:
|
||||||
|
print(f'❌ Expected {expected_count} payloads, got {len(env["payloads"])}')
|
||||||
|
passed = False
|
||||||
|
else:
|
||||||
|
print('✅ Correct number of payloads')
|
||||||
|
|
||||||
|
# Test each payload
|
||||||
|
expected_datanames = ['message', 'config', 'image']
|
||||||
|
expected_types = ['text', 'dictionary', 'image']
|
||||||
|
expected_data = [text_data, dict_data, image_data]
|
||||||
|
|
||||||
|
if table_available:
|
||||||
|
expected_datanames.append('users')
|
||||||
|
expected_types.append('table')
|
||||||
|
|
||||||
|
for i in range(len(env['payloads'])):
|
||||||
|
payload = env['payloads'][i]
|
||||||
|
|
||||||
|
if payload['dataname'] != expected_datanames[i]:
|
||||||
|
print(f"❌ Payload {i + 1}: Expected dataname '{expected_datanames[i]}', got '{payload['dataname']}'")
|
||||||
|
passed = False
|
||||||
|
else:
|
||||||
|
print(f'✅ Payload {i + 1}: Correct dataname')
|
||||||
|
|
||||||
|
if payload['payload_type'] != expected_types[i]:
|
||||||
|
print(f"❌ Payload {i + 1}: Expected type '{expected_types[i]}', got '{payload['payload_type']}'")
|
||||||
|
passed = False
|
||||||
|
else:
|
||||||
|
print(f'✅ Payload {i + 1}: Correct type')
|
||||||
|
|
||||||
|
if payload['transport'] != 'direct':
|
||||||
|
print(f"❌ Payload {i + 1}: Expected transport 'direct', got '{payload['transport']}'")
|
||||||
|
passed = False
|
||||||
|
else:
|
||||||
|
print(f'✅ Payload {i + 1}: Correct transport')
|
||||||
|
|
||||||
|
if payload['encoding'] != 'base64':
|
||||||
|
print(f"❌ Payload {i + 1}: Expected encoding 'base64', got '{payload['encoding']}'")
|
||||||
|
passed = False
|
||||||
|
else:
|
||||||
|
print(f'✅ Payload {i + 1}: Correct encoding')
|
||||||
|
|
||||||
|
# Verify data integrity based on type
|
||||||
|
decoded_data = base64.b64decode(payload['data'])
|
||||||
|
|
||||||
|
if expected_types[i] == 'text':
|
||||||
|
decoded_text = decoded_data.decode('utf8')
|
||||||
|
if decoded_text != expected_data[i]:
|
||||||
|
print(f'❌ Payload {i + 1}: Data integrity mismatch')
|
||||||
|
passed = False
|
||||||
|
else:
|
||||||
|
print(f'✅ Payload {i + 1}: Data integrity verified')
|
||||||
|
elif expected_types[i] == 'dictionary':
|
||||||
|
import json
|
||||||
|
decoded_dict = json.loads(decoded_data.decode('utf8'))
|
||||||
|
if json.dumps(decoded_dict, sort_keys=True) != json.dumps(expected_data[i], sort_keys=True):
|
||||||
|
print(f'❌ Payload {i + 1}: Data integrity mismatch')
|
||||||
|
passed = False
|
||||||
|
else:
|
||||||
|
print(f'✅ Payload {i + 1}: Data integrity verified')
|
||||||
|
elif expected_types[i] == 'image':
|
||||||
|
if decoded_data != expected_data[i]:
|
||||||
|
print(f'❌ Payload {i + 1}: Data integrity mismatch')
|
||||||
|
passed = False
|
||||||
|
else:
|
||||||
|
print(f'✅ Payload {i + 1}: Data integrity verified')
|
||||||
|
elif expected_types[i] == 'table':
|
||||||
|
if len(decoded_data) > 0:
|
||||||
|
print(f'✅ Payload {i + 1}: Arrow IPC data present ({len(decoded_data)} bytes)')
|
||||||
|
else:
|
||||||
|
print(f'❌ Payload {i + 1}: Arrow IPC data is empty')
|
||||||
|
passed = False
|
||||||
|
|
||||||
|
print(f' Size: {payload["size"]} bytes\n')
|
||||||
|
|
||||||
|
# Test with chat-like payload (text + image + audio)
|
||||||
|
print('=== Chat-like Payload Test ===')
|
||||||
|
chat_data = [
|
||||||
|
('text', 'Hello!', 'text'),
|
||||||
|
('image', bytes([0xFF, 0xD8, 0xFF, 0xE0]), 'image'),
|
||||||
|
('audio', bytes([0x46, 0x4C, 0x41, 0x43]), 'audio')
|
||||||
|
]
|
||||||
|
|
||||||
|
chat_env, _ = await smartsend(
|
||||||
|
TEST_SUBJECT,
|
||||||
|
chat_data,
|
||||||
|
broker_url=TEST_BROKER_URL,
|
||||||
|
fileserver_url=TEST_FILESERVER_URL,
|
||||||
|
correlation_id='chat-' + correlation_id,
|
||||||
|
is_publish=False
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(chat_env['payloads']) == 3:
|
||||||
|
print('✅ Chat-like payloads handled correctly')
|
||||||
|
else:
|
||||||
|
print('❌ Chat-like payloads handling failed')
|
||||||
|
passed = False
|
||||||
|
|
||||||
|
# Final result
|
||||||
|
print('\n=== Test Result ===')
|
||||||
|
if passed:
|
||||||
|
print('✅ ALL TESTS PASSED')
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
print('❌ SOME TESTS FAILED')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f'❌ Test failed with error: {e}')
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(run_test())
|
||||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user