Compare commits
31 Commits
1b86a9252d
...
v0.5.5
| 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 |
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
package.json
|
||||||
|
package-lock.json
|
||||||
@@ -18,12 +18,9 @@ Create a walkthrough for Julia service-A service sending a mix-content chat mess
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
I updated the following:
|
I updated the following:
|
||||||
- NATSBridge.jl. Essentially I add NATS_connection keyword and new publish_message function to support the keyword.
|
- 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.
|
Use them and ONLY them as ground truth.
|
||||||
|
|
||||||
Then update the following files accordingly:
|
Then update the following files accordingly:
|
||||||
- architecture.md
|
- architecture.md
|
||||||
- implementation.md
|
- implementation.md
|
||||||
@@ -39,11 +36,8 @@ All API should be semantically consistent and naming should be consistent across
|
|||||||
|
|
||||||
|
|
||||||
Task: Update NATSBridge.js to reflect recent changes in NATSBridge.jl and docs
|
Task: Update NATSBridge.js to reflect recent changes in NATSBridge.jl and docs
|
||||||
|
|
||||||
Context: NATSBridge.jl and docs has been updated.
|
Context: NATSBridge.jl and docs has been updated.
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
|
|
||||||
Source of Truth: Treat the updated NATSBridge.jl and docs as the definitive source.
|
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.
|
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.
|
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.
|
||||||
@@ -63,3 +57,47 @@ Now, help me do the following:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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.5"
|
version = "0.5.4"
|
||||||
authors = ["narawat <narawat@gmail.com>"]
|
authors = ["narawat <narawat@gmail.com>"]
|
||||||
|
|
||||||
[deps]
|
[deps]
|
||||||
|
|||||||
425
README.md
425
README.md
@@ -12,14 +12,12 @@ A high-performance, bi-directional data bridge for **Julia, JavaScript, Python,
|
|||||||
- [Overview](#overview)
|
- [Overview](#overview)
|
||||||
- [Cross-Platform Support](#cross-platform-support)
|
- [Cross-Platform Support](#cross-platform-support)
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
- [Architecture](#architecture)
|
|
||||||
- [Installation](#installation)
|
|
||||||
- [Quick Start](#quick-start)
|
- [Quick Start](#quick-start)
|
||||||
- [API Reference](#api-reference)
|
- [API Reference](#api-reference)
|
||||||
- [Payload Types](#payload-types)
|
- [Payload Types](#payload-types)
|
||||||
- [Transport Strategies](#transport-strategies)
|
|
||||||
- [Cross-Platform Examples](#cross-platform-examples)
|
- [Cross-Platform Examples](#cross-platform-examples)
|
||||||
- [Testing](#testing)
|
- [Testing](#testing)
|
||||||
|
- [Documentation](#documentation)
|
||||||
- [License](#license)
|
- [License](#license)
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -47,23 +45,24 @@ NATSBridge enables seamless communication across multiple platforms through NATS
|
|||||||
| Platform | Implementation | Features |
|
| Platform | Implementation | Features |
|
||||||
|----------|----------------|----------|
|
|----------|----------------|----------|
|
||||||
| **Julia** | [`src/NATSBridge.jl`](src/NATSBridge.jl) | Full feature set, Arrow IPC, multiple dispatch |
|
| **Julia** | [`src/NATSBridge.jl`](src/NATSBridge.jl) | Full feature set, Arrow IPC, multiple dispatch |
|
||||||
| **JavaScript** | [`src/natbridge.js`](src/natbridge.js) | Node.js & browser, async/await |
|
| **JavaScript** | [`src/natsbridge.js`](src/natsbridge.js) | Node.js, async/await |
|
||||||
| **Python** | [`src/natbridge.py`](src/natbridge.py) | Desktop Python, asyncio, type hints |
|
| **JavaScript (Browser)** | [`src/natsbridge_csr.js`](src/natsbridge_csr.js) | Browser, WebSocket NATS, async/await |
|
||||||
| **MicroPython** | [`src/natbridge_mpy.py`](src/natbridge_mpy.py) | Memory-constrained, synchronous API |
|
| **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
|
### Platform Comparison
|
||||||
|
|
||||||
| Feature | Julia | JavaScript | Python | MicroPython |
|
| Feature | Julia | JavaScript | JavaScript (Browser) | Python | MicroPython |
|
||||||
|---------|-------|------------|--------|-------------|
|
|---------|-------|------------|----------------------|--------|-------------|
|
||||||
| Multiple Dispatch | ✅ Native | ❌ | ❌ | ❌ |
|
| Multiple Dispatch | ✅ Native | ❌ | ❌ | ❌ | ❌ |
|
||||||
| Async/Await | ❌ | ✅ Native | ✅ Native | ⚠️ (uasyncio) |
|
| Async/Await | ❌ | ✅ Native | ✅ Native | ✅ Native | ⚠️ (uasyncio) |
|
||||||
| Type Safety | ✅ Strong | ⚠️ (TypeScript) | ✅ (Type hints) | ❌ |
|
| Type Safety | ✅ Strong | ⚠️ (TypeScript) | ⚠️ (TypeScript) | ✅ (Type hints) | ❌ |
|
||||||
| Memory Management | ✅ GC | ✅ GC | ✅ GC | ⚠️ (Manual) |
|
| Arrow IPC | ✅ Native | ✅ | ✅ | ✅ | ❌ |
|
||||||
| Arrow IPC | ✅ Native | ✅ | ✅ | ❌ |
|
| Direct Transport | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
| Direct Transport | ✅ | ✅ | ✅ | ✅ |
|
| Link Transport | ✅ | ✅ | ✅ | ✅ | ⚠️ (Limited) |
|
||||||
| Link Transport | ✅ | ✅ | ✅ | ⚠️ (Limited) |
|
| Handler Functions | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
| Handler Functions | ✅ | ✅ | ✅ | ✅ |
|
| Cross-Platform API | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
| Cross-Platform API | ✅ | ✅ | ✅ | ✅ |
|
| WebSocket NATS | ❌ | ❌ | ✅ | ❌ | ❌ |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -82,171 +81,6 @@ NATSBridge enables seamless communication across multiple platforms through NATS
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### System Components
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TB
|
|
||||||
subgraph Sender["Application (Sender)"]
|
|
||||||
SenderApp[App Code]
|
|
||||||
NATSBridge_Send[NATSBridge]
|
|
||||||
NATS_Client[<b>NATS.jl</b>]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Receiver["Application (Receiver)"]
|
|
||||||
ReceiverApp[App Code]
|
|
||||||
NATSBridge_Recv[NATSBridge]
|
|
||||||
NATS_Client_Recv[<b>NATS.jl</b>]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Infrastructure["Infrastructure"]
|
|
||||||
NATS[<b>NATS Server</b><br/>Message Broker]
|
|
||||||
FileServer[<b>HTTP File Server</b><br/>Upload/Download]
|
|
||||||
end
|
|
||||||
|
|
||||||
SenderApp --> NATSBridge_Send
|
|
||||||
NATSBridge_Send --> NATS_Client
|
|
||||||
NATS_Client --> NATS
|
|
||||||
|
|
||||||
NATS --> NATS_Client_Recv
|
|
||||||
NATS_Client_Recv --> NATSBridge_Recv
|
|
||||||
NATSBridge_Recv --> ReceiverApp
|
|
||||||
|
|
||||||
NATSBridge_Send -.->|HTTP POST upload| FileServer
|
|
||||||
FileServer -.->|HTTP GET download| NATSBridge_Recv
|
|
||||||
|
|
||||||
style SenderApp fill:#e8f5e9
|
|
||||||
style ReceiverApp fill:#e8f5e9
|
|
||||||
style NATS fill:#fff3e0
|
|
||||||
style FileServer fill:#f3e5f5
|
|
||||||
```
|
|
||||||
|
|
||||||
### Message Flow
|
|
||||||
|
|
||||||
1. **Sender** creates a message envelope with payloads using `smartsend()`
|
|
||||||
2. **NATSBridge** serializes and encodes each payload based on type
|
|
||||||
3. **Transport Decision**:
|
|
||||||
- **Direct** (< 1MB): Payload encoded as Base64, published to NATS
|
|
||||||
- **Link** (≥ 1MB): Payload uploaded to HTTP file server, URL published to NATS
|
|
||||||
4. **NATS** routes message envelope to subscribers
|
|
||||||
5. **Receiver** receives message via NATS subscription callback
|
|
||||||
6. **NATSBridge** processes envelope:
|
|
||||||
- Decodes Base64 payloads from NATS message
|
|
||||||
- Fetches URLs from file server with exponential backoff
|
|
||||||
7. **Receiver** deserializes payloads based on their type
|
|
||||||
|
|
||||||
### File Server Handler Abstraction
|
|
||||||
|
|
||||||
The system uses handler functions to abstract file server operations:
|
|
||||||
|
|
||||||
| Handler | Purpose |
|
|
||||||
|---------|---------|
|
|
||||||
| `plik_oneshot_upload()` / `plikOneshotUpload()` | Uploads payload bytes to file server, returns URL |
|
|
||||||
| `_fetch_with_backoff()` / `fetchWithBackoff()` | Downloads data from URL with exponential backoff retry |
|
|
||||||
|
|
||||||
This abstraction allows support for different file server implementations (Plik, AWS S3, custom HTTP server).
|
|
||||||
|
|
||||||
### Message Envelope Schema
|
|
||||||
|
|
||||||
All platforms use identical JSON schemas for message envelopes:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"correlation_id": "uuid-v4-string",
|
|
||||||
"msg_id": "uuid-v4-string",
|
|
||||||
"timestamp": "2024-01-15T10:30:00Z",
|
|
||||||
"send_to": "topic/subject",
|
|
||||||
"msg_purpose": "ACK | NACK | updateStatus | shutdown | chat",
|
|
||||||
"sender_name": "agent-wine-web-frontend",
|
|
||||||
"sender_id": "uuid4",
|
|
||||||
"receiver_name": "agent-backend",
|
|
||||||
"receiver_id": "uuid4",
|
|
||||||
"reply_to": "topic",
|
|
||||||
"reply_to_msg_id": "uuid4",
|
|
||||||
"broker_url": "nats://localhost:4222",
|
|
||||||
"metadata": {},
|
|
||||||
"payloads": [
|
|
||||||
{
|
|
||||||
"id": "uuid4",
|
|
||||||
"dataname": "login_image",
|
|
||||||
"payload_type": "image",
|
|
||||||
"transport": "direct",
|
|
||||||
"encoding": "base64",
|
|
||||||
"size": 15433,
|
|
||||||
"data": "base64-encoded-string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "uuid4",
|
|
||||||
"dataname": "large_table",
|
|
||||||
"payload_type": "table",
|
|
||||||
"transport": "link",
|
|
||||||
"encoding": "none",
|
|
||||||
"size": 524288,
|
|
||||||
"data": "http://localhost:8080/file/UPLOAD_ID/FILE_ID/data.arrow"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- **NATS Server** (v2.10+ recommended)
|
|
||||||
- **HTTP File Server** (optional, for payloads > 1MB)
|
|
||||||
|
|
||||||
### Platform-Specific Dependencies
|
|
||||||
|
|
||||||
#### 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
|
|
||||||
# or
|
|
||||||
yarn add nats uuid apache-arrow node-fetch
|
|
||||||
```
|
|
||||||
|
|
||||||
#### JavaScript (Browser)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install nats uuid apache-arrow
|
|
||||||
# or use CDN:
|
|
||||||
# https://unpkg.com/nats-js/dist/bundle/nats.min.js
|
|
||||||
# https://unpkg.com/apache-arrow/arrow.min.js
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Python (Desktop)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install nats-py aiohttp pyarrow pandas python-dateutil
|
|
||||||
```
|
|
||||||
|
|
||||||
#### MicroPython
|
|
||||||
|
|
||||||
MicroPython uses built-in modules:
|
|
||||||
- `network` - NATS connection (custom implementation)
|
|
||||||
- `time` - Timestamps
|
|
||||||
- `uos` - File operations
|
|
||||||
- `base64` - Base64 encoding
|
|
||||||
- `json` - JSON parsing
|
|
||||||
- `struct` - Binary data handling
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### Step 1: Start NATS Server
|
### Step 1: Start NATS Server
|
||||||
@@ -265,6 +99,46 @@ mkdir -p /tmp/fileserver
|
|||||||
python3 -m http.server 8080 --directory /tmp/fileserver
|
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
|
## API Reference
|
||||||
@@ -308,8 +182,8 @@ Sends data either directly via NATS or via a fileserver URL, depending on payloa
|
|||||||
using NATSBridge
|
using NATSBridge
|
||||||
|
|
||||||
env, env_json_str = NATSBridge.smartsend(
|
env, env_json_str = NATSBridge.smartsend(
|
||||||
subject::String, # NATS subject
|
subject::String,
|
||||||
data::AbstractArray{Tuple{String, Any, String}}; # List of (dataname, data, type)
|
data::AbstractArray{Tuple{String, Any, String}};
|
||||||
broker_url::String = "nats://localhost:4222",
|
broker_url::String = "nats://localhost:4222",
|
||||||
fileserver_url = "http://localhost:8080",
|
fileserver_url = "http://localhost:8080",
|
||||||
fileserver_upload_handler::Function = plik_oneshot_upload,
|
fileserver_upload_handler::Function = plik_oneshot_upload,
|
||||||
@@ -332,7 +206,7 @@ env, env_json_str = NATSBridge.smartsend(
|
|||||||
#### JavaScript
|
#### JavaScript
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const NATSBridge = require('natbridge');
|
const NATSBridge = require('natsbridge');
|
||||||
|
|
||||||
const [env, env_json_str] = await NATSBridge.smartsend(
|
const [env, env_json_str] = await NATSBridge.smartsend(
|
||||||
subject,
|
subject,
|
||||||
@@ -361,7 +235,7 @@ const [env, env_json_str] = await NATSBridge.smartsend(
|
|||||||
#### Python
|
#### Python
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from natbridge import NATSBridge
|
from natsbridge import NATSBridge
|
||||||
|
|
||||||
env, env_json_str = await NATSBridge.smartsend(
|
env, env_json_str = await NATSBridge.smartsend(
|
||||||
subject: str,
|
subject: str,
|
||||||
@@ -388,7 +262,7 @@ env, env_json_str = await NATSBridge.smartsend(
|
|||||||
#### MicroPython
|
#### MicroPython
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from natbridge import NATSBridge
|
from natsbridge import NATSBridge
|
||||||
|
|
||||||
# Limited to direct transport (< 100KB threshold)
|
# Limited to direct transport (< 100KB threshold)
|
||||||
env, env_json_str = NATSBridge.smartsend(
|
env, env_json_str = NATSBridge.smartsend(
|
||||||
@@ -468,7 +342,8 @@ env = NATSBridge.smartreceive(
|
|||||||
|------|-------|------------|--------|-------------|-------------|
|
|------|-------|------------|--------|-------------|-------------|
|
||||||
| `text` | `String` | `string` | `str` | `str` | Plain text strings |
|
| `text` | `String` | `string` | `str` | `str` | Plain text strings |
|
||||||
| `dictionary` | `Dict`, `NamedTuple` | `Object`, `Array` | `dict`, `list` | `dict` | JSON-serializable dictionaries |
|
| `dictionary` | `Dict`, `NamedTuple` | `Object`, `Array` | `dict`, `list` | `dict` | JSON-serializable dictionaries |
|
||||||
| `table` | `DataFrame`, `Arrow.Table` | `Array<Object>` | `pandas.DataFrame` | ❌ | Tabular data (Arrow IPC) |
|
| `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) |
|
| `image` | `Vector{UInt8}` | `Uint8Array`, `Buffer` | `bytes` | `bytearray` | Image data (PNG, JPG) |
|
||||||
| `audio` | `Vector{UInt8}` | `Uint8Array`, `Buffer` | `bytes` | `bytearray` | Audio data (WAV, MP3) |
|
| `audio` | `Vector{UInt8}` | `Uint8Array`, `Buffer` | `bytes` | `bytearray` | Audio data (WAV, MP3) |
|
||||||
| `video` | `Vector{UInt8}` | `Uint8Array`, `Buffer` | `bytes` | `bytearray` | Video data (MP4, AVI) |
|
| `video` | `Vector{UInt8}` | `Uint8Array`, `Buffer` | `bytes` | `bytearray` | Video data (MP4, AVI) |
|
||||||
@@ -476,58 +351,6 @@ env = NATSBridge.smartreceive(
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Transport Strategies
|
|
||||||
|
|
||||||
### Direct Transport (Payloads < 1MB)
|
|
||||||
|
|
||||||
Small payloads are sent directly via NATS with Base64 encoding.
|
|
||||||
|
|
||||||
#### Cross-Platform
|
|
||||||
|
|
||||||
```julia
|
|
||||||
# Julia
|
|
||||||
data = [("message", "Hello", "text")]
|
|
||||||
smartsend("/topic", data)
|
|
||||||
```
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// JavaScript
|
|
||||||
const data = [["message", "Hello", "text"]];
|
|
||||||
smartsend("/topic", data);
|
|
||||||
```
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Python
|
|
||||||
data = [("message", "Hello", "text")]
|
|
||||||
await smartsend("/topic", data)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Link Transport (Payloads >= 1MB)
|
|
||||||
|
|
||||||
Large payloads are uploaded to an HTTP file server.
|
|
||||||
|
|
||||||
#### Cross-Platform
|
|
||||||
|
|
||||||
```julia
|
|
||||||
# Julia
|
|
||||||
data = [("file", large_data, "binary")]
|
|
||||||
smartsend("/topic", data; fileserver_url="http://localhost:8080")
|
|
||||||
```
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// JavaScript
|
|
||||||
const data = [["file", largeData, "binary"]];
|
|
||||||
smartsend("/topic", data, { fileserver_url: 'http://localhost:8080' });
|
|
||||||
```
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Python
|
|
||||||
data = [("file", large_data, "binary")]
|
|
||||||
await smartsend("/topic", data, fileserver_url="http://localhost:8080")
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Cross-Platform Examples
|
## Cross-Platform Examples
|
||||||
|
|
||||||
### Example 1: Chat with Mixed Content
|
### Example 1: Chat with Mixed Content
|
||||||
@@ -551,7 +374,7 @@ env, env_json_str = NATSBridge.smartsend("/chat/room1", data; fileserver_url="ht
|
|||||||
#### JavaScript
|
#### JavaScript
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const NATSBridge = require('natbridge');
|
const NATSBridge = require('natsbridge');
|
||||||
|
|
||||||
const data = [
|
const data = [
|
||||||
["message_text", "Hello!", "text"],
|
["message_text", "Hello!", "text"],
|
||||||
@@ -569,7 +392,7 @@ const [env, env_json_str] = await NATSBridge.smartsend(
|
|||||||
#### Python
|
#### Python
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from natbridge import NATSBridge
|
from natsbridge import NATSBridge
|
||||||
|
|
||||||
data = [
|
data = [
|
||||||
("message_text", "Hello!", "text"),
|
("message_text", "Hello!", "text"),
|
||||||
@@ -606,7 +429,7 @@ env, env_json_str = NATSBridge.smartsend("/device/config", data)
|
|||||||
#### JavaScript
|
#### JavaScript
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const NATSBridge = require('natbridge');
|
const NATSBridge = require('natsbridge');
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
wifi_ssid: "MyNetwork",
|
wifi_ssid: "MyNetwork",
|
||||||
@@ -623,7 +446,7 @@ const [env, env_json_str] = await NATSBridge.smartsend(
|
|||||||
#### Python
|
#### Python
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from natbridge import NATSBridge
|
from natsbridge import NATSBridge
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"wifi_ssid": "MyNetwork",
|
"wifi_ssid": "MyNetwork",
|
||||||
@@ -651,14 +474,14 @@ df = DataFrame(
|
|||||||
score = [95, 88, 92]
|
score = [95, 88, 92]
|
||||||
)
|
)
|
||||||
|
|
||||||
data = [("students", df, "table")]
|
data = [("students", df, "arrowtable")]
|
||||||
env, env_json_str = NATSBridge.smartsend("/data/analysis", data)
|
env, env_json_str = NATSBridge.smartsend("/data/analysis", data)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### JavaScript
|
#### JavaScript
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const NATSBridge = require('natbridge');
|
const NATSBridge = require('natsbridge');
|
||||||
|
|
||||||
const df = [
|
const df = [
|
||||||
{ id: 1, name: "Alice", score: 95 },
|
{ id: 1, name: "Alice", score: 95 },
|
||||||
@@ -668,14 +491,14 @@ const df = [
|
|||||||
|
|
||||||
const [env, env_json_str] = await NATSBridge.smartsend(
|
const [env, env_json_str] = await NATSBridge.smartsend(
|
||||||
"/data/analysis",
|
"/data/analysis",
|
||||||
[["students", df, "table"]]
|
[["students", df, "arrowtable"]]
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Python
|
#### Python
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from natbridge import NATSBridge
|
from natsbridge import NATSBridge
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
df = pd.DataFrame({
|
df = pd.DataFrame({
|
||||||
@@ -684,7 +507,7 @@ df = pd.DataFrame({
|
|||||||
"score": [95, 88, 92]
|
"score": [95, 88, 92]
|
||||||
})
|
})
|
||||||
|
|
||||||
data = [("students", df, "table")]
|
data = [("students", df, "arrowtable")]
|
||||||
env, env_json_str = await NATSBridge.smartsend("/data/analysis", data)
|
env, env_json_str = await NATSBridge.smartsend("/data/analysis", data)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -706,36 +529,10 @@ env, env_json_str = NATSBridge.smartsend(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
```julia
|
|
||||||
# Responder
|
|
||||||
using NATS, NATSBridge
|
|
||||||
|
|
||||||
function test_responder()
|
|
||||||
conn = NATS.connect("nats://localhost:4222")
|
|
||||||
NATS.subscribe(conn, "/device/command") do msg
|
|
||||||
env = NATSBridge.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
|
|
||||||
```
|
|
||||||
|
|
||||||
#### JavaScript
|
#### JavaScript
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const NATSBridge = require('natbridge');
|
const NATSBridge = require('natsbridge');
|
||||||
|
|
||||||
// Requester
|
// Requester
|
||||||
const [env, env_json_str] = await NATSBridge.smartsend(
|
const [env, env_json_str] = await NATSBridge.smartsend(
|
||||||
@@ -745,44 +542,10 @@ const [env, env_json_str] = await NATSBridge.smartsend(
|
|||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Responder
|
|
||||||
const nats = require('nats');
|
|
||||||
const NATSBridge = require('natbridge');
|
|
||||||
|
|
||||||
async function testResponder() {
|
|
||||||
const conn = await nats.connect('nats://localhost:4222');
|
|
||||||
|
|
||||||
const subscription = await conn.subscribe('/device/command');
|
|
||||||
|
|
||||||
for await (const msg of subscription) {
|
|
||||||
const env = await NATSBridge.smartreceive(msg, {
|
|
||||||
fileserver_download_handler: NATSBridge.fetchWithBackoff
|
|
||||||
});
|
|
||||||
|
|
||||||
const replyTo = env.reply_to;
|
|
||||||
|
|
||||||
for (const [dataname, data, type] of env.payloads) {
|
|
||||||
if (dataname === 'command' && data.action === 'read_sensor') {
|
|
||||||
const response = { sensor_id: 'sensor-001', value: 42.5 };
|
|
||||||
if (replyTo) {
|
|
||||||
await NATSBridge.smartsend(
|
|
||||||
replyTo,
|
|
||||||
[["data", response, "dictionary"]]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => conn.close(), 120000);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Python
|
#### Python
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from natbridge import NATSBridge
|
from natsbridge import NATSBridge
|
||||||
|
|
||||||
# Requester
|
# Requester
|
||||||
env, env_json_str = await NATSBridge.smartsend(
|
env, env_json_str = await NATSBridge.smartsend(
|
||||||
@@ -793,38 +556,6 @@ env, env_json_str = await NATSBridge.smartsend(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
```python
|
|
||||||
# Responder
|
|
||||||
from natbridge import NATSBridge
|
|
||||||
import asyncio
|
|
||||||
import nats
|
|
||||||
|
|
||||||
async def test_responder():
|
|
||||||
nc = await nats.connect('nats://localhost:4222')
|
|
||||||
|
|
||||||
async def msg_handler(msg):
|
|
||||||
env = await NATSBridge.smartreceive(
|
|
||||||
msg,
|
|
||||||
fileserver_download_handler=fetch_with_backoff
|
|
||||||
)
|
|
||||||
|
|
||||||
reply_to = env["reply_to"]
|
|
||||||
|
|
||||||
for dataname, data, type_ in env["payloads"]:
|
|
||||||
if dataname == "command" and data["action"] == "read_sensor":
|
|
||||||
response = {"sensor_id": "sensor-001", "value": 42.5}
|
|
||||||
if reply_to:
|
|
||||||
await NATSBridge.smartsend(
|
|
||||||
reply_to,
|
|
||||||
[("data", response, "dictionary")]
|
|
||||||
)
|
|
||||||
|
|
||||||
await nc.subscribe('/device/command', cb=msg_handler)
|
|
||||||
|
|
||||||
await asyncio.sleep(120)
|
|
||||||
await nc.drain()
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
@@ -909,8 +640,10 @@ python3 test/test_py_table_receiver.py
|
|||||||
|
|
||||||
For detailed architecture and implementation information, see:
|
For detailed architecture and implementation information, see:
|
||||||
|
|
||||||
- [Architecture Documentation](docs/architecture.md) - Cross-platform architecture, API parity, platform-specific patterns
|
- [Architecture Documentation](docs/architecture_updated.md) - Cross-platform architecture, API parity, platform-specific patterns
|
||||||
- [Implementation Guide](docs/implementation.md) - Detailed implementation for each platform, handler functions, testing
|
- [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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -936,4 +669,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
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
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
This document describes the implementation of the high-performance, bi-directional data bridge using **NATS (Core & JetStream)**, implementing the Claim-Check pattern for large payloads. The system is implemented across three platforms with **high-level API parity** while maintaining **idiomatic implementations** for each language.
|
This document describes the detailed implementation of the high-performance, bi-directional data bridge using **NATS (Core & JetStream)**, implementing the Claim-Check pattern for large payloads. The system is implemented across three platforms with **high-level API parity** while maintaining **idiomatic implementations** for each language.
|
||||||
|
|
||||||
**Supported Platforms:**
|
**Supported Platforms:**
|
||||||
- **Julia** - Ground truth implementation (reference)
|
- **Julia** - Ground truth implementation (reference)
|
||||||
@@ -11,14 +11,60 @@ This document describes the implementation of the high-performance, bi-direction
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Cross-Platform Compatibility Notes
|
||||||
|
|
||||||
|
### 1. Python Payload Type Naming
|
||||||
|
|
||||||
|
The Python implementation uses `"table"` as a single payload type for both Arrow and JSON table serialization, while Julia and JavaScript use separate types (`"arrowtable"` and `"jsontable"`):
|
||||||
|
|
||||||
|
| Platform | Table Types |
|
||||||
|
|----------|-------------|
|
||||||
|
| Julia | `"arrowtable"`, `"jsontable"` |
|
||||||
|
| JavaScript | `"arrowtable"`, `"jsontable"` |
|
||||||
|
| Python | `"table"` (single type) |
|
||||||
|
| MicroPython | Not supported |
|
||||||
|
|
||||||
|
**Impact:** When exchanging data between Python and Julia/JavaScript, the payload type will differ. Python code should use `"table"` while Julia/JavaScript code should use `"arrowtable"` or `"jsontable"`.
|
||||||
|
|
||||||
|
### 2. Direct Transport Encoding Field
|
||||||
|
|
||||||
|
The encoding field in direct transport payloads differs between platforms:
|
||||||
|
|
||||||
|
| Platform | Encoding for Direct Transport |
|
||||||
|
|----------|-------------------------------|
|
||||||
|
| Julia | Preserves original type: `"base64"`, `"json"`, or `"arrow-ipc"` |
|
||||||
|
| JavaScript | Preserves original type: `"base64"`, `"json"`, or `"arrow-ipc"` |
|
||||||
|
| Python | Always `"base64"` for all direct transport payloads |
|
||||||
|
| MicroPython | Always `"base64"` for all direct transport payloads |
|
||||||
|
|
||||||
|
**Impact:** The encoding field may not accurately reflect the original serialization format when using Python or MicroPython.
|
||||||
|
|
||||||
|
### 3. MicroPython Limitations
|
||||||
|
|
||||||
|
MicroPython has significant constraints that affect feature support:
|
||||||
|
|
||||||
|
| Feature | Desktop Platforms | MicroPython |
|
||||||
|
|---------|-------------------|-------------|
|
||||||
|
| `arrowtable` | ✅ | ❌ (not supported - memory constraints) |
|
||||||
|
| `jsontable` | ✅ | ❌ (not supported - memory constraints) |
|
||||||
|
| `table` | ✅ | ❌ (not supported - memory constraints) |
|
||||||
|
| Async/await | ✅ | ❌ (synchronous only) |
|
||||||
|
| File upload/download | ✅ | ⚠️ (placeholder implementations) |
|
||||||
|
| MAX_PAYLOAD_SIZE | 1MB+ | 50KB (hard limit) |
|
||||||
|
| DEFAULT_SIZE_THRESHOLD | 1MB | 100KB |
|
||||||
|
|
||||||
|
**Impact:** MicroPython should only be used for small payloads with direct transport. File server operations are not fully implemented.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Implementation Files
|
## Implementation Files
|
||||||
|
|
||||||
| Language | Implementation File | Description |
|
| Language | Implementation File | Description |
|
||||||
|----------|---------------------|-------------|
|
|----------|---------------------|-------------|
|
||||||
| **Julia** | [`src/NATSBridge.jl`](../src/NATSBridge.jl) | Full Julia implementation with Arrow IPC support |
|
| **Julia** | [`src/NATSBridge.jl`](../src/NATSBridge.jl) | Full Julia implementation with Arrow IPC support |
|
||||||
| **JavaScript** | `src/natbridge.js` | Node.js/browser implementation |
|
| **JavaScript** | `src/natsbridge.js` | Node.js/browser implementation |
|
||||||
| **Python** | `src/natbridge.py` | Desktop Python implementation |
|
| **Python** | `src/natsbridge.py` | Desktop Python implementation |
|
||||||
| **MicroPython** | `src/natbridge_mpy.py` | MicroPython implementation (limited features) |
|
| **MicroPython** | `src/natsbridge_mpy.py` | MicroPython implementation (limited features) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -177,321 +223,15 @@ The system uses a **standardized list-of-tuples format** for all payload operati
|
|||||||
|------|-------|------------|--------|-------------|
|
|------|-------|------------|--------|-------------|
|
||||||
| `text` | `String` | `string` | `str` | `str` |
|
| `text` | `String` | `string` | `str` | `str` |
|
||||||
| `dictionary` | `Dict`, `NamedTuple` | `Object`, `Array` | `dict`, `list` | `dict` |
|
| `dictionary` | `Dict`, `NamedTuple` | `Object`, `Array` | `dict`, `list` | `dict` |
|
||||||
| `table` | `DataFrame`, `Arrow.Table` | `Array<Object>` (input) → `Buffer` (Arrow IPC) | `pandas.DataFrame`, `bytes` (Arrow IPC) | ❌ (not supported) |
|
| `arrowtable` | `DataFrame`, `Arrow.Table` | `Array<Object>` (input) → `Buffer` (Arrow IPC) | `pandas.DataFrame`, `bytes` (Arrow IPC) | ❌ (not supported) |
|
||||||
|
| `jsontable` | `Vector{NamedTuple}`, `Vector{Dict}` | `Array<Object>` | `list[dict]`, `list` | ⚠️ (limited) |
|
||||||
|
| `table` | ❌ | ❌ | `pandas.DataFrame`, `bytes` (Arrow IPC) | ❌ |
|
||||||
| `image` | `Vector{UInt8}` | `Uint8Array`, `Buffer` | `bytes` | `bytearray` |
|
| `image` | `Vector{UInt8}` | `Uint8Array`, `Buffer` | `bytes` | `bytearray` |
|
||||||
| `audio` | `Vector{UInt8}` | `Uint8Array`, `Buffer` | `bytes` | `bytearray` |
|
| `audio` | `Vector{UInt8}` | `Uint8Array`, `Buffer` | `bytes` | `bytearray` |
|
||||||
| `video` | `Vector{UInt8}` | `Uint8Array`, `Buffer` | `bytes` | `bytearray` |
|
| `video` | `Vector{UInt8}` | `Uint8Array`, `Buffer` | `bytes` | `bytearray` |
|
||||||
| `binary` | `Vector{UInt8}`, `IOBuffer` | `Uint8Array`, `Buffer` | `bytes`, `bytearray` | `bytearray` |
|
| `binary` | `Vector{UInt8}`, `IOBuffer` | `Uint8Array`, `Buffer` | `bytes`, `bytearray` | `bytearray` |
|
||||||
|
|
||||||
### Cross-Platform Examples
|
**Note:** Python uses `"table"` as a single type for both Arrow and JSON table serialization. When exchanging data between Python and Julia/JavaScript, ensure the payload type is correctly translated (`"table"` ↔ `"arrowtable"` or `"jsontable"`).
|
||||||
|
|
||||||
#### Julia
|
|
||||||
|
|
||||||
```julia
|
|
||||||
using NATSBridge
|
|
||||||
|
|
||||||
# Single payload - still wrapped in a list
|
|
||||||
env, env_json_str = smartsend(
|
|
||||||
"/test",
|
|
||||||
[("dataname1", data1, "dictionary")],
|
|
||||||
broker_url="nats://localhost:4222",
|
|
||||||
fileserver_upload_handler=plik_oneshot_upload
|
|
||||||
)
|
|
||||||
|
|
||||||
# Multiple payloads with different types
|
|
||||||
env, env_json_str = smartsend(
|
|
||||||
"/test",
|
|
||||||
[("dataname1", data1, "dictionary"), ("dataname2", data2, "table")],
|
|
||||||
broker_url="nats://localhost:4222"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Mixed content (chat with text, image, audio)
|
|
||||||
env, env_json_str = smartsend(
|
|
||||||
"/chat",
|
|
||||||
[
|
|
||||||
("message_text", "Hello!", "text"),
|
|
||||||
("user_image", image_data, "image"),
|
|
||||||
("audio_clip", audio_data, "audio")
|
|
||||||
],
|
|
||||||
broker_url="nats://localhost:4222"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Receive returns a JSON.Object{String, Any} envelope
|
|
||||||
env = smartreceive(msg; fileserver_download_handler=_fetch_with_backoff)
|
|
||||||
# env is a JSON.Object{String, Any} with "payloads" field containing Vector{Tuple{String, Any, String}}
|
|
||||||
# Access payloads: env["payloads"] which is a Vector of tuples
|
|
||||||
for (dataname, data, type) in env["payloads"]
|
|
||||||
println("$dataname: $data (type: $type)")
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
#### JavaScript
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const NATSBridge = require('natbridge');
|
|
||||||
|
|
||||||
// Single payload
|
|
||||||
const [env, env_json_str] = await NATSBridge.smartsend(
|
|
||||||
"/test",
|
|
||||||
[["dataname1", data1, "dictionary"]],
|
|
||||||
{
|
|
||||||
broker_url: "nats://localhost:4222",
|
|
||||||
fileserver_upload_handler: plikOneshotUpload
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Multiple payloads
|
|
||||||
const [env, env_json_str] = await NATSBridge.smartsend(
|
|
||||||
"/test",
|
|
||||||
[
|
|
||||||
["dataname1", data1, "dictionary"],
|
|
||||||
["dataname2", data2, "table"]
|
|
||||||
],
|
|
||||||
{ broker_url: "nats://localhost:4222" }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Mixed content
|
|
||||||
const [env, env_json_str] = await NATSBridge.smartsend(
|
|
||||||
"/chat",
|
|
||||||
[
|
|
||||||
["message_text", "Hello!", "text"],
|
|
||||||
["user_image", imageData, "image"],
|
|
||||||
["audio_clip", audioData, "audio"]
|
|
||||||
],
|
|
||||||
{ broker_url: "nats://localhost:4222" }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Receive
|
|
||||||
const env = await NATSBridge.smartreceive(msg, {
|
|
||||||
fileserver_download_handler: fetchWithBackoff
|
|
||||||
});
|
|
||||||
// env is an object with "payloads" field containing Array of arrays
|
|
||||||
// Access payloads: env.payloads which is an Array of [dataname, data, type] arrays
|
|
||||||
for (const [dataname, data, type] of env.payloads) {
|
|
||||||
console.log(`${dataname}: ${data} (type: ${type})`);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Python
|
|
||||||
|
|
||||||
```python
|
|
||||||
from natbridge import NATSBridge
|
|
||||||
|
|
||||||
# Single payload
|
|
||||||
env, env_json_str = await NATSBridge.smartsend(
|
|
||||||
"/test",
|
|
||||||
[("dataname1", data1, "dictionary")],
|
|
||||||
broker_url="nats://localhost:4222",
|
|
||||||
fileserver_upload_handler=plik_oneshot_upload
|
|
||||||
)
|
|
||||||
|
|
||||||
# Multiple payloads
|
|
||||||
env, env_json_str = await NATSBridge.smartsend(
|
|
||||||
"/test",
|
|
||||||
[("dataname1", data1, "dictionary"), ("dataname2", data2, "table")],
|
|
||||||
broker_url="nats://localhost:4222"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Mixed content
|
|
||||||
env, env_json_str = await NATSBridge.smartsend(
|
|
||||||
"/chat",
|
|
||||||
[
|
|
||||||
("message_text", "Hello!", "text"),
|
|
||||||
("user_image", image_data, "image"),
|
|
||||||
("audio_clip", audio_data, "audio")
|
|
||||||
],
|
|
||||||
broker_url="nats://localhost:4222"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Receive
|
|
||||||
env = await NATSBridge.smartreceive(
|
|
||||||
msg,
|
|
||||||
fileserver_download_handler=fetch_with_backoff
|
|
||||||
)
|
|
||||||
# env is a Dict with "payloads" key containing List[Tuple[str, Any, str]]
|
|
||||||
# Access payloads: env["payloads"] which is a list of tuples
|
|
||||||
for dataname, data, type_ in env["payloads"]:
|
|
||||||
print(f"{dataname}: {data} (type: {type_})")
|
|
||||||
```
|
|
||||||
|
|
||||||
#### MicroPython
|
|
||||||
|
|
||||||
```python
|
|
||||||
from natbridge import NATSBridge
|
|
||||||
|
|
||||||
# Limited to text and binary (no tables due to memory constraints)
|
|
||||||
env, env_json_str = NATSBridge.smartsend(
|
|
||||||
"/chat",
|
|
||||||
[
|
|
||||||
("message_text", "Hello!", "text"),
|
|
||||||
("binary_data", data_bytes, "binary")
|
|
||||||
],
|
|
||||||
broker_url="nats://localhost:4222",
|
|
||||||
size_threshold=100000 # Lower threshold for memory constraints
|
|
||||||
)
|
|
||||||
# Note: MicroPython uses synchronous handlers
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Cross-Platform Claim-Check Pattern
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
A[SmartSend Function] --> B{Is payload size < 1MB?}
|
|
||||||
B -->|Yes | C[Direct Path<br/><small>< 1MB</small>]
|
|
||||||
B -->|No | D[Link Path<br/><small>>= 1MB</small>]
|
|
||||||
|
|
||||||
C --> C1[Serialize to Buffer]
|
|
||||||
C1 --> C2[Base64 encode]
|
|
||||||
C2 --> C3[Publish to NATS]
|
|
||||||
|
|
||||||
D --> D1[Serialize to Buffer]
|
|
||||||
D1 --> D2[Upload to HTTP Server]
|
|
||||||
D2 --> D3[Publish to NATS with URL]
|
|
||||||
|
|
||||||
style A fill:#e1f5ff,stroke:#0066cc,stroke-width:2px
|
|
||||||
style B fill:#fff4e1,stroke:#cc6600,stroke-width:2px
|
|
||||||
style C fill:#e8f5e9,stroke:#008000,stroke-width:2px
|
|
||||||
style D fill:#e8f5e9,stroke:#008000,stroke-width:2px
|
|
||||||
style C1 fill:#f5f5f5,stroke:#666,stroke-width:1px
|
|
||||||
style C2 fill:#f5f5f5,stroke:#666,stroke-width:1px
|
|
||||||
style C3 fill:#f5f5f5,stroke:#666,stroke-width:1px
|
|
||||||
style D1 fill:#f5f5f5,stroke:#666,stroke-width:1px
|
|
||||||
style D2 fill:#f5f5f5,stroke:#666,stroke-width:1px
|
|
||||||
style D3 fill:#f5f5f5,stroke:#666,stroke-width:1px
|
|
||||||
```
|
|
||||||
|
|
||||||
**Claim-Check Pattern Overview:**
|
|
||||||
- **Direct Path** (< 1MB): Payload is serialized, Base64-encoded, and published directly to NATS
|
|
||||||
- **Link Path** (≥ 1MB): Payload is serialized, uploaded to an HTTP file server, and only the URL is published to NATS (claim-check pattern)
|
|
||||||
|
|
||||||
### smartsend Return Value
|
|
||||||
|
|
||||||
All platforms return a tuple/array containing both the envelope and JSON string:
|
|
||||||
|
|
||||||
#### Julia
|
|
||||||
|
|
||||||
```julia
|
|
||||||
env, env_json_str = smartsend(...)
|
|
||||||
# Returns: ::Tuple{msg_envelope_v1, String}
|
|
||||||
# env::msg_envelope_v1 - The envelope object with all metadata and payloads
|
|
||||||
# env_json_str::String - JSON string for publishing to NATS
|
|
||||||
```
|
|
||||||
|
|
||||||
#### JavaScript
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const [env, env_json_str] = await smartsend(...);
|
|
||||||
// Returns: Promise<[env, env_json_str]>
|
|
||||||
// env: Object with all metadata and payloads
|
|
||||||
// env_json_str: String for publishing to NATS
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Python
|
|
||||||
|
|
||||||
```python
|
|
||||||
env, env_json_str = await smartsend(...)
|
|
||||||
# Returns: Tuple[Dict, str]
|
|
||||||
# env: Dict with all metadata and payloads
|
|
||||||
# env_json_str: String for publishing to NATS
|
|
||||||
```
|
|
||||||
|
|
||||||
#### MicroPython
|
|
||||||
|
|
||||||
```python
|
|
||||||
env, env_json_str = NATSBridge.smartsend(...)
|
|
||||||
# Returns: Tuple[Dict, str]
|
|
||||||
# Note: MicroPython returns plain dicts (no structured envelope object)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Julia Dependencies
|
|
||||||
|
|
||||||
```julia
|
|
||||||
using Pkg
|
|
||||||
Pkg.add("NATS")
|
|
||||||
Pkg.add("Arrow")
|
|
||||||
Pkg.add("JSON3")
|
|
||||||
Pkg.add("HTTP")
|
|
||||||
Pkg.add("UUIDs")
|
|
||||||
Pkg.add("Dates")
|
|
||||||
```
|
|
||||||
|
|
||||||
### JavaScript Dependencies (Node.js)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install nats uuid apache-arrow node-fetch
|
|
||||||
# or
|
|
||||||
yarn add nats uuid apache-arrow node-fetch
|
|
||||||
```
|
|
||||||
|
|
||||||
### JavaScript Dependencies (Browser)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install nats uuid apache-arrow
|
|
||||||
# or use CDN:
|
|
||||||
# https://unpkg.com/nats-js/dist/bundle/nats.min.js
|
|
||||||
# https://unpkg.com/apache-arrow/arrow.min.js
|
|
||||||
```
|
|
||||||
|
|
||||||
### Python Dependencies (Desktop)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install nats-py aiohttp pyarrow pandas python-dateutil
|
|
||||||
```
|
|
||||||
|
|
||||||
### MicroPython Dependencies
|
|
||||||
|
|
||||||
MicroPython uses built-in modules:
|
|
||||||
- `network` - NATS connection (custom implementation)
|
|
||||||
- `time` - Timestamps
|
|
||||||
- `uos` - File operations
|
|
||||||
- `base64` - Base64 encoding
|
|
||||||
- `json` - JSON parsing
|
|
||||||
- `struct` - Binary data handling
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Usage Tutorial
|
|
||||||
|
|
||||||
### 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 any HTTP server that supports POST for file uploads
|
|
||||||
# Example: Python's built-in server
|
|
||||||
python3 -m http.server 8080 --directory /tmp/fileserver
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Run Test Scenarios
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Julia tests
|
|
||||||
julia test/test_julia_to_julia_text_sender.jl
|
|
||||||
julia test/test_julia_to_julia_text_receiver.jl
|
|
||||||
|
|
||||||
# JavaScript tests (Node.js)
|
|
||||||
node test/test_js_text_sender.js
|
|
||||||
node test/test_js_text_receiver.js
|
|
||||||
|
|
||||||
# Python tests
|
|
||||||
python3 test/test_py_text_sender.py
|
|
||||||
python3 test/test_py_text_receiver.py
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -592,7 +332,7 @@ function _serialize_data(data::Dict, payload_type::String)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function _serialize_data(data::DataFrame, payload_type::String)
|
function _serialize_data(data::DataFrame, payload_type::String)
|
||||||
# Table handling
|
# Table handling - arrowtable
|
||||||
io = IOBuffer()
|
io = IOBuffer()
|
||||||
Arrow.write(io, data)
|
Arrow.write(io, data)
|
||||||
return take!(io)
|
return take!(io)
|
||||||
@@ -784,10 +524,16 @@ function _serialize_data(data::Any, payload_type::String)
|
|||||||
json_str = JSON.json(data)
|
json_str = JSON.json(data)
|
||||||
json_str_bytes = Vector{UInt8}(json_str)
|
json_str_bytes = Vector{UInt8}(json_str)
|
||||||
return json_str_bytes
|
return json_str_bytes
|
||||||
elseif payload_type == "table"
|
elseif payload_type == "arrowtable"
|
||||||
|
# Serialize DataFrame to Arrow IPC format
|
||||||
io = IOBuffer()
|
io = IOBuffer()
|
||||||
Arrow.write(io, data)
|
Arrow.write(io, data)
|
||||||
return take!(io)
|
return take!(io)
|
||||||
|
elseif payload_type == "jsontable"
|
||||||
|
# Serialize to JSON
|
||||||
|
# data is Vector{NamedTuple} or Vector{Dict}
|
||||||
|
json_str = JSON.json(data)
|
||||||
|
return Vector{UInt8}(json_str)
|
||||||
elseif payload_type == "image"
|
elseif payload_type == "image"
|
||||||
if isa(data, Vector{UInt8})
|
if isa(data, Vector{UInt8})
|
||||||
return data
|
return data
|
||||||
@@ -833,10 +579,17 @@ function _deserialize_data(
|
|||||||
elseif payload_type == "dictionary"
|
elseif payload_type == "dictionary"
|
||||||
json_str = String(data)
|
json_str = String(data)
|
||||||
return JSON.parse(json_str)
|
return JSON.parse(json_str)
|
||||||
elseif payload_type == "table"
|
elseif payload_type == "arrowtable"
|
||||||
|
# Deserialize from Arrow IPC format
|
||||||
io = IOBuffer(data)
|
io = IOBuffer(data)
|
||||||
df = Arrow.Table(io)
|
arrow_table = Arrow.Table(io)
|
||||||
return df
|
return arrow_table
|
||||||
|
elseif payload_type == "jsontable"
|
||||||
|
# Deserialize from JSON format
|
||||||
|
# Returns Vector{NamedTuple} or Vector{Dict}
|
||||||
|
json_str = String(data)
|
||||||
|
parsed = JSON.parse(json_str)
|
||||||
|
return parsed
|
||||||
elseif payload_type == "image"
|
elseif payload_type == "image"
|
||||||
return data
|
return data
|
||||||
elseif payload_type == "audio"
|
elseif payload_type == "audio"
|
||||||
@@ -887,6 +640,8 @@ end
|
|||||||
|
|
||||||
#### plik_oneshot_upload Implementation
|
#### plik_oneshot_upload Implementation
|
||||||
|
|
||||||
|
**Overload 1: Upload from binary data**
|
||||||
|
|
||||||
```julia
|
```julia
|
||||||
function plik_oneshot_upload(file_server_url::String, dataname::String, data::Vector{UInt8})
|
function plik_oneshot_upload(file_server_url::String, dataname::String, data::Vector{UInt8})
|
||||||
# Get upload id
|
# Get upload id
|
||||||
@@ -922,6 +677,46 @@ function plik_oneshot_upload(file_server_url::String, dataname::String, data::Ve
|
|||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Overload 2: Upload from file path**
|
||||||
|
|
||||||
|
```julia
|
||||||
|
function plik_oneshot_upload(file_server_url::String, filepath::String)
|
||||||
|
# Get upload id
|
||||||
|
filename = basename(filepath)
|
||||||
|
url_getUploadID = "$file_server_url/upload"
|
||||||
|
headers = ["Content-Type" => "application/json"]
|
||||||
|
body = """{ "OneShot" : true }"""
|
||||||
|
http_response = HTTP.request("POST", url_getUploadID, headers, body; body_is_form=false)
|
||||||
|
response_json = JSON.parse(http_response.body)
|
||||||
|
|
||||||
|
uploadid = response_json["id"]
|
||||||
|
uploadtoken = response_json["uploadToken"]
|
||||||
|
|
||||||
|
# Upload file
|
||||||
|
url_upload = "$file_server_url/file/$uploadid"
|
||||||
|
headers = ["X-UploadToken" => uploadtoken]
|
||||||
|
http_response = open(filepath, "r") do file_stream
|
||||||
|
form = HTTP.Form(Dict("file" => file_stream))
|
||||||
|
|
||||||
|
# Adding status_exception=false prevents 4xx/5xx from triggering 'catch'
|
||||||
|
HTTP.post(url_upload, headers, form; status_exception = false)
|
||||||
|
end
|
||||||
|
|
||||||
|
if !isnothing(http_response) && http_response.status == 200
|
||||||
|
# Success - response already logged by caller
|
||||||
|
else
|
||||||
|
error("Failed to upload file: server returned status $(http_response.status)")
|
||||||
|
end
|
||||||
|
response_json = JSON.parse(http_response.body)
|
||||||
|
fileid = response_json["id"]
|
||||||
|
|
||||||
|
# url of the uploaded data e.g. "http://192.168.1.20:8080/file/3F62E/4AgGT/test.zip"
|
||||||
|
url = "$file_server_url/file/$uploadid/$fileid/$filename"
|
||||||
|
|
||||||
|
return Dict("status" => http_response.status, "uploadid" => uploadid, "fileid" => fileid, "url" => url)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### JavaScript Implementation
|
### JavaScript Implementation
|
||||||
@@ -929,11 +724,14 @@ end
|
|||||||
#### Module Structure
|
#### Module Structure
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// natbridge.js
|
// natsbridge.js
|
||||||
const nats = require('nats');
|
const nats = require('nats');
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const crypto = require('crypto');
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
|
|
||||||
|
// UUID generation using built-in crypto module
|
||||||
|
const uuidv4 = () => crypto.randomUUID();
|
||||||
|
|
||||||
const DEFAULT_SIZE_THRESHOLD = 1_000_000;
|
const DEFAULT_SIZE_THRESHOLD = 1_000_000;
|
||||||
const DEFAULT_BROKER_URL = 'nats://localhost:4222';
|
const DEFAULT_BROKER_URL = 'nats://localhost:4222';
|
||||||
const DEFAULT_FILESERVER_URL = 'http://localhost:8080';
|
const DEFAULT_FILESERVER_URL = 'http://localhost:8080';
|
||||||
@@ -984,10 +782,13 @@ module.exports = {
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const nats = require('nats');
|
const nats = require('nats');
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const crypto = require('crypto');
|
||||||
const fetch = require('node-fetch');
|
const fetch = require('node-fetch');
|
||||||
const arrow = require('apache-arrow');
|
const arrow = require('apache-arrow');
|
||||||
|
|
||||||
|
// UUID generation using built-in crypto module
|
||||||
|
const uuidv4 = () => crypto.randomUUID();
|
||||||
|
|
||||||
const DEFAULT_SIZE_THRESHOLD = 1_000_000;
|
const DEFAULT_SIZE_THRESHOLD = 1_000_000;
|
||||||
const DEFAULT_BROKER_URL = 'nats://localhost:4222';
|
const DEFAULT_BROKER_URL = 'nats://localhost:4222';
|
||||||
const DEFAULT_FILESERVER_URL = 'http://localhost:8080';
|
const DEFAULT_FILESERVER_URL = 'http://localhost:8080';
|
||||||
@@ -1108,21 +909,34 @@ async function serializeData(data, payload_type) {
|
|||||||
} else if (payload_type === 'dictionary') {
|
} else if (payload_type === 'dictionary') {
|
||||||
const jsonStr = JSON.stringify(data);
|
const jsonStr = JSON.stringify(data);
|
||||||
return Buffer.from(jsonStr, 'utf8');
|
return Buffer.from(jsonStr, 'utf8');
|
||||||
} else if (payload_type === 'table') {
|
} else if (payload_type === 'arrowtable') {
|
||||||
// Convert to Arrow IPC
|
// Convert Array<Object> to Arrow IPC
|
||||||
const buffer = Buffer.alloc(1024 * 1024); // Pre-allocate buffer
|
if (!Array.isArray(data) || data.length === 0) {
|
||||||
const writer = new arrow.RecordBatchWriter([
|
throw new Error('arrowtable data must be a non-empty array of objects');
|
||||||
new arrow.Schema(Object.keys(data[0]).map(key => new arrow.Field(key, arrow.any())))
|
}
|
||||||
]);
|
|
||||||
|
|
||||||
|
// Create schema from first row
|
||||||
|
const schemaFields = Object.keys(data[0]).map(key =>
|
||||||
|
new arrow.Field(key, arrow.any())
|
||||||
|
);
|
||||||
|
const schema = new arrow.Schema(schemaFields);
|
||||||
|
|
||||||
|
// Create writer
|
||||||
|
const writer = new arrow.RecordBatchWriter([schema]);
|
||||||
|
|
||||||
|
// Write rows
|
||||||
for (const row of data) {
|
for (const row of data) {
|
||||||
const recordBatch = arrow.recordBatch.fromObjects([row], writer.schema);
|
const recordBatch = arrow.recordBatch.fromObjects([row], schema);
|
||||||
writer.write(recordBatch);
|
writer.write(recordBatch);
|
||||||
}
|
}
|
||||||
await writer.close();
|
await writer.close();
|
||||||
|
|
||||||
// Read from the underlying buffer
|
// Read buffer
|
||||||
return buffer;
|
return writer.toBuffer();
|
||||||
|
} else if (payload_type === 'jsontable') {
|
||||||
|
// Serialize directly to JSON
|
||||||
|
const jsonStr = JSON.stringify(data);
|
||||||
|
return Buffer.from(jsonStr, 'utf8');
|
||||||
} else if (payload_type === 'image') {
|
} else if (payload_type === 'image') {
|
||||||
if (data instanceof Uint8Array || Buffer.isBuffer(data)) {
|
if (data instanceof Uint8Array || Buffer.isBuffer(data)) {
|
||||||
return Buffer.from(data);
|
return Buffer.from(data);
|
||||||
@@ -1168,10 +982,15 @@ async function deserializeData(data, payload_type, correlation_id) {
|
|||||||
} else if (payload_type === 'dictionary') {
|
} else if (payload_type === 'dictionary') {
|
||||||
const jsonStr = Buffer.from(data).toString('utf8');
|
const jsonStr = Buffer.from(data).toString('utf8');
|
||||||
return JSON.parse(jsonStr);
|
return JSON.parse(jsonStr);
|
||||||
} else if (payload_type === 'table') {
|
} else if (payload_type === 'arrowtable') {
|
||||||
|
// Deserialize from Arrow IPC
|
||||||
const buffer = Buffer.from(data);
|
const buffer = Buffer.from(data);
|
||||||
const table = arrow.tableFromRawBytes(buffer);
|
const table = arrow.tableFromRawBytes(buffer);
|
||||||
return table;
|
return table;
|
||||||
|
} else if (payload_type === 'jsontable') {
|
||||||
|
// Deserialize from JSON - returns Array<Object>
|
||||||
|
const jsonStr = Buffer.from(data).toString('utf8');
|
||||||
|
return JSON.parse(jsonStr);
|
||||||
} else if (payload_type === 'image') {
|
} else if (payload_type === 'image') {
|
||||||
return Buffer.from(data);
|
return Buffer.from(data);
|
||||||
} else if (payload_type === 'audio') {
|
} else if (payload_type === 'audio') {
|
||||||
@@ -1272,7 +1091,7 @@ async function plikOneshotUpload(file_server_url, dataname, data) {
|
|||||||
#### Module Structure
|
#### Module Structure
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# natbridge.py
|
# natsbridge.py
|
||||||
import asyncio
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
@@ -1489,60 +1308,57 @@ from typing import Any
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
import pyarrow as arrow
|
import pyarrow as arrow
|
||||||
import pyarrow.parquet as pq
|
import pyarrow.feather as feather
|
||||||
|
import pyarrow.ipc as ipc
|
||||||
ARROW_AVAILABLE = True
|
ARROW_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
ARROW_AVAILABLE = False
|
ARROW_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
def _serialize_data(data: Any, payload_type: str) -> bytes:
|
def _serialize_data(data: Any, payload_type: str) -> bytes:
|
||||||
"""Serialize data to bytes based on type."""
|
"""
|
||||||
|
Serialize data to bytes based on type.
|
||||||
|
|
||||||
|
Note: Python uses "table" as a single type for both Arrow and JSON table
|
||||||
|
serialization. Julia/JavaScript use separate "arrowtable" and "jsontable" types.
|
||||||
|
"""
|
||||||
if payload_type == 'text':
|
if payload_type == 'text':
|
||||||
if isinstance(data, str):
|
if isinstance(data, str):
|
||||||
return data.encode('utf-8')
|
return data.encode('utf-8')
|
||||||
else:
|
else:
|
||||||
raise Error('Text data must be a string')
|
raise ValueError('Text data must be a string')
|
||||||
elif payload_type == 'dictionary':
|
elif payload_type == 'dictionary':
|
||||||
json_str = json.dumps(data)
|
json_str = json.dumps(data)
|
||||||
return json_str.encode('utf-8')
|
return json_str.encode('utf-8')
|
||||||
elif payload_type == 'table':
|
elif payload_type == 'table':
|
||||||
|
# Python uses "table" for both arrowtable and jsontable
|
||||||
if not ARROW_AVAILABLE:
|
if not ARROW_AVAILABLE:
|
||||||
raise Error('pyarrow not available for table serialization')
|
raise RuntimeError('pyarrow not available for table serialization')
|
||||||
|
|
||||||
# Convert DataFrame to Arrow
|
|
||||||
import io
|
import io
|
||||||
buf = io.BytesIO()
|
buf = io.BytesIO()
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
if isinstance(data, pd.DataFrame):
|
if isinstance(data, pd.DataFrame):
|
||||||
|
# Serialize DataFrame to Arrow
|
||||||
table = arrow.Table.from_pandas(data)
|
table = arrow.Table.from_pandas(data)
|
||||||
sink = arrow.ipc.new_file(buf)
|
sink = ipc.new_file(buf, table.schema)
|
||||||
arrow.ipc.write_table(table, sink)
|
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()
|
sink.close()
|
||||||
return buf.getvalue()
|
return buf.getvalue()
|
||||||
else:
|
else:
|
||||||
raise Error('Table data must be a pandas DataFrame')
|
raise ValueError('Table data must be a pandas DataFrame or pyarrow Table')
|
||||||
elif payload_type == 'image':
|
elif payload_type in ('image', 'audio', 'video', 'binary'):
|
||||||
if isinstance(data, (bytes, bytearray)):
|
if isinstance(data, (bytes, bytearray)):
|
||||||
return bytes(data)
|
return bytes(data)
|
||||||
else:
|
else:
|
||||||
raise Error('Image data must be bytes')
|
raise ValueError(f'{payload_type} data must be bytes')
|
||||||
elif payload_type == 'audio':
|
|
||||||
if isinstance(data, (bytes, bytearray)):
|
|
||||||
return bytes(data)
|
|
||||||
else:
|
|
||||||
raise Error('Audio data must be bytes')
|
|
||||||
elif payload_type == 'video':
|
|
||||||
if isinstance(data, (bytes, bytearray)):
|
|
||||||
return bytes(data)
|
|
||||||
else:
|
|
||||||
raise Error('Video data must be bytes')
|
|
||||||
elif payload_type == 'binary':
|
|
||||||
if isinstance(data, (bytes, bytearray)):
|
|
||||||
return bytes(data)
|
|
||||||
else:
|
|
||||||
raise Error('Binary data must be bytes')
|
|
||||||
else:
|
else:
|
||||||
raise Error(f'Unknown payload_type: {payload_type}')
|
raise ValueError(f'Unknown payload_type: {payload_type}')
|
||||||
```
|
```
|
||||||
|
|
||||||
#### deserializeData Implementation
|
#### deserializeData Implementation
|
||||||
@@ -1554,36 +1370,38 @@ from typing import Any
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
import pyarrow as arrow
|
import pyarrow as arrow
|
||||||
|
import pyarrow.feather as feather
|
||||||
|
import pyarrow.ipc as ipc
|
||||||
ARROW_AVAILABLE = True
|
ARROW_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
ARROW_AVAILABLE = False
|
ARROW_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
def _deserialize_data(data: bytes, payload_type: str, correlation_id: str) -> Any:
|
def _deserialize_data(data: bytes, payload_type: str, correlation_id: str) -> Any:
|
||||||
"""Deserialize bytes to data based on type."""
|
"""
|
||||||
|
Deserialize bytes to data based on type.
|
||||||
|
|
||||||
|
Note: Python uses "table" as a single type for both Arrow and JSON table
|
||||||
|
deserialization. Julia/JavaScript use separate "arrowtable" and "jsontable" types.
|
||||||
|
"""
|
||||||
if payload_type == 'text':
|
if payload_type == 'text':
|
||||||
return data.decode('utf-8')
|
return data.decode('utf-8')
|
||||||
elif payload_type == 'dictionary':
|
elif payload_type == 'dictionary':
|
||||||
json_str = data.decode('utf-8')
|
json_str = data.decode('utf-8')
|
||||||
return json.loads(json_str)
|
return json.loads(json_str)
|
||||||
elif payload_type == 'table':
|
elif payload_type == 'table':
|
||||||
|
# Python uses "table" for both arrowtable and jsontable
|
||||||
if not ARROW_AVAILABLE:
|
if not ARROW_AVAILABLE:
|
||||||
raise Error('pyarrow not available for table deserialization')
|
raise RuntimeError('pyarrow not available for table deserialization')
|
||||||
|
|
||||||
import io
|
import io
|
||||||
buf = io.BytesIO(data)
|
buf = io.BytesIO(data)
|
||||||
reader = arrow.ipc.open_file(buf)
|
reader = ipc.open_file(buf)
|
||||||
return reader.read_all().to_pandas()
|
return reader.read_all().to_pandas()
|
||||||
elif payload_type == 'image':
|
elif payload_type in ('image', 'audio', 'video', 'binary'):
|
||||||
return data
|
|
||||||
elif payload_type == 'audio':
|
|
||||||
return data
|
|
||||||
elif payload_type == 'video':
|
|
||||||
return data
|
|
||||||
elif payload_type == 'binary':
|
|
||||||
return data
|
return data
|
||||||
else:
|
else:
|
||||||
raise Error(f'Unknown payload_type: {payload_type}')
|
raise ValueError(f'Unknown payload_type: {payload_type}')
|
||||||
```
|
```
|
||||||
|
|
||||||
#### fetchWithBackoff Implementation
|
#### fetchWithBackoff Implementation
|
||||||
@@ -1672,9 +1490,9 @@ async def plik_oneshot_upload(
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## MicroPython Implementation
|
### MicroPython Implementation
|
||||||
|
|
||||||
### Limitations
|
#### Limitations
|
||||||
|
|
||||||
MicroPython has significant constraints compared to desktop implementations:
|
MicroPython has significant constraints compared to desktop implementations:
|
||||||
|
|
||||||
@@ -1682,27 +1500,34 @@ MicroPython has significant constraints compared to desktop implementations:
|
|||||||
|---------|---------|-------------|
|
|---------|---------|-------------|
|
||||||
| Memory | Unlimited | ~256KB - 1MB |
|
| Memory | Unlimited | ~256KB - 1MB |
|
||||||
| Arrow IPC | ✅ | ❌ (not supported) |
|
| Arrow IPC | ✅ | ❌ (not supported) |
|
||||||
| Async/Await | ✅ | ⚠️ (uasyncio only) |
|
| Async/Await | ✅ | ❌ (synchronous only) |
|
||||||
| Large payloads (>1MB) | ✅ | ❌ (enforced limit) |
|
| Large payloads (>1MB) | ✅ | ❌ (enforced limit) |
|
||||||
| Table type | ✅ | ❌ |
|
| arrowtable | ✅ | ❌ (not supported) |
|
||||||
|
| jsontable | ✅ | ❌ (not supported) |
|
||||||
| Multiple payloads | ✅ | ⚠️ (limited) |
|
| Multiple payloads | ✅ | ⚠️ (limited) |
|
||||||
|
|
||||||
### MicroPython Module Structure
|
**Note:** MicroPython does NOT support table types (`arrowtable` or `jsontable`) due to memory constraints.
|
||||||
|
|
||||||
|
#### Module Structure
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# natbridge_mpy.py (MicroPython)
|
# natsbridge_mpy.py (MicroPython)
|
||||||
import network
|
import network
|
||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
import base64
|
import base64
|
||||||
import uos
|
import uos
|
||||||
import struct
|
import struct
|
||||||
|
import random
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
DEFAULT_SIZE_THRESHOLD = 100000 # 100KB for MicroPython
|
DEFAULT_SIZE_THRESHOLD = 100000 # 100KB for MicroPython
|
||||||
DEFAULT_BROKER_URL = "nats://localhost:4222"
|
DEFAULT_BROKER_URL = "nats://localhost:4222"
|
||||||
DEFAULT_FILESERVER_URL = "http://localhost:8080"
|
DEFAULT_FILESERVER_URL = "http://localhost:8080"
|
||||||
MAX_PAYLOAD_SIZE = 50000 # Hard limit
|
MAX_PAYLOAD_SIZE = 50000 # Hard limit (lower than threshold for safety)
|
||||||
|
|
||||||
|
# Note: MicroPython does NOT support table types (arrowtable/jsontable)
|
||||||
|
# Only supports: text, dictionary, image, audio, video, binary
|
||||||
|
|
||||||
|
|
||||||
class NATSBridge:
|
class NATSBridge:
|
||||||
@@ -1811,26 +1636,44 @@ class NATSBridge:
|
|||||||
return env_json_obj
|
return env_json_obj
|
||||||
|
|
||||||
def _serialize_data(self, data, payload_type):
|
def _serialize_data(self, data, payload_type):
|
||||||
"""Serialize data (MicroPython version - no table support)."""
|
"""
|
||||||
|
Serialize data (MicroPython version).
|
||||||
|
|
||||||
|
Note: MicroPython does NOT support table types (arrowtable/jsontable).
|
||||||
|
Only supports: text, dictionary, image, audio, video, binary
|
||||||
|
"""
|
||||||
if payload_type == 'text':
|
if payload_type == 'text':
|
||||||
return data.encode('utf-8')
|
if isinstance(data, str):
|
||||||
|
return data.encode('utf-8')
|
||||||
|
else:
|
||||||
|
raise ValueError('Text data must be a string')
|
||||||
elif payload_type == 'dictionary':
|
elif payload_type == 'dictionary':
|
||||||
return json.dumps(data).encode('utf-8')
|
json_str = json.dumps(data)
|
||||||
|
return json_str.encode('utf-8')
|
||||||
elif payload_type in ('image', 'audio', 'video', 'binary'):
|
elif payload_type in ('image', 'audio', 'video', 'binary'):
|
||||||
return bytes(data)
|
if isinstance(data, (bytes, bytearray, memoryview)):
|
||||||
|
return bytes(data)
|
||||||
|
else:
|
||||||
|
raise ValueError(f'{payload_type} data must be bytes')
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown payload_type: {payload_type}")
|
raise ValueError(f'Unknown payload_type: {payload_type}')
|
||||||
|
|
||||||
def _deserialize_data(self, data, payload_type):
|
def _deserialize_data(self, data, payload_type):
|
||||||
"""Deserialize data (MicroPython version)."""
|
"""
|
||||||
|
Deserialize data (MicroPython version).
|
||||||
|
|
||||||
|
Note: MicroPython does NOT support table types (arrowtable/jsontable).
|
||||||
|
Only supports: text, dictionary, image, audio, video, binary
|
||||||
|
"""
|
||||||
if payload_type == 'text':
|
if payload_type == 'text':
|
||||||
return data.decode('utf-8')
|
return data.decode('utf-8')
|
||||||
elif payload_type == 'dictionary':
|
elif payload_type == 'dictionary':
|
||||||
return json.loads(data.decode('utf-8'))
|
json_str = data.decode('utf-8')
|
||||||
|
return json.loads(json_str)
|
||||||
elif payload_type in ('image', 'audio', 'video', 'binary'):
|
elif payload_type in ('image', 'audio', 'video', 'binary'):
|
||||||
return data
|
return data
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown payload_type: {payload_type}")
|
raise ValueError(f'Unknown payload_type: {payload_type}')
|
||||||
|
|
||||||
def _generate_uuid(self):
|
def _generate_uuid(self):
|
||||||
"""Generate simple UUID (MicroPython compatible)."""
|
"""Generate simple UUID (MicroPython compatible)."""
|
||||||
@@ -1926,6 +1769,13 @@ All platforms use correlation IDs for distributed tracing:
|
|||||||
[timestamp] [Correlation: abc123] Message published to subject
|
[timestamp] [Correlation: abc123] Message published to subject
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Serialization Performance
|
||||||
|
|
||||||
|
| Format | Use Case | Pros | Cons |
|
||||||
|
|--------|----------|------|------|
|
||||||
|
| `arrowtable` | Large tabular data | Fast, zero-copy, schema-preserving | Binary format, requires Arrow library, not supported in MicroPython |
|
||||||
|
| `jsontable` | Small/medium tabular data | Human-readable, universal support, works in MicroPython | Slower, larger size, no schema enforcement |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
@@ -1978,6 +1828,7 @@ python3 test/test_py_text_receiver.py
|
|||||||
- Reduce `size_threshold`
|
- Reduce `size_threshold`
|
||||||
- Use direct transport only (< 100KB)
|
- Use direct transport only (< 100KB)
|
||||||
- Avoid large payloads
|
- Avoid large payloads
|
||||||
|
- Use `jsontable` instead of `arrowtable` (arrowtable not supported)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1993,6 +1844,16 @@ This cross-platform NATS bridge provides:
|
|||||||
- **MicroPython**: Synchronous API, memory-constrained optimizations
|
- **MicroPython**: Synchronous API, memory-constrained optimizations
|
||||||
3. **Message Format Consistency**: Identical JSON schemas across all platforms
|
3. **Message Format Consistency**: Identical JSON schemas across all platforms
|
||||||
4. **Handler Abstraction**: File server operations abstracted through configurable handlers
|
4. **Handler Abstraction**: File server operations abstracted through configurable handlers
|
||||||
5. **Platform-Specific Optimizations**: Arrow IPC in desktop platforms, streaming support in MicroPython
|
5. **Platform-Specific Optimizations**:
|
||||||
|
- **Arrow IPC** (`arrowtable`): Efficient binary format for large tabular data (not supported in MicroPython)
|
||||||
|
- **JSON** (`jsontable`): Universal human-readable format for smaller tables (works in Julia, JavaScript, Python; NOT supported in MicroPython)
|
||||||
|
|
||||||
The Julia implementation in [`src/NATSBridge.jl`](src/NATSBridge.jl:1) serves as the ground truth for API design and behavior.
|
The Julia implementation in [`src/NATSBridge.jl`](src/NATSBridge.jl:1) serves as the ground truth for API design and behavior.
|
||||||
|
|
||||||
|
### Datatype Summary
|
||||||
|
|
||||||
|
| Datatype | Serialization | Use Case | Encoding | Supported Platforms |
|
||||||
|
|----------|---------------|----------|----------|---------------------|
|
||||||
|
| `arrowtable` | Apache Arrow IPC | Large tabular data, schema-preserving | `arrow-ipc` → `base64` | Julia, JavaScript, Python |
|
||||||
|
| `jsontable` | JSON | Small/medium tabular data, human-readable | `json` → `base64` | Julia, JavaScript, Python |
|
||||||
|
| `table` | Apache Arrow IPC (Python only) | Python's unified table type | `arrow-ipc` → `base64` | Python |
|
||||||
|
|||||||
@@ -33,18 +33,28 @@ smartsend(subject, [(dataname, data, type), ...], options)
|
|||||||
env = smartreceive(msg, options)
|
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
|
### Supported Payload Types
|
||||||
|
|
||||||
| Type | Julia | JavaScript | Python | MicroPython |
|
| Type | Julia | JavaScript | Python | MicroPython |
|
||||||
|------|-------|------------|--------|-------------|
|
|------|-------|------------|--------|-------------|
|
||||||
| `text` | `String` | `string` | `str` | `str` |
|
| `text` | `String` | `string` | `str` | `str` |
|
||||||
| `dictionary` | `Dict` | `Object` | `dict` | `dict` |
|
| `dictionary` | `Dict` | `Object` | `dict` | `dict` |
|
||||||
| `table` | `DataFrame` | `Array<Object>` | `DataFrame` | ❌ |
|
| `arrowtable` | `DataFrame` | `Array<Object>` | `pandas.DataFrame` | ❌ |
|
||||||
|
| `jsontable` | `Vector{NamedTuple}` | `Array<Object>` | `list[dict]` | ❌ |
|
||||||
|
| `table` | ❌ | ❌ | `pandas.DataFrame` | ❌ |
|
||||||
| `image` | `Vector{UInt8}` | `Uint8Array` | `bytes` | `bytearray` |
|
| `image` | `Vector{UInt8}` | `Uint8Array` | `bytes` | `bytearray` |
|
||||||
| `audio` | `Vector{UInt8}` | `Uint8Array` | `bytes` | `bytearray` |
|
| `audio` | `Vector{UInt8}` | `Uint8Array` | `bytes` | `bytearray` |
|
||||||
| `video` | `Vector{UInt8}` | `Uint8Array` | `bytes` | `bytearray` |
|
| `video` | `Vector{UInt8}` | `Uint8Array` | `bytes` | `bytearray` |
|
||||||
| `binary` | `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
|
## Prerequisites
|
||||||
@@ -134,7 +144,7 @@ env, env_json_str = smartsend("/chat/room1", data, broker_url="nats://localhost:
|
|||||||
#### JavaScript
|
#### JavaScript
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const NATSBridge = require('./src/natbridge.js');
|
const NATSBridge = require('./src/natsbridge.js');
|
||||||
|
|
||||||
// Send a text message
|
// Send a text message
|
||||||
const data = [["message", "Hello World", "text"]];
|
const data = [["message", "Hello World", "text"]];
|
||||||
@@ -158,7 +168,7 @@ const [env, env_json_str] = await NATSBridge.smartsend(
|
|||||||
#### Python
|
#### Python
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from natbridge import smartsend
|
from natsbridge import smartsend
|
||||||
|
|
||||||
# Send a text message
|
# Send a text message
|
||||||
data = [("message", "Hello World", "text")]
|
data = [("message", "Hello World", "text")]
|
||||||
@@ -185,7 +195,7 @@ env, env_json_str = await smartsend(
|
|||||||
#### MicroPython
|
#### MicroPython
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from natbridge_mpy import NATSBridge
|
from natsbridge_mpy import NATSBridge
|
||||||
|
|
||||||
bridge = NATSBridge()
|
bridge = NATSBridge()
|
||||||
|
|
||||||
@@ -218,7 +228,7 @@ end
|
|||||||
#### JavaScript
|
#### JavaScript
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const NATSBridge = require('./src/natbridge.js');
|
const NATSBridge = require('./src/natsbridge.js');
|
||||||
|
|
||||||
// Receive and process message
|
// Receive and process message
|
||||||
const env = await NATSBridge.smartreceive(msg, {
|
const env = await NATSBridge.smartreceive(msg, {
|
||||||
@@ -233,7 +243,7 @@ for (const [dataname, data, type] of env.payloads) {
|
|||||||
#### Python
|
#### Python
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from natbridge import smartreceive, fetch_with_backoff
|
from natsbridge import smartreceive, fetch_with_backoff
|
||||||
|
|
||||||
# Receive and process message
|
# Receive and process message
|
||||||
env = await smartreceive(
|
env = await smartreceive(
|
||||||
@@ -269,7 +279,7 @@ env, env_json_str = smartsend("/device/config", data, broker_url="nats://localho
|
|||||||
#### JavaScript
|
#### JavaScript
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const NATSBridge = require('./src/natbridge.js');
|
const NATSBridge = require('./src/natsbridge.js');
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
wifi_ssid: "MyNetwork",
|
wifi_ssid: "MyNetwork",
|
||||||
@@ -288,7 +298,7 @@ const [env, env_json_str] = await NATSBridge.smartsend(
|
|||||||
#### Python
|
#### Python
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from natbridge import smartsend
|
from natsbridge import smartsend
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"wifi_ssid": "MyNetwork",
|
"wifi_ssid": "MyNetwork",
|
||||||
@@ -307,7 +317,7 @@ env, env_json_str = await smartsend(
|
|||||||
#### MicroPython
|
#### MicroPython
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from natbridge_mpy import NATSBridge
|
from natsbridge_mpy import NATSBridge
|
||||||
|
|
||||||
bridge = NATSBridge()
|
bridge = NATSBridge()
|
||||||
|
|
||||||
@@ -342,7 +352,7 @@ env, env_json_str = smartsend("/chat/image", data, broker_url="nats://localhost:
|
|||||||
#### JavaScript
|
#### JavaScript
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const NATSBridge = require('./src/natbridge.js');
|
const NATSBridge = require('./src/natsbridge.js');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
// Read image file
|
// Read image file
|
||||||
@@ -359,7 +369,7 @@ const [env, env_json_str] = await NATSBridge.smartsend(
|
|||||||
#### Python
|
#### Python
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from natbridge import smartsend
|
from natsbridge import smartsend
|
||||||
|
|
||||||
# Read image file
|
# Read image file
|
||||||
with open("image.png", "rb") as f:
|
with open("image.png", "rb") as f:
|
||||||
@@ -376,7 +386,7 @@ env, env_json_str = await smartsend(
|
|||||||
#### MicroPython
|
#### MicroPython
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from natbridge_mpy import NATSBridge
|
from natsbridge_mpy import NATSBridge
|
||||||
|
|
||||||
bridge = NATSBridge()
|
bridge = NATSBridge()
|
||||||
|
|
||||||
@@ -413,7 +423,7 @@ env, env_json_str = smartsend(
|
|||||||
#### JavaScript (Requester)
|
#### JavaScript (Requester)
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const NATSBridge = require('./src/natbridge.js');
|
const NATSBridge = require('./src/natsbridge.js');
|
||||||
|
|
||||||
// Send command with reply-to
|
// Send command with reply-to
|
||||||
const data = [["command", { action: "read_sensor" }, "dictionary"]];
|
const data = [["command", { action: "read_sensor" }, "dictionary"]];
|
||||||
@@ -431,7 +441,7 @@ const [env, env_json_str] = await NATSBridge.smartsend(
|
|||||||
#### Python (Requester)
|
#### Python (Requester)
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from natbridge import smartsend
|
from natsbridge import smartsend
|
||||||
|
|
||||||
# Send command with reply-to
|
# Send command with reply-to
|
||||||
data = [("command", {"action": "read_sensor"}, "dictionary")]
|
data = [("command", {"action": "read_sensor"}, "dictionary")]
|
||||||
@@ -506,7 +516,7 @@ println("File uploaded to: $(env.payloads[1].data)")
|
|||||||
#### JavaScript
|
#### JavaScript
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const NATSBridge = require('./src/natbridge.js');
|
const NATSBridge = require('./src/natsbridge.js');
|
||||||
|
|
||||||
// Create large data (> 1MB)
|
// Create large data (> 1MB)
|
||||||
const large_data = Buffer.alloc(2_000_000);
|
const large_data = Buffer.alloc(2_000_000);
|
||||||
@@ -530,7 +540,7 @@ console.log("File uploaded to:", env.payloads[0].data);
|
|||||||
#### Python
|
#### Python
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from natbridge import smartsend
|
from natsbridge import smartsend
|
||||||
|
|
||||||
# Create large data (> 1MB)
|
# Create large data (> 1MB)
|
||||||
import os
|
import os
|
||||||
@@ -552,7 +562,7 @@ print(f"File uploaded to: {env['payloads'][0]['data']}")
|
|||||||
MicroPython enforces a hard limit of 50KB per payload:
|
MicroPython enforces a hard limit of 50KB per payload:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from natbridge_mpy import NATSBridge
|
from natsbridge_mpy import NATSBridge
|
||||||
|
|
||||||
bridge = NATSBridge()
|
bridge = NATSBridge()
|
||||||
|
|
||||||
@@ -590,7 +600,7 @@ env, env_json_str = smartsend("/chat/mixed", data, broker_url="nats://localhost:
|
|||||||
#### JavaScript
|
#### JavaScript
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const NATSBridge = require('./src/natbridge.js');
|
const NATSBridge = require('./src/natsbridge.js');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
const image_data = fs.readFileSync('avatar.png');
|
const image_data = fs.readFileSync('avatar.png');
|
||||||
@@ -610,7 +620,7 @@ const [env, env_json_str] = await NATSBridge.smartsend(
|
|||||||
#### Python
|
#### Python
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from natbridge import smartsend
|
from natsbridge import smartsend
|
||||||
|
|
||||||
with open("avatar.png", "rb") as f:
|
with open("avatar.png", "rb") as f:
|
||||||
image_data = f.read()
|
image_data = f.read()
|
||||||
@@ -645,14 +655,14 @@ df = DataFrame(
|
|||||||
score = [95, 88, 92]
|
score = [95, 88, 92]
|
||||||
)
|
)
|
||||||
|
|
||||||
data = [("students", df, "table")]
|
data = [("students", df, "arrowtable")]
|
||||||
env, env_json_str = smartsend("/data/students", data, broker_url="nats://localhost:4222")
|
env, env_json_str = smartsend("/data/students", data, broker_url="nats://localhost:4222")
|
||||||
```
|
```
|
||||||
|
|
||||||
#### JavaScript
|
#### JavaScript
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
const NATSBridge = require('./src/natbridge.js');
|
const NATSBridge = require('./src/natsbridge.js');
|
||||||
|
|
||||||
// Create table data (array of objects)
|
// Create table data (array of objects)
|
||||||
const table_data = [
|
const table_data = [
|
||||||
@@ -661,7 +671,7 @@ const table_data = [
|
|||||||
{ id: 3, name: "Charlie", score: 92 }
|
{ id: 3, name: "Charlie", score: 92 }
|
||||||
];
|
];
|
||||||
|
|
||||||
const data = [["students", table_data, "table"]];
|
const data = [["students", table_data, "arrowtable"]];
|
||||||
const [env, env_json_str] = await NATSBridge.smartsend(
|
const [env, env_json_str] = await NATSBridge.smartsend(
|
||||||
"/data/students",
|
"/data/students",
|
||||||
data,
|
data,
|
||||||
@@ -672,7 +682,7 @@ const [env, env_json_str] = await NATSBridge.smartsend(
|
|||||||
#### Python
|
#### Python
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from natbridge import smartsend
|
from natsbridge import smartsend
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
# Create DataFrame
|
# Create DataFrame
|
||||||
@@ -728,4 +738,4 @@ MicroPython does not support table type due to memory constraints. Use dictionar
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
@@ -177,7 +177,7 @@ end
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// src/chat_ui.js
|
// src/chat_ui.js
|
||||||
const NATSBridge = require('./src/natbridge.js');
|
const NATSBridge = require('./src/natsbridge.js');
|
||||||
|
|
||||||
class ChatUI {
|
class ChatUI {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -333,7 +333,7 @@ end
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// src/chat_handler.js
|
// src/chat_handler.js
|
||||||
const NATSBridge = require('./src/natbridge.js');
|
const NATSBridge = require('./src/natsbridge.js');
|
||||||
const nats = require('nats');
|
const nats = require('nats');
|
||||||
|
|
||||||
class ChatHandler {
|
class ChatHandler {
|
||||||
@@ -393,7 +393,7 @@ module.exports = ChatHandler;
|
|||||||
# src/chat_handler.py
|
# src/chat_handler.py
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from natbridge import smartreceive, fetch_with_backoff
|
from natsbridge import smartreceive, fetch_with_backoff
|
||||||
|
|
||||||
class ChatHandler:
|
class ChatHandler:
|
||||||
def __init__(self, nats_connection):
|
def __init__(self, nats_connection):
|
||||||
@@ -526,7 +526,7 @@ end
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// src/file_upload_service.js
|
// src/file_upload_service.js
|
||||||
const NATSBridge = require('./src/natbridge.js');
|
const NATSBridge = require('./src/natsbridge.js');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
class FileUploadService {
|
class FileUploadService {
|
||||||
@@ -580,7 +580,7 @@ module.exports = FileUploadService;
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
# src/file_upload_service.py
|
# src/file_upload_service.py
|
||||||
from natbridge import smartsend
|
from natsbridge import smartsend
|
||||||
import os
|
import os
|
||||||
|
|
||||||
class FileUploadService:
|
class FileUploadService:
|
||||||
@@ -659,7 +659,7 @@ end
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// src/file_download_service.js
|
// src/file_download_service.js
|
||||||
const NATSBridge = require('./src/natbridge.js');
|
const NATSBridge = require('./src/natsbridge.js');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
class FileDownloadService {
|
class FileDownloadService {
|
||||||
@@ -690,7 +690,7 @@ module.exports = FileDownloadService;
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
# src/file_download_service.py
|
# src/file_download_service.py
|
||||||
from natbridge import smartreceive, fetch_with_backoff
|
from natsbridge import smartreceive, fetch_with_backoff
|
||||||
import os
|
import os
|
||||||
|
|
||||||
class FileDownloadService:
|
class FileDownloadService:
|
||||||
@@ -832,7 +832,7 @@ end
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// src/sensor_data.js
|
// src/sensor_data.js
|
||||||
const NATSBridge = require('./src/natbridge.js');
|
const NATSBridge = require('./src/natsbridge.js');
|
||||||
|
|
||||||
class SensorReading {
|
class SensorReading {
|
||||||
constructor(sensorId, value, unit, metadata = {}) {
|
constructor(sensorId, value, unit, metadata = {}) {
|
||||||
@@ -959,25 +959,14 @@ function send_batch(sender::SensorSender, readings::Vector{SensorReading})
|
|||||||
|
|
||||||
arrow_data = take!(buf)
|
arrow_data = take!(buf)
|
||||||
|
|
||||||
# Send based on size
|
# Send based on size (auto-selected by smartsend)
|
||||||
if length(arrow_data) < 1048576 # < 1MB
|
data = [("batch", arrow_data, "arrowtable")]
|
||||||
data = [("batch", arrow_data, "table")]
|
smartsend(
|
||||||
smartsend(
|
"/sensors/batch",
|
||||||
"/sensors/batch",
|
data,
|
||||||
data,
|
broker_url=sender.broker_url,
|
||||||
broker_url=sender.broker_url,
|
fileserver_url=sender.fileserver_url
|
||||||
fileserver_url=sender.fileserver_url
|
)
|
||||||
)
|
|
||||||
else
|
|
||||||
# Upload to file server
|
|
||||||
data = [("batch", arrow_data, "table")]
|
|
||||||
smartsend(
|
|
||||||
"/sensors/batch",
|
|
||||||
data,
|
|
||||||
broker_url=sender.broker_url,
|
|
||||||
fileserver_url=sender.fileserver_url
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -985,7 +974,7 @@ end
|
|||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// src/sensor_sender.js
|
// src/sensor_sender.js
|
||||||
const NATSBridge = require('./src/natbridge.js');
|
const NATSBridge = require('./src/natsbridge.js');
|
||||||
const { SensorReading, SensorBatch } = require('./sensor_data.js');
|
const { SensorReading, SensorBatch } = require('./sensor_data.js');
|
||||||
|
|
||||||
class SensorSender {
|
class SensorSender {
|
||||||
@@ -1037,28 +1026,16 @@ class SensorSender {
|
|||||||
const buffer = arrow.tableFromBatches([recordBatch]).toBuffer();
|
const buffer = arrow.tableFromBatches([recordBatch]).toBuffer();
|
||||||
const arrow_data = new Uint8Array(buffer);
|
const arrow_data = new Uint8Array(buffer);
|
||||||
|
|
||||||
// Send based on size
|
// Send based on size (auto-selected by smartsend)
|
||||||
if (arrow_data.length < 1048576) {
|
const data = [["batch", arrow_data, "arrowtable"]];
|
||||||
const data = [["batch", arrow_data, "table"]];
|
await NATSBridge.smartsend(
|
||||||
await NATSBridge.smartsend(
|
"/sensors/batch",
|
||||||
"/sensors/batch",
|
data,
|
||||||
data,
|
{
|
||||||
{
|
broker_url: this.broker_url,
|
||||||
broker_url: this.broker_url,
|
fileserver_url: this.fileserver_url
|
||||||
fileserver_url: this.fileserver_url
|
}
|
||||||
}
|
);
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const data = [["batch", arrow_data, "table"]];
|
|
||||||
await NATSBridge.smartsend(
|
|
||||||
"/sensors/batch",
|
|
||||||
data,
|
|
||||||
{
|
|
||||||
broker_url: this.broker_url,
|
|
||||||
fileserver_url: this.fileserver_url
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1069,7 +1046,7 @@ module.exports = SensorSender;
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
# src/sensor_sender.py
|
# src/sensor_sender.py
|
||||||
from natbridge import smartsend
|
from natsbridge import smartsend
|
||||||
from sensor_data import SensorReading, SensorBatch
|
from sensor_data import SensorReading, SensorBatch
|
||||||
|
|
||||||
class SensorSender:
|
class SensorSender:
|
||||||
@@ -1109,7 +1086,7 @@ class SensorSender:
|
|||||||
arrow_data = buf.getvalue()
|
arrow_data = buf.getvalue()
|
||||||
|
|
||||||
# Send based on size (auto-selected by smartsend)
|
# Send based on size (auto-selected by smartsend)
|
||||||
data = [("batch", arrow_data, "table")]
|
data = [("batch", arrow_data, "arrowtable")]
|
||||||
await smartsend(
|
await smartsend(
|
||||||
"/sensors/batch",
|
"/sensors/batch",
|
||||||
data,
|
data,
|
||||||
@@ -1152,7 +1129,7 @@ function send_batch_readings(sender::SensorSender, readings::Vector{Tuple{String
|
|||||||
# Send as single message
|
# Send as single message
|
||||||
smartsend(
|
smartsend(
|
||||||
"/sensors/batch",
|
"/sensors/batch",
|
||||||
[("batch", arrow_data, "table")],
|
[("batch", arrow_data, "arrowtable")],
|
||||||
broker_url=sender.broker_url
|
broker_url=sender.broker_url
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -1193,7 +1170,7 @@ async function sendBatchReadings(sender, readings) {
|
|||||||
const arrow_data = new Uint8Array(buffer);
|
const arrow_data = new Uint8Array(buffer);
|
||||||
|
|
||||||
// Send as single message
|
// Send as single message
|
||||||
const data = [["batch", arrow_data, "table"]];
|
const data = [["batch", arrow_data, "arrowtable"]];
|
||||||
await NATSBridge.smartsend(
|
await NATSBridge.smartsend(
|
||||||
"/sensors/batch",
|
"/sensors/batch",
|
||||||
data,
|
data,
|
||||||
@@ -1282,7 +1259,7 @@ end
|
|||||||
# Cache file server responses
|
# Cache file server responses
|
||||||
import asyncio
|
import asyncio
|
||||||
import threading
|
import threading
|
||||||
from natbridge import fetch_with_backoff
|
from natsbridge import fetch_with_backoff
|
||||||
|
|
||||||
file_cache = {}
|
file_cache = {}
|
||||||
cache_lock = threading.Lock()
|
cache_lock = threading.Lock()
|
||||||
@@ -1398,4 +1375,4 @@ For more information, check the [API documentation](../src/README.md) and [test
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
38
etc.jl
38
etc.jl
@@ -1,38 +0,0 @@
|
|||||||
Task: Update README.md to reflect recent changes in NATSbridge package.
|
|
||||||
|
|
||||||
Context: the package has been updated with the NATS_connection keyword and the publish_message function.
|
|
||||||
|
|
||||||
Requirements:
|
|
||||||
|
|
||||||
Source of Truth: Treat the updated NATSbridge code as the definitive source. Update README.md to align exactly with these changes.
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -31,15 +31,23 @@
|
|||||||
# [(dataname1, data1, type1), (dataname2, data2, type2), ...]
|
# [(dataname1, data1, type1), (dataname2, data2, type2), ...]
|
||||||
# ```
|
# ```
|
||||||
#
|
#
|
||||||
# Supported types: "text", "dictionary", "table", "image", "audio", "video", "binary"
|
# Supported types: "text", "dictionary", "arrowtable", "jsontable", "image", "audio", "video", "binary"
|
||||||
|
#
|
||||||
|
# Table Datatypes:
|
||||||
|
# - `arrowtable`: Apache Arrow IPC format for efficient binary serialization
|
||||||
|
# - Input: DataFrame, Arrow.Table
|
||||||
|
# - Encoding: arrow-ipc
|
||||||
|
# - `jsontable`: JSON format for human-readable tabular data
|
||||||
|
# - Input: Vector{NamedTuple}, Vector{Dict}
|
||||||
|
# - Encoding: json
|
||||||
|
|
||||||
module NATSBridge
|
module NATSBridge
|
||||||
|
|
||||||
using NATS, JSON, Arrow, HTTP, UUIDs, Dates, Base64, PrettyPrinting
|
using NATS, JSON, Arrow, HTTP, UUIDs, Dates, Base64, PrettyPrinting, DataFrames
|
||||||
# ---------------------------------------------- 100 --------------------------------------------- #
|
# ---------------------------------------------- 100 --------------------------------------------- #
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
const DEFAULT_SIZE_THRESHOLD = 1_000_000 # 1MB - threshold for switching from direct to link transport
|
const DEFAULT_SIZE_THRESHOLD = 500_000 # 0.5MB - threshold for switching from direct to link transport
|
||||||
const DEFAULT_BROKER_URL = "nats://localhost:4222" # Default NATS server URL
|
const DEFAULT_BROKER_URL = "nats://localhost:4222" # Default NATS server URL
|
||||||
const DEFAULT_FILESERVER_URL = "http://localhost:8080" # Default HTTP file server URL for link transport
|
const DEFAULT_FILESERVER_URL = "http://localhost:8080" # Default HTTP file server URL for link transport
|
||||||
|
|
||||||
@@ -51,7 +59,7 @@ It supports both direct transport (base64-encoded data) and link transport (URL-
|
|||||||
# Arguments:
|
# Arguments:
|
||||||
- `id::String` - Unique identifier for this payload (e.g., "uuid4")
|
- `id::String` - Unique identifier for this payload (e.g., "uuid4")
|
||||||
- `dataname::String` - Name of the payload (e.g., "login_image")
|
- `dataname::String` - Name of the payload (e.g., "login_image")
|
||||||
- `payload_type::String` - Payload type: "text", "dictionary", "table", "image", "audio", "video", "binary"
|
- `payload_type::String` - Payload type: "text", "dictionary", "arrowtable", "jsontable", "image", "audio", "video", "binary"
|
||||||
- `transport::String` - Transport method: "direct" or "link"
|
- `transport::String` - Transport method: "direct" or "link"
|
||||||
- `encoding::String` - Encoding method: "none", "json", "base64", "arrow-ipc"
|
- `encoding::String` - Encoding method: "none", "json", "base64", "arrow-ipc"
|
||||||
- `size::Integer` - Size of the payload in bytes (e.g., 15433)
|
- `size::Integer` - Size of the payload in bytes (e.g., 15433)
|
||||||
@@ -100,7 +108,7 @@ payload = msg_payload_v1(
|
|||||||
struct msg_payload_v1
|
struct msg_payload_v1
|
||||||
id::String # id of this payload e.g. "uuid4"
|
id::String # id of this payload e.g. "uuid4"
|
||||||
dataname::String # name of this payload e.g. "login_image"
|
dataname::String # name of this payload e.g. "login_image"
|
||||||
payload_type::String # this payload type. Can be "text", "dictionary", "table", "image", "audio", "video", "binary"
|
payload_type::String # this payload type. Can be "text", "dictionary", "arrowtable", "jsontable", "image", "audio", "video", "binary"
|
||||||
transport::String # transport method: "direct" or "link"
|
transport::String # transport method: "direct" or "link"
|
||||||
encoding::String # encoding method: "none", "json", "base64", "arrow-ipc"
|
encoding::String # encoding method: "none", "json", "base64", "arrow-ipc"
|
||||||
size::Integer # data size in bytes e.g. 15433
|
size::Integer # data size in bytes e.g. 15433
|
||||||
@@ -292,19 +300,7 @@ function envelope_to_json(env::msg_envelope_v1)
|
|||||||
"encoding" => payload.encoding,
|
"encoding" => payload.encoding,
|
||||||
"size" => payload.size,
|
"size" => payload.size,
|
||||||
)
|
)
|
||||||
# Include data based on transport type
|
payload_obj["data"] = payload.data
|
||||||
if payload.transport == "direct" && payload.data !== nothing
|
|
||||||
if payload.encoding == "base64" || payload.encoding == "json"
|
|
||||||
payload_obj["data"] = payload.data
|
|
||||||
else
|
|
||||||
# For other encodings, use base64
|
|
||||||
payload_bytes = _get_payload_bytes(payload.data)
|
|
||||||
payload_obj["data"] = Base64.base64encode(payload_bytes)
|
|
||||||
end
|
|
||||||
elseif payload.transport == "link" && payload.data !== nothing
|
|
||||||
# For link transport, data is a URL string - include directly
|
|
||||||
payload_obj["data"] = payload.data
|
|
||||||
end
|
|
||||||
if !isempty(payload.metadata)
|
if !isempty(payload.metadata)
|
||||||
payload_obj["metadata"] = Dict(String(k) => v for (k, v) in payload.metadata)
|
payload_obj["metadata"] = Dict(String(k) => v for (k, v) in payload.metadata)
|
||||||
end
|
end
|
||||||
@@ -363,7 +359,7 @@ Each payload can have a different type, enabling mixed-content messages (e.g., c
|
|||||||
- `data::AbstractArray{Tuple{String, Any, String}}` - List of (dataname, data, type) tuples to send
|
- `data::AbstractArray{Tuple{String, Any, String}}` - List of (dataname, data, type) tuples to send
|
||||||
- `dataname::String` - Name of the payload
|
- `dataname::String` - Name of the payload
|
||||||
- `data::Any` - The actual data to send
|
- `data::Any` - The actual data to send
|
||||||
- `payload_type::String` - Payload type: "text", "dictionary", "table", "image", "audio", "video", "binary"
|
- `payload_type::String` - Payload type: "text", "dictionary", "arrowtable", "jsontable", "image", "audio", "video", "binary"
|
||||||
- No standalone `type` parameter - type is specified per payload
|
- No standalone `type` parameter - type is specified per payload
|
||||||
|
|
||||||
# Keyword Arguments:
|
# Keyword Arguments:
|
||||||
@@ -399,11 +395,15 @@ env, msg_json = smartsend("my.subject", [("dataname1", data, "dictionary")])
|
|||||||
# Send multiple payloads in one message with different types
|
# Send multiple payloads in one message with different types
|
||||||
data1 = Dict("key1" => "value1")
|
data1 = Dict("key1" => "value1")
|
||||||
data2 = rand(10_000) # Small array
|
data2 = rand(10_000) # Small array
|
||||||
env, msg_json = smartsend("my.subject", [("dataname1", data1, "dictionary"), ("dataname2", data2, "table")])
|
env, msg_json = smartsend("my.subject", [("dataname1", data1, "dictionary"), ("dataname2", data2, "arrowtable")])
|
||||||
|
|
||||||
# Send a large array using fileserver upload
|
# Send a large array using fileserver upload
|
||||||
data = rand(10_000_000) # ~80 MB
|
data = rand(10_000_000) # ~80 MB
|
||||||
env, msg_json = smartsend("large.data", [("large_table", data, "table")])
|
env, msg_json = smartsend("large.data", [("large_arrow_table", data, "arrowtable")])
|
||||||
|
|
||||||
|
# Send jsontable (JSON format)
|
||||||
|
rows = [Dict("id" => 1, "name" => "Alice"), Dict("id" => 2, "name" => "Bob")]
|
||||||
|
env, msg_json = smartsend("json.data", [("users", rows, "jsontable")])
|
||||||
|
|
||||||
# Mixed content (e.g., chat with text and image)
|
# Mixed content (e.g., chat with text and image)
|
||||||
env, msg_json = smartsend("chat.subject", [
|
env, msg_json = smartsend("chat.subject", [
|
||||||
@@ -424,13 +424,12 @@ function smartsend(
|
|||||||
fileserver_upload_handler::Function = plik_oneshot_upload, # a function to handle uploading data to specific HTTP fileserver
|
fileserver_upload_handler::Function = plik_oneshot_upload, # a function to handle uploading data to specific HTTP fileserver
|
||||||
size_threshold::Int = DEFAULT_SIZE_THRESHOLD,
|
size_threshold::Int = DEFAULT_SIZE_THRESHOLD,
|
||||||
|
|
||||||
#=
|
# Generate a globally unique identifier (UUID) at the start of the request.
|
||||||
Generate a globally unique identifier (UUID) at the start of the request.
|
# This ID must remain constant and immutable as it propagates through every
|
||||||
This ID must remain constant and immutable as it propagates through every
|
# stage of the execution pipeline. It serves as the end-to-end ID for
|
||||||
stage of the execution pipeline. It serves as the end-to-end ID for
|
# distributed tracing, enabling the correlation of all logs, metrics, and
|
||||||
distributed tracing, enabling the correlation of all logs, metrics, and
|
# errors across the system back to this specific request instance.
|
||||||
errors across the system back to this specific request instance.
|
|
||||||
=#
|
|
||||||
correlation_id::String = string(uuid4()),
|
correlation_id::String = string(uuid4()),
|
||||||
|
|
||||||
msg_purpose::String = "chat",
|
msg_purpose::String = "chat",
|
||||||
@@ -451,6 +450,8 @@ function smartsend(
|
|||||||
# Process each payload in the list
|
# Process each payload in the list
|
||||||
payloads = msg_payload_v1[]
|
payloads = msg_payload_v1[]
|
||||||
for (dataname, payload_data, payload_type) in data
|
for (dataname, payload_data, payload_type) in data
|
||||||
|
@show dataname typeof(payload_data)
|
||||||
|
|
||||||
# Serialize data based on type
|
# Serialize data based on type
|
||||||
payload_bytes = _serialize_data(payload_data, payload_type)
|
payload_bytes = _serialize_data(payload_data, payload_type)
|
||||||
|
|
||||||
@@ -463,6 +464,14 @@ function smartsend(
|
|||||||
payload_b64 = Base64.base64encode(payload_bytes) # Encode bytes as base64 string
|
payload_b64 = Base64.base64encode(payload_bytes) # Encode bytes as base64 string
|
||||||
log_trace(correlation_id, "Using direct transport for $payload_size bytes") # Log transport choice
|
log_trace(correlation_id, "Using direct transport for $payload_size bytes") # Log transport choice
|
||||||
|
|
||||||
|
# Determine encoding based on payload_type
|
||||||
|
encoding = "base64"
|
||||||
|
if payload_type == "jsontable"
|
||||||
|
encoding = "json"
|
||||||
|
elseif payload_type == "arrowtable"
|
||||||
|
encoding = "arrow-ipc"
|
||||||
|
end
|
||||||
|
|
||||||
# Create msg_payload_v1 for direct transport
|
# Create msg_payload_v1 for direct transport
|
||||||
payload = msg_payload_v1(
|
payload = msg_payload_v1(
|
||||||
payload_b64,
|
payload_b64,
|
||||||
@@ -470,7 +479,7 @@ function smartsend(
|
|||||||
id = string(uuid4()),
|
id = string(uuid4()),
|
||||||
dataname = dataname,
|
dataname = dataname,
|
||||||
transport = "direct",
|
transport = "direct",
|
||||||
encoding = "base64",
|
encoding = encoding,
|
||||||
size = payload_size,
|
size = payload_size,
|
||||||
metadata = Dict{String, Any}("payload_bytes" => payload_size)
|
metadata = Dict{String, Any}("payload_bytes" => payload_size)
|
||||||
)
|
)
|
||||||
@@ -481,7 +490,7 @@ function smartsend(
|
|||||||
|
|
||||||
# Upload to HTTP server
|
# Upload to HTTP server
|
||||||
response = fileserver_upload_handler(fileserver_url, dataname, payload_bytes)
|
response = fileserver_upload_handler(fileserver_url, dataname, payload_bytes)
|
||||||
|
|
||||||
if response["status"] != 200 # Check if upload was successful
|
if response["status"] != 200 # Check if upload was successful
|
||||||
error("Failed to upload data to fileserver: $(response["status"])") # Throw error if upload failed
|
error("Failed to upload data to fileserver: $(response["status"])") # Throw error if upload failed
|
||||||
end
|
end
|
||||||
@@ -489,6 +498,14 @@ function smartsend(
|
|||||||
url = response["url"] # URL for the uploaded data
|
url = response["url"] # URL for the uploaded data
|
||||||
log_trace(correlation_id, "Uploaded to URL: $url") # Log successful upload
|
log_trace(correlation_id, "Uploaded to URL: $url") # Log successful upload
|
||||||
|
|
||||||
|
# Determine encoding based on payload_type
|
||||||
|
encoding = "none"
|
||||||
|
if payload_type == "jsontable"
|
||||||
|
encoding = "json"
|
||||||
|
elseif payload_type == "arrowtable"
|
||||||
|
encoding = "arrow-ipc"
|
||||||
|
end
|
||||||
|
|
||||||
# Create msg_payload_v1 for link transport
|
# Create msg_payload_v1 for link transport
|
||||||
payload = msg_payload_v1(
|
payload = msg_payload_v1(
|
||||||
url,
|
url,
|
||||||
@@ -496,7 +513,7 @@ function smartsend(
|
|||||||
id = string(uuid4()),
|
id = string(uuid4()),
|
||||||
dataname = dataname,
|
dataname = dataname,
|
||||||
transport = "link",
|
transport = "link",
|
||||||
encoding = "none",
|
encoding = encoding,
|
||||||
size = payload_size,
|
size = payload_size,
|
||||||
metadata = Dict{String, Any}()
|
metadata = Dict{String, Any}()
|
||||||
)
|
)
|
||||||
@@ -543,12 +560,13 @@ It supports multiple serialization formats for different data types.
|
|||||||
2. Converts data to binary representation according to format rules
|
2. Converts data to binary representation according to format rules
|
||||||
3. For text: converts string to UTF-8 bytes
|
3. For text: converts string to UTF-8 bytes
|
||||||
4. For dictionary: serializes as JSON then converts to bytes
|
4. For dictionary: serializes as JSON then converts to bytes
|
||||||
5. For table: uses Arrow.jl to write as IPC stream
|
5. For arrowtable: uses Arrow.jl to write as IPC stream
|
||||||
6. For image/audio/video/binary: returns binary data directly
|
6. For jsontable: converts to JSON then to bytes
|
||||||
|
7. For image/audio/video/binary: returns binary data directly
|
||||||
|
|
||||||
# Arguments:
|
# Arguments:
|
||||||
- `data::Any` - Data to serialize (string for `"text"`, JSON-serializable for `"dictionary"`, table-like for `"table"`, binary for `"image"`, `"audio"`, `"video"`, `"binary"`)
|
- `data::Any` - Data to serialize (string for `"text"`, JSON-serializable for `"dictionary"`, table-like for `"arrowtable"`, Vector{NamedTuple}/Vector{Dict} for `"jsontable"`, binary for `"image"`, `"audio"`, `"video"`, `"binary"`)
|
||||||
- `payload_type::String` - Target format: "text", "dictionary", "table", "image", "audio", "video", "binary"
|
- `payload_type::String` - Target format: "text", "dictionary", "arrowtable", "jsontable", "image", "audio", "video", "binary"
|
||||||
|
|
||||||
# Return:
|
# Return:
|
||||||
- `Vector{UInt8}` - Binary representation of the serialized data
|
- `Vector{UInt8}` - Binary representation of the serialized data
|
||||||
@@ -569,9 +587,13 @@ text_bytes = _serialize_data(text_data, "text")
|
|||||||
json_data = Dict("name" => "Alice", "age" => 30)
|
json_data = Dict("name" => "Alice", "age" => 30)
|
||||||
json_bytes = _serialize_data(json_data, "dictionary")
|
json_bytes = _serialize_data(json_data, "dictionary")
|
||||||
|
|
||||||
# Table serialization with a DataFrame (recommended for tabular data)
|
# Arrow table serialization with a DataFrame (recommended for tabular data)
|
||||||
df = DataFrame(id = 1:3, name = ["Alice", "Bob", "Charlie"], score = [95, 88, 92])
|
df = DataFrame(id = 1:3, name = ["Alice", "Bob", "Charlie"], score = [95, 88, 92])
|
||||||
table_bytes = _serialize_data(df, "table")
|
arrow_bytes = _serialize_data(df, "arrowtable")
|
||||||
|
|
||||||
|
# JSON table serialization - Vector{NamedTuple} or Vector{Dict}
|
||||||
|
rows = [Dict("id" => 1, "name" => "Alice"), Dict("id" => 2, "name" => "Bob")]
|
||||||
|
json_bytes = _serialize_data(rows, "jsontable")
|
||||||
|
|
||||||
# Image data (Vector{UInt8})
|
# Image data (Vector{UInt8})
|
||||||
image_bytes = UInt8[1, 2, 3] # Image bytes
|
image_bytes = UInt8[1, 2, 3] # Image bytes
|
||||||
@@ -622,10 +644,30 @@ function _serialize_data(data::Any, payload_type::String)
|
|||||||
json_str = JSON.json(data) # Convert Julia data to JSON string
|
json_str = JSON.json(data) # Convert Julia data to JSON string
|
||||||
json_str_bytes = Vector{UInt8}(json_str) # Convert JSON string to bytes
|
json_str_bytes = Vector{UInt8}(json_str) # Convert JSON string to bytes
|
||||||
return json_str_bytes
|
return json_str_bytes
|
||||||
elseif payload_type == "table" # Table data - convert to Arrow IPC stream
|
elseif payload_type == "arrowtable" # Arrow table data - convert to Arrow IPC stream
|
||||||
io = IOBuffer() # Create in-memory buffer
|
io = IOBuffer() # Create in-memory buffer
|
||||||
Arrow.write(io, data) # Write data as Arrow IPC stream to buffer
|
Arrow.write(io, data) # Write data as Arrow IPC stream to buffer
|
||||||
return take!(io) # Return the buffer contents as bytes
|
return take!(io) # Return the buffer contents as bytes
|
||||||
|
elseif payload_type == "jsontable" # JSON table data - convert to JSON
|
||||||
|
# data can be Vector{NamedTuple}, Vector{Dict}, or DataFrame
|
||||||
|
# If DataFrame, convert to Vector{Dict} first
|
||||||
|
if isa(data, DataFrame)
|
||||||
|
# Convert DataFrame to Vector{Dict} (row-oriented)
|
||||||
|
rows = []
|
||||||
|
for i in 1:nrow(data)
|
||||||
|
row_dict = Dict()
|
||||||
|
for col in names(data)
|
||||||
|
row_dict[String(col)] = data[i, col]
|
||||||
|
end
|
||||||
|
push!(rows, row_dict)
|
||||||
|
end
|
||||||
|
json_str = JSON.json(rows)
|
||||||
|
return Vector{UInt8}(json_str)
|
||||||
|
else
|
||||||
|
# Already Vector{NamedTuple} or Vector{Dict}
|
||||||
|
json_str = JSON.json(data)
|
||||||
|
return Vector{UInt8}(json_str)
|
||||||
|
end
|
||||||
elseif payload_type == "image" # Image data - treat as binary
|
elseif payload_type == "image" # Image data - treat as binary
|
||||||
if isa(data, Vector{UInt8})
|
if isa(data, Vector{UInt8})
|
||||||
return data # Return binary data directly
|
return data # Return binary data directly
|
||||||
@@ -881,24 +923,25 @@ end
|
|||||||
|
|
||||||
""" _deserialize_data - Deserialize bytes to data based on type
|
""" _deserialize_data - Deserialize bytes to data based on type
|
||||||
This internal function converts serialized bytes back to Julia data based on type.
|
This internal function converts serialized bytes back to Julia data based on type.
|
||||||
It handles "text" (string), "dictionary" (JSON deserialization), "table" (Arrow IPC deserialization),
|
It handles "text" (string), "dictionary" (JSON deserialization), "arrowtable" (Arrow IPC deserialization),
|
||||||
"image" (binary data), "audio" (binary data), "video" (binary data), and "binary" (binary data).
|
"jsontable" (JSON deserialization), "image" (binary data), "audio" (binary data), "video" (binary data), and "binary" (binary data).
|
||||||
|
|
||||||
# Function Workflow:
|
# Function Workflow:
|
||||||
1. Validates the data type against supported formats
|
1. Validates the data type against supported formats
|
||||||
2. Converts bytes to appropriate Julia data type based on format
|
2. Converts bytes to appropriate Julia data type based on format
|
||||||
3. For text: converts bytes to string
|
3. For text: converts bytes to string
|
||||||
4. For dictionary: converts bytes to JSON string then parses to Julia object
|
4. For dictionary: converts bytes to JSON string then parses to Julia object
|
||||||
5. For table: reads Arrow IPC format and returns DataFrame
|
5. For arrowtable: reads Arrow IPC format and returns a DataFrame
|
||||||
6. For image/audio/video/binary: returns bytes directly
|
6. For jsontable: converts bytes to JSON string then parses to Vector{Dict} and return a DataFrame
|
||||||
|
7. For image/audio/video/binary: returns bytes directly
|
||||||
|
|
||||||
# Arguments:
|
# Arguments:
|
||||||
- `data::Vector{UInt8}` - Serialized data as bytes
|
- `data::Vector{UInt8}` - Serialized data as bytes
|
||||||
- `payload_type::String` - Data type ("text", "dictionary", "table", "image", "audio", "video", "binary")
|
- `payload_type::String` - Data type ("text", "dictionary", "arrowtable", "jsontable", "image", "audio", "video", "binary")
|
||||||
- `correlation_id::String` - Correlation ID for logging
|
- `correlation_id::String` - Correlation ID for logging
|
||||||
|
|
||||||
# Return:
|
# Return:
|
||||||
- Deserialized data (String for "text", DataFrame for "table", JSON data for "dictionary", bytes for "image", "audio", "video", "binary")
|
- Deserialized data (String for "text", Arrow.Table for "arrowtable", Vector{Dict} for "jsontable", JSON data for "dictionary", bytes for "image", "audio", "video", "binary")
|
||||||
|
|
||||||
# Throws:
|
# Throws:
|
||||||
- `Error` if `payload_type` is not one of the supported types
|
- `Error` if `payload_type` is not one of the supported types
|
||||||
@@ -913,9 +956,13 @@ text_data = _deserialize_data(text_bytes, "text", "correlation123")
|
|||||||
json_bytes = UInt8[123, 34, 110, 97, 109, 101, 34, 58, 34, 65, 108, 105, 99, 101, 125] # {"name":"Alice"}
|
json_bytes = UInt8[123, 34, 110, 97, 109, 101, 34, 58, 34, 65, 108, 105, 99, 101, 125] # {"name":"Alice"}
|
||||||
json_data = _deserialize_data(json_bytes, "dictionary", "correlation123")
|
json_data = _deserialize_data(json_bytes, "dictionary", "correlation123")
|
||||||
|
|
||||||
# Arrow IPC data (table)
|
# Arrow IPC data (arrowtable)
|
||||||
arrow_bytes = Vector{UInt8}([1, 2, 3]) # Arrow IPC bytes
|
arrow_bytes = Vector{UInt8}([1, 2, 3]) # Arrow IPC bytes
|
||||||
table_data = _deserialize_data(arrow_bytes, "table", "correlation123")
|
df = _deserialize_data(arrow_bytes, "arrowtable", "correlation123")
|
||||||
|
|
||||||
|
# JSON table data (jsontable)
|
||||||
|
json_table_bytes = UInt8[91, 123, 34, 105, 100, 34, 58, 49, 44, 34, 110, 97, 109, 101, 34, 58, 34, 65, 108, 105, 99, 101, 34, 125] # [{"id":1,"name":"Alice"}]
|
||||||
|
df = _deserialize_data(json_table_bytes, "jsontable", "correlation123")
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function _deserialize_data(
|
function _deserialize_data(
|
||||||
@@ -928,10 +975,16 @@ function _deserialize_data(
|
|||||||
elseif payload_type == "dictionary" # JSON data - deserialize
|
elseif payload_type == "dictionary" # JSON data - deserialize
|
||||||
json_str = String(data) # Convert bytes to string
|
json_str = String(data) # Convert bytes to string
|
||||||
return JSON.parse(json_str) # Parse JSON string to JSON object
|
return JSON.parse(json_str) # Parse JSON string to JSON object
|
||||||
elseif payload_type == "table" # Table data - deserialize Arrow IPC stream
|
elseif payload_type == "arrowtable" # Arrow table data - deserialize Arrow IPC stream
|
||||||
io = IOBuffer(data) # Create buffer from bytes
|
io = IOBuffer(data) # Create buffer from bytes
|
||||||
df = Arrow.Table(io) # Read Arrow IPC format from buffer
|
arrowtable = Arrow.Table(io) # Read Arrow IPC format from buffer
|
||||||
return df # Return DataFrame
|
df = DataFrame(arrowtable)
|
||||||
|
return df
|
||||||
|
elseif payload_type == "jsontable" # JSON table data - deserialize JSON
|
||||||
|
json_str = String(data) # Convert bytes to string
|
||||||
|
jsontable = JSON.parse(json_str) # Parse JSON string to jsontable i.e. Vector{Dict}
|
||||||
|
df = DataFrame(jsontable)
|
||||||
|
return df
|
||||||
elseif payload_type == "image" # Image data - return binary
|
elseif payload_type == "image" # Image data - return binary
|
||||||
return data # Return bytes directly
|
return data # Return bytes directly
|
||||||
elseif payload_type == "audio" # Audio data - return binary
|
elseif payload_type == "audio" # Audio data - return binary
|
||||||
@@ -970,19 +1023,19 @@ retrieves an upload ID and token, then uploads the file data as multipart form d
|
|||||||
- `"url"` - Full URL to download the uploaded file
|
- `"url"` - Full URL to download the uploaded file
|
||||||
|
|
||||||
# Example
|
# Example
|
||||||
```jldoctest
|
```jldoctest
|
||||||
using HTTP, JSON
|
using HTTP, JSON
|
||||||
|
|
||||||
fileserver_url = "http://localhost:8080"
|
fileserver_url = "http://localhost:8080"
|
||||||
dataname = "test.txt"
|
dataname = "test.txt"
|
||||||
data = Vector{UInt8}("hello world")
|
data = Vector{UInt8}("hello world")
|
||||||
|
|
||||||
# Upload to local plik server
|
# Upload to local plik server
|
||||||
result = plik_oneshot_upload(file_server_url, dataname, data)
|
result = plik_oneshot_upload(file_server_url, dataname, data)
|
||||||
|
|
||||||
# Access the result as a Dict
|
# Access the result as a Dict
|
||||||
# result["status"], result["uploadid"], result["fileid"], result["url"]
|
# result["status"], result["uploadid"], result["fileid"], result["url"]
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
function plik_oneshot_upload(file_server_url::String, dataname::String, data::Vector{UInt8})
|
function plik_oneshot_upload(file_server_url::String, dataname::String, data::Vector{UInt8})
|
||||||
|
|
||||||
@@ -1106,18 +1159,4 @@ end
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
end # module
|
end # module
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ This module provides functionality for sending and receiving data across network
|
|||||||
using NATS as the message bus, with support for both direct payload transport and
|
using NATS as the message bus, with support for both direct payload transport and
|
||||||
URL-based transport for larger payloads.
|
URL-based transport for larger payloads.
|
||||||
|
|
||||||
@package natbridge
|
@package natsbridge
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -34,9 +34,9 @@ except ImportError:
|
|||||||
# ---------------------------------------------- Constants ---------------------------------------------- #
|
# ---------------------------------------------- Constants ---------------------------------------------- #
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Default size threshold for switching from direct to link transport (1MB)
|
Default size threshold for switching from direct to link transport (0.5MB)
|
||||||
"""
|
"""
|
||||||
DEFAULT_SIZE_THRESHOLD = 1_000_000
|
DEFAULT_SIZE_THRESHOLD = 500_000
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Default NATS server URL
|
Default NATS server URL
|
||||||
@@ -71,8 +71,9 @@ def _serialize_data(data: Any, payload_type: str) -> bytes:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
data: Data to serialize (string for "text", JSON-serializable for "dictionary",
|
data: Data to serialize (string for "text", JSON-serializable for "dictionary",
|
||||||
table-like for "table", binary for "image", "audio", "video", "binary")
|
table-like for "arrowtable"/"jsontable", binary for "image", "audio", "video", "binary")
|
||||||
payload_type: Target format: "text", "dictionary", "table", "image", "audio", "video", "binary"
|
payload_type: Target format: "text", "dictionary", "arrowtable", "jsontable",
|
||||||
|
"image", "audio", "video", "binary"
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Binary representation of the serialized data
|
Binary representation of the serialized data
|
||||||
@@ -80,7 +81,8 @@ def _serialize_data(data: Any, payload_type: str) -> bytes:
|
|||||||
Raises:
|
Raises:
|
||||||
Error: If payload_type is not one of the supported types
|
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 "image", "audio", or "video" but data is not bytes
|
||||||
Error: If payload_type is "table" but data is not a pandas DataFrame or pyarrow Table
|
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 payload_type == 'text':
|
||||||
if isinstance(data, str):
|
if isinstance(data, str):
|
||||||
@@ -90,9 +92,9 @@ def _serialize_data(data: Any, payload_type: str) -> bytes:
|
|||||||
elif payload_type == 'dictionary':
|
elif payload_type == 'dictionary':
|
||||||
json_str = json.dumps(data)
|
json_str = json.dumps(data)
|
||||||
return json_str.encode('utf-8')
|
return json_str.encode('utf-8')
|
||||||
elif payload_type == 'table':
|
elif payload_type == 'arrowtable':
|
||||||
if not ARROW_AVAILABLE:
|
if not ARROW_AVAILABLE:
|
||||||
raise RuntimeError('pyarrow not available for table serialization')
|
raise RuntimeError('pyarrow not available for arrowtable serialization')
|
||||||
|
|
||||||
import io
|
import io
|
||||||
buf = io.BytesIO()
|
buf = io.BytesIO()
|
||||||
@@ -110,7 +112,14 @@ def _serialize_data(data: Any, payload_type: str) -> bytes:
|
|||||||
sink.close()
|
sink.close()
|
||||||
return buf.getvalue()
|
return buf.getvalue()
|
||||||
else:
|
else:
|
||||||
raise ValueError('Table data must be a pandas DataFrame or pyarrow Table')
|
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':
|
elif payload_type == 'image':
|
||||||
if isinstance(data, (bytes, bytearray)):
|
if isinstance(data, (bytes, bytearray)):
|
||||||
return bytes(data)
|
return bytes(data)
|
||||||
@@ -141,12 +150,13 @@ def _deserialize_data(data: bytes, payload_type: str, correlation_id: str) -> An
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
data: Serialized data as bytes
|
data: Serialized data as bytes
|
||||||
payload_type: Data type ("text", "dictionary", "table", "image", "audio", "video", "binary")
|
payload_type: Data type ("text", "dictionary", "arrowtable", "jsontable",
|
||||||
|
"image", "audio", "video", "binary")
|
||||||
correlation_id: Correlation ID for logging
|
correlation_id: Correlation ID for logging
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Deserialized data (String for "text", DataFrame for "table", JSON data for "dictionary",
|
Deserialized data (String for "text", DataFrame for "arrowtable",
|
||||||
bytes for "image", "audio", "video", "binary")
|
Vector{Dict} for "jsontable"/"dictionary", bytes for "image", "audio", "video", "binary")
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
Error: If payload_type is not one of the supported types
|
Error: If payload_type is not one of the supported types
|
||||||
@@ -156,14 +166,18 @@ def _deserialize_data(data: bytes, payload_type: str, correlation_id: str) -> An
|
|||||||
elif payload_type == 'dictionary':
|
elif payload_type == 'dictionary':
|
||||||
json_str = data.decode('utf-8')
|
json_str = data.decode('utf-8')
|
||||||
return json.loads(json_str)
|
return json.loads(json_str)
|
||||||
elif payload_type == 'table':
|
elif payload_type == 'arrowtable':
|
||||||
if not ARROW_AVAILABLE:
|
if not ARROW_AVAILABLE:
|
||||||
raise RuntimeError('pyarrow not available for table deserialization')
|
raise RuntimeError('pyarrow not available for arrowtable deserialization')
|
||||||
|
|
||||||
import io
|
import io
|
||||||
buf = io.BytesIO(data)
|
buf = io.BytesIO(data)
|
||||||
reader = ipc.open_file(buf)
|
reader = ipc.open_file(buf)
|
||||||
return reader.read_all().to_pandas()
|
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':
|
elif payload_type == 'image':
|
||||||
return data
|
return data
|
||||||
elif payload_type == 'audio':
|
elif payload_type == 'audio':
|
||||||
@@ -317,7 +331,7 @@ class NATSClient:
|
|||||||
self._client = nats.connect(self.url)
|
self._client = nats.connect(self.url)
|
||||||
await self._client
|
await self._client
|
||||||
else:
|
else:
|
||||||
raise Error('nats-py not available')
|
raise RuntimeError('nats-py not available')
|
||||||
return self._client
|
return self._client
|
||||||
|
|
||||||
async def publish(self, subject: str, message: str, correlation_id: str = "") -> None:
|
async def publish(self, subject: str, message: str, correlation_id: str = "") -> None:
|
||||||
@@ -397,12 +411,19 @@ def _build_payload(
|
|||||||
Returns:
|
Returns:
|
||||||
Payload object
|
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 {
|
return {
|
||||||
'id': str(uuid.uuid4()),
|
'id': str(uuid.uuid4()),
|
||||||
'dataname': dataname,
|
'dataname': dataname,
|
||||||
'payload_type': payload_type,
|
'payload_type': payload_type,
|
||||||
'transport': transport,
|
'transport': transport,
|
||||||
'encoding': 'base64' if transport == 'direct' else 'none',
|
'encoding': encoding,
|
||||||
'size': len(payload_bytes),
|
'size': len(payload_bytes),
|
||||||
'data': data,
|
'data': data,
|
||||||
'metadata': {'payload_bytes': len(payload_bytes)} if transport == 'direct' else {}
|
'metadata': {'payload_bytes': len(payload_bytes)} if transport == 'direct' else {}
|
||||||
@@ -476,7 +497,7 @@ async def smartsend(
|
|||||||
data: List of (dataname, data, type) tuples to send
|
data: List of (dataname, data, type) tuples to send
|
||||||
- dataname: Name of the payload
|
- dataname: Name of the payload
|
||||||
- data: The actual data to send
|
- data: The actual data to send
|
||||||
- type: Payload type: "text", "dictionary", "table", "image", "audio", "video", "binary"
|
- type: Payload type: "text", "dictionary", "arrowtable", "jsontable", "image", "audio", "video", "binary"
|
||||||
broker_url: URL of the NATS server
|
broker_url: URL of the NATS server
|
||||||
fileserver_url: URL of the HTTP file server for large payloads
|
fileserver_url: URL of the HTTP file server for large payloads
|
||||||
fileserver_upload_handler: Function to handle fileserver uploads (must return Dict with "status",
|
fileserver_upload_handler: Function to handle fileserver uploads (must return Dict with "status",
|
||||||
@@ -514,14 +535,21 @@ async def smartsend(
|
|||||||
>>> data2 = [1, 2, 3, 4, 5]
|
>>> data2 = [1, 2, 3, 4, 5]
|
||||||
>>> env, env_json_str = await smartsend(
|
>>> env, env_json_str = await smartsend(
|
||||||
... "my.subject",
|
... "my.subject",
|
||||||
... [("dataname1", data1, "dictionary"), ("dataname2", data2, "table")]
|
... [("dataname1", data1, "dictionary"), ("dataname2", data2, "arrowtable")]
|
||||||
... )
|
... )
|
||||||
>>>
|
>>>
|
||||||
>>> # Send a large array using fileserver upload
|
>>> # Send a large array using fileserver upload
|
||||||
>>> data = list(range(10_000_000)) # ~80 MB
|
>>> data = list(range(10_000_000)) # ~80 MB
|
||||||
>>> env, env_json_str = await smartsend(
|
>>> env, env_json_str = await smartsend(
|
||||||
... "large.data",
|
... "large.data",
|
||||||
... [("large_table", data, "table")]
|
... [("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)
|
>>> # Mixed content (e.g., chat with text and image)
|
||||||
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;
|
||||||
@@ -1,25 +1,37 @@
|
|||||||
/**
|
/**
|
||||||
* NATSBridge - Cross-Platform Bi-Directional Data Bridge
|
* NATSBridge - Cross-Platform Bi-Directional Data Bridge
|
||||||
* JavaScript/Node.js Implementation
|
* JavaScript/Node.js Implementation (Client-Side Rendering)
|
||||||
*
|
*
|
||||||
* This module provides functionality for sending and receiving data across network boundaries
|
* 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
|
* using NATS as the message bus, with support for both direct payload transport and
|
||||||
* URL-based transport for larger payloads.
|
* URL-based transport for larger payloads.
|
||||||
*
|
*
|
||||||
|
* Supported payload types: "text", "dictionary", "arrowtable", "jsontable", "image", "audio", "video", "binary"
|
||||||
|
*
|
||||||
* @module NATSBridge
|
* @module NATSBridge
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const nats = require('nats');
|
const nats = require('nats');
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const crypto = require('crypto');
|
||||||
const fetch = require('node-fetch');
|
// Use native fetch available in Node.js 18+
|
||||||
const arrow = require('apache-arrow');
|
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 ---------------------------------------------- //
|
// ---------------------------------------------- Constants ---------------------------------------------- //
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default size threshold for switching from direct to link transport (1MB)
|
* Default size threshold for switching from direct to link transport (0.5MB)
|
||||||
*/
|
*/
|
||||||
const DEFAULT_SIZE_THRESHOLD = 1_000_000;
|
const DEFAULT_SIZE_THRESHOLD = 500_000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default NATS server URL
|
* Default NATS server URL
|
||||||
@@ -57,7 +69,7 @@ function logTrace(correlationId, message) {
|
|||||||
/**
|
/**
|
||||||
* Serialize data according to specified format
|
* Serialize data according to specified format
|
||||||
* @param {any} data - Data to serialize
|
* @param {any} data - Data to serialize
|
||||||
* @param {string} payloadType - Target format: "text", "dictionary", "table", "image", "audio", "video", "binary"
|
* @param {string} payloadType - Target format: "text", "dictionary", "arrowtable", "jsontable", "image", "audio", "video", "binary"
|
||||||
* @returns {Buffer} Binary representation of the serialized data
|
* @returns {Buffer} Binary representation of the serialized data
|
||||||
*/
|
*/
|
||||||
async function serializeData(data, payloadType) {
|
async function serializeData(data, payloadType) {
|
||||||
@@ -70,13 +82,20 @@ async function serializeData(data, payloadType) {
|
|||||||
} else if (payloadType === 'dictionary') {
|
} else if (payloadType === 'dictionary') {
|
||||||
const jsonStr = JSON.stringify(data);
|
const jsonStr = JSON.stringify(data);
|
||||||
return Buffer.from(jsonStr, 'utf8');
|
return Buffer.from(jsonStr, 'utf8');
|
||||||
} else if (payloadType === 'table') {
|
} else if (payloadType === 'arrowtable') {
|
||||||
// Convert array of objects to Arrow IPC format
|
// Convert array of objects to Arrow IPC format
|
||||||
if (!Array.isArray(data) || data.length === 0) {
|
if (!Array.isArray(data) || data.length === 0) {
|
||||||
throw new Error('Table data must be a non-empty array of objects');
|
throw new Error('Arrow table data must be a non-empty array of objects');
|
||||||
}
|
}
|
||||||
|
|
||||||
return serializeArrowTable(data);
|
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') {
|
} else if (payloadType === 'image') {
|
||||||
if (data instanceof Uint8Array || Buffer.isBuffer(data)) {
|
if (data instanceof Uint8Array || Buffer.isBuffer(data)) {
|
||||||
return Buffer.from(data);
|
return Buffer.from(data);
|
||||||
@@ -116,41 +135,34 @@ function serializeArrowTable(data) {
|
|||||||
throw new Error('Table data must be a non-empty array of objects');
|
throw new Error('Table data must be a non-empty array of objects');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build schema from first row
|
logTrace('serializeArrowTable', `Serializing table with ${data.length} rows`);
|
||||||
const fields = Object.keys(data[0]).map(key => {
|
|
||||||
const value = data[0][key];
|
|
||||||
let arrowType;
|
|
||||||
if (typeof value === 'number') {
|
|
||||||
arrowType = Number.isInteger(value) ? arrow.Int64 : arrow.Float64;
|
|
||||||
} else if (typeof value === 'boolean') {
|
|
||||||
arrowType = arrow.Boolean;
|
|
||||||
} else if (value instanceof Date) {
|
|
||||||
arrowType = arrow.Date;
|
|
||||||
} else {
|
|
||||||
arrowType = arrow.Utf8;
|
|
||||||
}
|
|
||||||
return new arrow.Field(key, arrowType, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
const schema = new arrow.Schema(fields);
|
// Use arrow.tableFromArrays which handles the conversion properly
|
||||||
const batches = [];
|
// Convert array of objects to a key-value format expected by tableFromArrays
|
||||||
|
const columns = {};
|
||||||
// Create record batches
|
for (const key of Object.keys(data[0])) {
|
||||||
for (const row of data) {
|
columns[key] = data.map(row => row[key]);
|
||||||
const batch = arrow.recordBatch.fromObjects([row], schema);
|
|
||||||
batches.push(batch);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write to buffer using IPC format
|
logTrace('serializeArrowTable', `Columns: ${Object.keys(columns).join(', ')}`);
|
||||||
const buffers = arrow.ipc.recordBatchesToMessage(batches, schema).buffers;
|
|
||||||
const combined = new Uint8Array(buffers.reduce((acc, b) => acc + b.byteLength, 0));
|
|
||||||
let offset = 0;
|
|
||||||
for (const buf of buffers) {
|
|
||||||
combined.set(new Uint8Array(buf), offset);
|
|
||||||
offset += buf.byteLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Buffer.from(combined);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -163,21 +175,71 @@ function serializeArrowTable(data) {
|
|||||||
async function deserializeData(data, payloadType, correlationId) {
|
async function deserializeData(data, payloadType, correlationId) {
|
||||||
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
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') {
|
if (payloadType === 'text') {
|
||||||
return buffer.toString('utf8');
|
const result = buffer.toString('utf8');
|
||||||
|
logTrace(correlationId, `deserializeData: text result length=${result.length}`);
|
||||||
|
return result;
|
||||||
} else if (payloadType === 'dictionary') {
|
} else if (payloadType === 'dictionary') {
|
||||||
const jsonStr = buffer.toString('utf8');
|
const jsonStr = buffer.toString('utf8');
|
||||||
return JSON.parse(jsonStr);
|
const result = JSON.parse(jsonStr);
|
||||||
} else if (payloadType === 'table') {
|
logTrace(correlationId, `deserializeData: dictionary keys=${Object.keys(result).join(', ')}`);
|
||||||
const table = arrow.tableFromRawBytes(buffer);
|
return result;
|
||||||
return table;
|
} 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') {
|
} else if (payloadType === 'image') {
|
||||||
|
logTrace(correlationId, `deserializeData: image buffer length=${buffer.length}`);
|
||||||
return buffer;
|
return buffer;
|
||||||
} else if (payloadType === 'audio') {
|
} else if (payloadType === 'audio') {
|
||||||
|
logTrace(correlationId, `deserializeData: audio buffer length=${buffer.length}`);
|
||||||
return buffer;
|
return buffer;
|
||||||
} else if (payloadType === 'video') {
|
} else if (payloadType === 'video') {
|
||||||
|
logTrace(correlationId, `deserializeData: video buffer length=${buffer.length}`);
|
||||||
return buffer;
|
return buffer;
|
||||||
} else if (payloadType === 'binary') {
|
} else if (payloadType === 'binary') {
|
||||||
|
logTrace(correlationId, `deserializeData: binary buffer length=${buffer.length}`);
|
||||||
return buffer;
|
return buffer;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unknown payload_type: ${payloadType}`);
|
throw new Error(`Unknown payload_type: ${payloadType}`);
|
||||||
@@ -264,7 +326,7 @@ async function fetchWithBackoff(url, maxRetries, baseDelay, maxDelay, correlatio
|
|||||||
throw new Error(`Failed to fetch: ${response.status}`);
|
throw new Error(`Failed to fetch: ${response.status}`);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logTrace(correlationId, `Attempt ${attempt} failed: ${e.constructor.name}`);
|
logTrace(correlationId, `Attempt ${attempt} failed: ${e.constructor.name} - ${e.message}`);
|
||||||
|
|
||||||
if (attempt < maxRetries) {
|
if (attempt < maxRetries) {
|
||||||
await new Promise(resolve => setTimeout(resolve, delay));
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
@@ -338,8 +400,8 @@ async function publishMessage(brokerUrlOrClient, subject, message, correlationId
|
|||||||
|
|
||||||
if (brokerUrlOrClient instanceof NATSClient) {
|
if (brokerUrlOrClient instanceof NATSClient) {
|
||||||
conn = brokerUrlOrClient;
|
conn = brokerUrlOrClient;
|
||||||
} else if (brokerUrlOrClient instanceof nats.Connection) {
|
} else if (brokerUrlOrClient && typeof brokerUrlOrClient.publish === 'function') {
|
||||||
// Create a wrapper for direct connection
|
// Create a wrapper for direct connection (duck-typing check for NATS connection)
|
||||||
conn = {
|
conn = {
|
||||||
async publish(subj, msg) {
|
async publish(subj, msg) {
|
||||||
await brokerUrlOrClient.publish(subj, msg);
|
await brokerUrlOrClient.publish(subj, msg);
|
||||||
@@ -397,12 +459,20 @@ function buildEnvelope(subject, payloads, options) {
|
|||||||
* @returns {Object} Payload object
|
* @returns {Object} Payload object
|
||||||
*/
|
*/
|
||||||
function buildPayload(dataname, payloadType, payloadBytes, transport, data) {
|
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 {
|
return {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
dataname,
|
dataname,
|
||||||
payload_type: payloadType,
|
payload_type: payloadType,
|
||||||
transport,
|
transport,
|
||||||
encoding: transport === 'direct' ? 'base64' : 'none',
|
encoding,
|
||||||
size: payloadBytes.byteLength,
|
size: payloadBytes.byteLength,
|
||||||
data,
|
data,
|
||||||
metadata: transport === 'direct' ? { payload_bytes: payloadBytes.byteLength } : {}
|
metadata: transport === 'direct' ? { payload_bytes: payloadBytes.byteLength } : {}
|
||||||
@@ -419,12 +489,13 @@ function buildPayload(dataname, payloadType, payloadBytes, transport, data) {
|
|||||||
*
|
*
|
||||||
* @param {string} subject - NATS subject to publish the message to
|
* @param {string} subject - NATS subject to publish the message to
|
||||||
* @param {Array} data - List of [dataname, data, type] tuples to send
|
* @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 {Object} options - Optional configuration
|
||||||
* @param {string} [options.broker_url=DEFAULT_BROKER_URL] - URL of the NATS server
|
* @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 {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 {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 {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.correlation_id=crypto.randomUUID()] - Correlation ID for tracing
|
||||||
* @param {string} [options.msg_purpose="chat"] - Purpose of the message
|
* @param {string} [options.msg_purpose="chat"] - Purpose of the message
|
||||||
* @param {string} [options.sender_name="NATSBridge"] - Name of the sender
|
* @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_name=""] - Name of the receiver (empty means broadcast)
|
||||||
@@ -433,8 +504,8 @@ function buildPayload(dataname, payloadType, payloadBytes, transport, data) {
|
|||||||
* @param {string} [options.reply_to_msg_id=""] - Message ID this message is replying 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 {boolean} [options.is_publish=true] - Whether to automatically publish the message
|
||||||
* @param {NATSClient|NATS.Connection} [options.nats_connection=null] - Pre-existing NATS connection
|
* @param {NATSClient|NATS.Connection} [options.nats_connection=null] - Pre-existing NATS connection
|
||||||
* @param {string} [options.msg_id=uuidv4()] - Message ID
|
* @param {string} [options.msg_id=crypto.randomUUID()] - Message ID
|
||||||
* @param {string} [options.sender_id=uuidv4()] - Sender ID
|
* @param {string} [options.sender_id=crypto.randomUUID()] - Sender ID
|
||||||
* @returns {Promise<[Object, string]>} Tuple of [env, env_json_str]
|
* @returns {Promise<[Object, string]>} Tuple of [env, env_json_str]
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
@@ -450,7 +521,7 @@ function buildPayload(dataname, payloadType, payloadBytes, transport, data) {
|
|||||||
* "/test",
|
* "/test",
|
||||||
* [
|
* [
|
||||||
* ["dataname1", data1, "dictionary"],
|
* ["dataname1", data1, "dictionary"],
|
||||||
* ["dataname2", data2, "table"]
|
* ["dataname2", data2, "arrowtable"]
|
||||||
* ],
|
* ],
|
||||||
* { broker_url: "nats://localhost:4222" }
|
* { broker_url: "nats://localhost:4222" }
|
||||||
* );
|
* );
|
||||||
@@ -483,19 +554,35 @@ async function smartsend(subject, data, options = {}) {
|
|||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
logTrace(correlation_id, `Starting smartsend for subject: ${subject}`);
|
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
|
// Process payloads
|
||||||
const payloads = [];
|
const payloads = [];
|
||||||
for (const [dataname, payloadData, payloadType] of data) {
|
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 payloadBytes = await serializeData(payloadData, payloadType);
|
||||||
const payloadSize = payloadBytes.byteLength;
|
const payloadSize = payloadBytes.byteLength;
|
||||||
|
|
||||||
logTrace(correlation_id, `Serialized payload '${dataname}' (type: ${payloadType}) size: ${payloadSize} bytes`);
|
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) {
|
if (payloadSize < size_threshold) {
|
||||||
// Direct path
|
// Direct path
|
||||||
const payloadB64 = bufferToBase64(payloadBytes);
|
const payloadB64 = bufferToBase64(payloadBytes);
|
||||||
logTrace(correlation_id, `Using direct transport for ${payloadSize} bytes`);
|
logTrace(correlation_id, `Using direct transport for ${payloadSize} bytes, base64 length=${payloadB64.length}`);
|
||||||
|
|
||||||
const payload = buildPayload(dataname, payloadType, payloadBytes, 'direct', payloadB64);
|
const payload = buildPayload(dataname, payloadType, payloadBytes, 'direct', payloadB64);
|
||||||
payloads.push(payload);
|
payloads.push(payload);
|
||||||
@@ -579,32 +666,68 @@ async function smartreceive(msg, options = {}) {
|
|||||||
max_delay = 5000
|
max_delay = 5000
|
||||||
} = options;
|
} = 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
|
// Parse the JSON envelope
|
||||||
const payload = typeof msg.payload === 'string' ? msg.payload : Buffer.from(msg.payload).toString('utf8');
|
// NATS.js v2.x uses msg.data instead of msg.payload
|
||||||
const envJsonObj = JSON.parse(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, 'Processing received message');
|
||||||
|
logTrace(envJsonObj.correlation_id, `smartreceive: envelope has ${envJsonObj.payloads.length} payloads`);
|
||||||
|
|
||||||
// Process all payloads in the envelope
|
// Process all payloads in the envelope
|
||||||
const payloadsList = [];
|
const payloadsList = [];
|
||||||
const numPayloads = envJsonObj.payloads.length;
|
const numPayloads = envJsonObj.payloads.length;
|
||||||
|
|
||||||
|
logTrace(envJsonObj.correlation_id, `smartreceive: Processing ${numPayloads} payloads`);
|
||||||
|
|
||||||
for (let i = 0; i < numPayloads; i++) {
|
for (let i = 0; i < numPayloads; i++) {
|
||||||
const payloadObj = envJsonObj.payloads[i];
|
const payloadObj = envJsonObj.payloads[i];
|
||||||
const transport = payloadObj.transport;
|
const transport = payloadObj.transport;
|
||||||
const dataname = payloadObj.dataname;
|
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') {
|
if (transport === 'direct') {
|
||||||
logTrace(envJsonObj.correlation_id, `Direct transport - decoding payload '${dataname}'`);
|
logTrace(envJsonObj.correlation_id, `Direct transport - decoding payload '${dataname}'`);
|
||||||
|
|
||||||
// Extract base64 payload from the payload
|
// Extract base64 payload from the payload
|
||||||
const payloadB64 = payloadObj.data;
|
const payloadB64 = payloadObj.data;
|
||||||
|
logTrace(envJsonObj.correlation_id, `Direct transport: base64 length=${payloadB64?.length}`);
|
||||||
|
|
||||||
// Decode Base64 payload
|
// Decode Base64 payload
|
||||||
const payloadBytes = Buffer.from(payloadB64, 'base64');
|
const payloadBytes = Buffer.from(payloadB64, 'base64');
|
||||||
|
logTrace(envJsonObj.correlation_id, `Direct transport: decoded bytes=${payloadBytes.length}`);
|
||||||
|
|
||||||
// Deserialize based on type
|
// Deserialize based on type
|
||||||
const dataType = payloadObj.payload_type;
|
const dataType = payloadObj.payload_type;
|
||||||
const data = await deserializeData(payloadBytes, dataType, envJsonObj.correlation_id);
|
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]);
|
payloadsList.push([dataname, data, dataType]);
|
||||||
} else if (transport === 'link') {
|
} else if (transport === 'link') {
|
||||||
@@ -631,6 +754,7 @@ async function smartreceive(msg, options = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logTrace(envJsonObj.correlation_id, `smartreceive: Successfully processed all ${payloadsList.length} payloads`);
|
||||||
envJsonObj.payloads = payloadsList;
|
envJsonObj.payloads = payloadsList;
|
||||||
return envJsonObj;
|
return envJsonObj;
|
||||||
}
|
}
|
||||||
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 |
@@ -1,215 +0,0 @@
|
|||||||
/**
|
|
||||||
* JavaScript Binary Receiver Test
|
|
||||||
* Tests the smartreceive function with binary/image/audio/video payloads
|
|
||||||
*/
|
|
||||||
|
|
||||||
const NATSBridge = require('../src/natbridge.js');
|
|
||||||
|
|
||||||
const TEST_BROKER_URL = process.env.NATS_URL || 'nats://localhost:4222';
|
|
||||||
const TEST_FILESERVER_URL = process.env.FILESERVER_URL || 'http://localhost:8080';
|
|
||||||
|
|
||||||
async function runTest() {
|
|
||||||
console.log('=== JavaScript Binary Receiver Test ===\n');
|
|
||||||
|
|
||||||
// Create mock NATS message with binary payloads
|
|
||||||
const binaryData = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); // PNG header
|
|
||||||
const audioData = Buffer.from([0x46, 0x4C, 0x41, 0x43, 0x00, 0x00, 0x00, 0x00]); // FLAC header
|
|
||||||
const videoData = Buffer.from([0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70]); // MP4 header
|
|
||||||
const genericBinary = Buffer.from([0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE]);
|
|
||||||
|
|
||||||
const testData = {
|
|
||||||
correlation_id: 'js-binary-receiver-' + Date.now(),
|
|
||||||
msg_id: 'msg-' + Date.now(),
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
send_to: '/test/binary',
|
|
||||||
msg_purpose: 'test',
|
|
||||||
sender_name: 'js-binary-test',
|
|
||||||
sender_id: 'sender-' + Date.now(),
|
|
||||||
receiver_name: 'js-receiver',
|
|
||||||
receiver_id: 'receiver-' + Date.now(),
|
|
||||||
reply_to: '',
|
|
||||||
reply_to_msg_id: '',
|
|
||||||
broker_url: TEST_BROKER_URL,
|
|
||||||
metadata: {},
|
|
||||||
payloads: [
|
|
||||||
{
|
|
||||||
id: 'payload-1',
|
|
||||||
dataname: 'image',
|
|
||||||
payload_type: 'image',
|
|
||||||
transport: 'direct',
|
|
||||||
encoding: 'base64',
|
|
||||||
size: binaryData.length,
|
|
||||||
data: binaryData.toString('base64'),
|
|
||||||
metadata: { payload_bytes: binaryData.length }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'payload-2',
|
|
||||||
dataname: 'audio',
|
|
||||||
payload_type: 'audio',
|
|
||||||
transport: 'direct',
|
|
||||||
encoding: 'base64',
|
|
||||||
size: audioData.length,
|
|
||||||
data: audioData.toString('base64'),
|
|
||||||
metadata: { payload_bytes: audioData.length }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'payload-3',
|
|
||||||
dataname: 'video',
|
|
||||||
payload_type: 'video',
|
|
||||||
transport: 'direct',
|
|
||||||
encoding: 'base64',
|
|
||||||
size: videoData.length,
|
|
||||||
data: videoData.toString('base64'),
|
|
||||||
metadata: { payload_bytes: videoData.length }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'payload-4',
|
|
||||||
dataname: 'binary',
|
|
||||||
payload_type: 'binary',
|
|
||||||
transport: 'direct',
|
|
||||||
encoding: 'base64',
|
|
||||||
size: genericBinary.length,
|
|
||||||
data: genericBinary.toString('base64'),
|
|
||||||
metadata: { payload_bytes: genericBinary.length }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockMsg = {
|
|
||||||
payload: JSON.stringify(testData)
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('Mock Message Created:');
|
|
||||||
console.log(` Correlation ID: ${testData.correlation_id}`);
|
|
||||||
console.log(` Payloads: ${testData.payloads.length}`);
|
|
||||||
console.log(` Payload types: ${testData.payloads.map(p => p.payload_type).join(', ')}\n`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Receive and process the message
|
|
||||||
console.log('Receiving and processing message...');
|
|
||||||
const env = await NATSBridge.smartreceive(
|
|
||||||
mockMsg,
|
|
||||||
{
|
|
||||||
max_retries: 3,
|
|
||||||
base_delay: 100,
|
|
||||||
max_delay: 1000
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('\n=== Received Envelope ===');
|
|
||||||
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(`Payloads: ${env.payloads.length}\n`);
|
|
||||||
|
|
||||||
// Validate received data
|
|
||||||
console.log('=== Validation ===');
|
|
||||||
let passed = true;
|
|
||||||
|
|
||||||
if (!env.correlation_id) {
|
|
||||||
console.log('❌ correlation_id is missing');
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ correlation_id present');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (env.payloads.length !== 4) {
|
|
||||||
console.log(`❌ Expected 4 payloads, got ${env.payloads.length}`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ Correct number of payloads');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expected data
|
|
||||||
const expectedData = [
|
|
||||||
['image', binaryData, 'image'],
|
|
||||||
['audio', audioData, 'audio'],
|
|
||||||
['video', videoData, 'video'],
|
|
||||||
['binary', genericBinary, 'binary']
|
|
||||||
];
|
|
||||||
|
|
||||||
for (let i = 0; i < env.payloads.length; i++) {
|
|
||||||
const payload = env.payloads[i];
|
|
||||||
const expected = expectedData[i];
|
|
||||||
|
|
||||||
if (payload[0] !== expected[0]) {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Expected dataname '${expected[0]}', got '${payload[0]}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log(`✅ Payload ${i + 1}: Correct dataname`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload[2] !== expected[2]) {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Expected type '${expected[2]}', got '${payload[2]}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log(`✅ Payload ${i + 1}: Correct type`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify binary data integrity
|
|
||||||
const receivedData = payload[1];
|
|
||||||
if (!(receivedData instanceof Buffer || receivedData instanceof Uint8Array)) {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Expected Buffer/Uint8Array, got ${typeof receivedData}`);
|
|
||||||
passed = false;
|
|
||||||
} else if (Buffer.isBuffer(receivedData)) {
|
|
||||||
if (receivedData.length !== expected[1].length) {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Length mismatch`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
let dataMatch = true;
|
|
||||||
for (let j = 0; j < expected[1].length; j++) {
|
|
||||||
if (receivedData[j] !== expected[1][j]) {
|
|
||||||
dataMatch = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (dataMatch) {
|
|
||||||
console.log(`✅ Payload ${i + 1}: Data correctly deserialized`);
|
|
||||||
} else {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Data mismatch`);
|
|
||||||
passed = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Uint8Array comparison
|
|
||||||
const receivedBuffer = Buffer.from(receivedData);
|
|
||||||
if (receivedBuffer.length !== expected[1].length) {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Length mismatch`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
let dataMatch = true;
|
|
||||||
for (let j = 0; j < expected[1].length; j++) {
|
|
||||||
if (receivedBuffer[j] !== expected[1][j]) {
|
|
||||||
dataMatch = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (dataMatch) {
|
|
||||||
console.log(`✅ Payload ${i + 1}: Data correctly deserialized`);
|
|
||||||
} else {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Data mismatch`);
|
|
||||||
passed = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final result
|
|
||||||
console.log('\n=== Test Result ===');
|
|
||||||
if (passed) {
|
|
||||||
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();
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
/**
|
|
||||||
* JavaScript Binary Sender Test
|
|
||||||
* Tests the smartsend function with binary/image/audio/video payloads
|
|
||||||
*/
|
|
||||||
|
|
||||||
const NATSBridge = require('../src/natbridge.js');
|
|
||||||
|
|
||||||
const TEST_SUBJECT = '/test/binary';
|
|
||||||
const TEST_BROKER_URL = process.env.NATS_URL || 'nats://localhost:4222';
|
|
||||||
const TEST_FILESERVER_URL = process.env.FILESERVER_URL || 'http://localhost:8080';
|
|
||||||
|
|
||||||
async function runTest() {
|
|
||||||
console.log('=== JavaScript Binary Sender Test ===\n');
|
|
||||||
|
|
||||||
const correlationId = NATSBridge.uuidv4();
|
|
||||||
console.log(`Correlation ID: ${correlationId}`);
|
|
||||||
console.log(`Subject: ${TEST_SUBJECT}`);
|
|
||||||
console.log(`Broker URL: ${TEST_BROKER_URL}\n`);
|
|
||||||
|
|
||||||
// Test data - binary data for different types
|
|
||||||
const binaryData = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); // PNG header
|
|
||||||
const audioData = Buffer.from([0x46, 0x4C, 0x41, 0x43, 0x00, 0x00, 0x00, 0x00]); // FLAC header
|
|
||||||
const videoData = Buffer.from([0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70]); // MP4 header
|
|
||||||
const genericBinary = Buffer.from([0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE]);
|
|
||||||
|
|
||||||
const testData = [
|
|
||||||
['image', binaryData, 'image'],
|
|
||||||
['audio', audioData, 'audio'],
|
|
||||||
['video', videoData, 'video'],
|
|
||||||
['binary', genericBinary, 'binary']
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Send the message
|
|
||||||
console.log('Sending binary payloads...');
|
|
||||||
const [env, envJsonStr] = await NATSBridge.smartsend(
|
|
||||||
TEST_SUBJECT,
|
|
||||||
testData,
|
|
||||||
{
|
|
||||||
broker_url: TEST_BROKER_URL,
|
|
||||||
fileserver_url: TEST_FILESERVER_URL,
|
|
||||||
correlation_id: correlationId,
|
|
||||||
msg_purpose: 'test',
|
|
||||||
sender_name: 'js-binary-test',
|
|
||||||
is_publish: false
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
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`);
|
|
||||||
|
|
||||||
// Validate envelope structure
|
|
||||||
console.log('=== Validation ===');
|
|
||||||
let passed = true;
|
|
||||||
|
|
||||||
if (env.payloads.length !== 4) {
|
|
||||||
console.log(`❌ Expected 4 payloads, got ${env.payloads.length}`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ Correct number of payloads');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test each payload
|
|
||||||
const expectedDatanames = ['image', 'audio', 'video', 'binary'];
|
|
||||||
const expectedTypes = ['image', 'audio', 'video', 'binary'];
|
|
||||||
const expectedData = [binaryData, audioData, videoData, genericBinary];
|
|
||||||
|
|
||||||
for (let i = 0; i < env.payloads.length; i++) {
|
|
||||||
const payload = env.payloads[i];
|
|
||||||
|
|
||||||
if (payload.dataname !== expectedDatanames[i]) {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Expected dataname '${expectedDatanames[i]}', got '${payload.dataname}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log(`✅ Payload ${i + 1}: Correct dataname`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.payload_type !== expectedTypes[i]) {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Expected type '${expectedTypes[i]}', got '${payload.payload_type}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log(`✅ Payload ${i + 1}: Correct type`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.transport !== 'direct') {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Expected transport 'direct', got '${payload.transport}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log(`✅ Payload ${i + 1}: Correct transport`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.encoding !== 'base64') {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Expected encoding 'base64', got '${payload.encoding}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log(`✅ Payload ${i + 1}: Correct encoding`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode and verify the data
|
|
||||||
const decodedData = Buffer.from(payload.data, 'base64');
|
|
||||||
const originalData = expectedData[i];
|
|
||||||
|
|
||||||
if (decodedData.length !== originalData.length) {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Length mismatch (${decodedData.length} vs ${originalData.length})`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
let dataMatch = true;
|
|
||||||
for (let j = 0; j < originalData.length; j++) {
|
|
||||||
if (decodedData[j] !== originalData[j]) {
|
|
||||||
dataMatch = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (dataMatch) {
|
|
||||||
console.log(`✅ Payload ${i + 1}: Data integrity verified`);
|
|
||||||
} else {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Data integrity mismatch`);
|
|
||||||
passed = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(` Size: ${payload.size} bytes\n`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with larger binary data (simulating file upload scenario)
|
|
||||||
console.log('=== Large Binary Data Test ===');
|
|
||||||
const largeData = Buffer.alloc(10000, 0xFF); // 10KB of binary data
|
|
||||||
const largeTestData = [
|
|
||||||
['large_binary', largeData, 'binary']
|
|
||||||
];
|
|
||||||
|
|
||||||
const [largeEnv, _] = await NATSBridge.smartsend(
|
|
||||||
TEST_SUBJECT,
|
|
||||||
largeTestData,
|
|
||||||
{
|
|
||||||
broker_url: TEST_BROKER_URL,
|
|
||||||
fileserver_url: TEST_FILESERVER_URL,
|
|
||||||
correlation_id: 'large-' + correlationId,
|
|
||||||
is_publish: false
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (largeEnv.payloads.length === 1 && largeEnv.payloads[0].size === 10000) {
|
|
||||||
console.log('✅ Large binary data handled correctly');
|
|
||||||
} else {
|
|
||||||
console.log('❌ Large binary data handling failed');
|
|
||||||
passed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final result
|
|
||||||
console.log('\n=== Test Result ===');
|
|
||||||
if (passed) {
|
|
||||||
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();
|
|
||||||
@@ -1,220 +0,0 @@
|
|||||||
/**
|
|
||||||
* JavaScript Dictionary Receiver Test
|
|
||||||
* Tests the smartreceive function with dictionary payloads
|
|
||||||
*/
|
|
||||||
|
|
||||||
const NATSBridge = require('../src/natbridge.js');
|
|
||||||
|
|
||||||
const TEST_BROKER_URL = process.env.NATS_URL || 'nats://localhost:4222';
|
|
||||||
const TEST_FILESERVER_URL = process.env.FILESERVER_URL || 'http://localhost:8080';
|
|
||||||
|
|
||||||
async function runTest() {
|
|
||||||
console.log('=== JavaScript Dictionary Receiver Test ===\n');
|
|
||||||
|
|
||||||
// Create a mock NATS message with dictionary payloads
|
|
||||||
const simpleDict = { key1: 'value1', key2: 'value2' };
|
|
||||||
const nestedDict = { outer: { inner: 'value', number: 42 } };
|
|
||||||
const arrayDict = { items: [1, 2, 3, 'four', 'five'] };
|
|
||||||
const mixedDict = { string: 'text', number: 123, boolean: true, null_val: null };
|
|
||||||
|
|
||||||
const testData = {
|
|
||||||
correlation_id: 'test-receiver-dict-' + Date.now(),
|
|
||||||
msg_id: 'msg-' + Date.now(),
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
send_to: '/test/dictionary',
|
|
||||||
msg_purpose: 'test',
|
|
||||||
sender_name: 'js-dict-test',
|
|
||||||
sender_id: 'sender-' + Date.now(),
|
|
||||||
receiver_name: 'js-receiver',
|
|
||||||
receiver_id: 'receiver-' + Date.now(),
|
|
||||||
reply_to: '',
|
|
||||||
reply_to_msg_id: '',
|
|
||||||
broker_url: TEST_BROKER_URL,
|
|
||||||
metadata: {},
|
|
||||||
payloads: [
|
|
||||||
{
|
|
||||||
id: 'payload-1',
|
|
||||||
dataname: 'simple_dict',
|
|
||||||
payload_type: 'dictionary',
|
|
||||||
transport: 'direct',
|
|
||||||
encoding: 'base64',
|
|
||||||
size: Buffer.from(JSON.stringify(simpleDict)).length,
|
|
||||||
data: Buffer.from(JSON.stringify(simpleDict)).toString('base64'),
|
|
||||||
metadata: { payload_bytes: Buffer.from(JSON.stringify(simpleDict)).length }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'payload-2',
|
|
||||||
dataname: 'nested_dict',
|
|
||||||
payload_type: 'dictionary',
|
|
||||||
transport: 'direct',
|
|
||||||
encoding: 'base64',
|
|
||||||
size: Buffer.from(JSON.stringify(nestedDict)).length,
|
|
||||||
data: Buffer.from(JSON.stringify(nestedDict)).toString('base64'),
|
|
||||||
metadata: { payload_bytes: Buffer.from(JSON.stringify(nestedDict)).length }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'payload-3',
|
|
||||||
dataname: 'array_dict',
|
|
||||||
payload_type: 'dictionary',
|
|
||||||
transport: 'direct',
|
|
||||||
encoding: 'base64',
|
|
||||||
size: Buffer.from(JSON.stringify(arrayDict)).length,
|
|
||||||
data: Buffer.from(JSON.stringify(arrayDict)).toString('base64'),
|
|
||||||
metadata: { payload_bytes: Buffer.from(JSON.stringify(arrayDict)).length }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'payload-4',
|
|
||||||
dataname: 'mixed_dict',
|
|
||||||
payload_type: 'dictionary',
|
|
||||||
transport: 'direct',
|
|
||||||
encoding: 'base64',
|
|
||||||
size: Buffer.from(JSON.stringify(mixedDict)).length,
|
|
||||||
data: Buffer.from(JSON.stringify(mixedDict)).toString('base64'),
|
|
||||||
metadata: { payload_bytes: Buffer.from(JSON.stringify(mixedDict)).length }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockMsg = {
|
|
||||||
payload: JSON.stringify(testData)
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('Mock Message Created:');
|
|
||||||
console.log(` Correlation ID: ${testData.correlation_id}`);
|
|
||||||
console.log(` Payloads: ${testData.payloads.length}`);
|
|
||||||
console.log(` Payload types: ${testData.payloads.map(p => p.payload_type).join(', ')}\n`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Receive and process the message
|
|
||||||
console.log('Receiving and processing message...');
|
|
||||||
const env = await NATSBridge.smartreceive(
|
|
||||||
mockMsg,
|
|
||||||
{
|
|
||||||
max_retries: 3,
|
|
||||||
base_delay: 100,
|
|
||||||
max_delay: 1000
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('\n=== Received Envelope ===');
|
|
||||||
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(`Payloads: ${env.payloads.length}\n`);
|
|
||||||
|
|
||||||
// Validate received data
|
|
||||||
console.log('=== Validation ===');
|
|
||||||
let passed = true;
|
|
||||||
|
|
||||||
if (!env.correlation_id) {
|
|
||||||
console.log('❌ correlation_id is missing');
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ correlation_id present');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (env.payloads.length !== 4) {
|
|
||||||
console.log(`❌ Expected 4 payloads, got ${env.payloads.length}`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ Correct number of payloads');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expected data
|
|
||||||
const expectedData = [
|
|
||||||
['simple_dict', simpleDict, 'dictionary'],
|
|
||||||
['nested_dict', nestedDict, 'dictionary'],
|
|
||||||
['array_dict', arrayDict, 'dictionary'],
|
|
||||||
['mixed_dict', mixedDict, 'dictionary']
|
|
||||||
];
|
|
||||||
|
|
||||||
for (let i = 0; i < env.payloads.length; i++) {
|
|
||||||
const payload = env.payloads[i];
|
|
||||||
const expected = expectedData[i];
|
|
||||||
|
|
||||||
if (payload[0] !== expected[0]) {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Expected dataname '${expected[0]}', got '${payload[0]}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log(`✅ Payload ${i + 1}: Correct dataname`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload[2] !== expected[2]) {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Expected type '${expected[2]}', got '${payload[2]}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log(`✅ Payload ${i + 1}: Correct type`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const dataMatch = JSON.stringify(payload[1]) === JSON.stringify(expected[1]);
|
|
||||||
if (!dataMatch) {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Data mismatch`);
|
|
||||||
console.log(` Expected: ${JSON.stringify(expected[1])}`);
|
|
||||||
console.log(` Got: ${JSON.stringify(payload[1])}`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log(`✅ Payload ${i + 1}: Data correctly deserialized`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test round-trip with receive
|
|
||||||
console.log('\n=== Round-trip Test ===');
|
|
||||||
const roundTripData = {
|
|
||||||
correlation_id: 'roundtrip-' + Date.now(),
|
|
||||||
msg_id: 'msg-' + Date.now(),
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
send_to: '/test/dictionary',
|
|
||||||
msg_purpose: 'test',
|
|
||||||
sender_name: 'js-test',
|
|
||||||
sender_id: 'sender-' + Date.now(),
|
|
||||||
receiver_name: 'js-receiver',
|
|
||||||
receiver_id: 'receiver-' + Date.now(),
|
|
||||||
reply_to: '',
|
|
||||||
reply_to_msg_id: '',
|
|
||||||
broker_url: TEST_BROKER_URL,
|
|
||||||
metadata: {},
|
|
||||||
payloads: [
|
|
||||||
{
|
|
||||||
id: 'payload-rt',
|
|
||||||
dataname: 'roundtrip',
|
|
||||||
payload_type: 'dictionary',
|
|
||||||
transport: 'direct',
|
|
||||||
encoding: 'base64',
|
|
||||||
size: Buffer.from(JSON.stringify({ test: 'data', nested: { a: 1, b: 2 } })).length,
|
|
||||||
data: Buffer.from(JSON.stringify({ test: 'data', nested: { a: 1, b: 2 } })).toString('base64'),
|
|
||||||
metadata: { payload_bytes: Buffer.from(JSON.stringify({ test: 'data', nested: { a: 1, b: 2 } })).length }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockRtMsg = { payload: JSON.stringify(roundTripData) };
|
|
||||||
const rtEnv = await NATSBridge.smartreceive(mockRtMsg);
|
|
||||||
|
|
||||||
if (rtEnv.payloads.length === 1 &&
|
|
||||||
rtEnv.payloads[0][0] === 'roundtrip' &&
|
|
||||||
rtEnv.payloads[0][2] === 'dictionary') {
|
|
||||||
console.log('✅ Round-trip test successful');
|
|
||||||
} else {
|
|
||||||
console.log('❌ Round-trip test failed');
|
|
||||||
passed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final result
|
|
||||||
console.log('\n=== Test Result ===');
|
|
||||||
if (passed) {
|
|
||||||
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();
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
/**
|
|
||||||
* JavaScript Dictionary Sender Test
|
|
||||||
* Tests the smartsend function with dictionary payloads
|
|
||||||
*/
|
|
||||||
|
|
||||||
const NATSBridge = require('../src/natbridge.js');
|
|
||||||
|
|
||||||
const TEST_SUBJECT = '/test/dictionary';
|
|
||||||
const TEST_BROKER_URL = process.env.NATS_URL || 'nats://localhost:4222';
|
|
||||||
const TEST_FILESERVER_URL = process.env.FILESERVER_URL || 'http://localhost:8080';
|
|
||||||
|
|
||||||
async function runTest() {
|
|
||||||
console.log('=== JavaScript Dictionary Sender Test ===\n');
|
|
||||||
|
|
||||||
const correlationId = NATSBridge.uuidv4();
|
|
||||||
console.log(`Correlation ID: ${correlationId}`);
|
|
||||||
console.log(`Subject: ${TEST_SUBJECT}`);
|
|
||||||
console.log(`Broker URL: ${TEST_BROKER_URL}\n`);
|
|
||||||
|
|
||||||
// Test data - various dictionary structures
|
|
||||||
const testData = [
|
|
||||||
['simple_dict', { key1: 'value1', key2: 'value2' }, 'dictionary'],
|
|
||||||
['nested_dict', { outer: { inner: 'value', number: 42 } }, 'dictionary'],
|
|
||||||
['array_dict', { items: [1, 2, 3, 'four', 'five'] }, 'dictionary'],
|
|
||||||
['mixed_dict', { string: 'text', number: 123, boolean: true, null_val: null }, 'dictionary']
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Send the message
|
|
||||||
console.log('Sending dictionary payloads...');
|
|
||||||
const [env, envJsonStr] = await NATSBridge.smartsend(
|
|
||||||
TEST_SUBJECT,
|
|
||||||
testData,
|
|
||||||
{
|
|
||||||
broker_url: TEST_BROKER_URL,
|
|
||||||
fileserver_url: TEST_FILESERVER_URL,
|
|
||||||
correlation_id: correlationId,
|
|
||||||
msg_purpose: 'test',
|
|
||||||
sender_name: 'js-dict-test',
|
|
||||||
is_publish: false
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
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`);
|
|
||||||
|
|
||||||
// Validate envelope structure
|
|
||||||
console.log('=== Validation ===');
|
|
||||||
let passed = true;
|
|
||||||
|
|
||||||
if (env.payloads.length !== 4) {
|
|
||||||
console.log(`❌ Expected 4 payloads, got ${env.payloads.length}`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ Correct number of payloads');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test each payload
|
|
||||||
const expectedDatanames = ['simple_dict', 'nested_dict', 'array_dict', 'mixed_dict'];
|
|
||||||
const expectedTypes = ['dictionary', 'dictionary', 'dictionary', 'dictionary'];
|
|
||||||
|
|
||||||
for (let i = 0; i < env.payloads.length; i++) {
|
|
||||||
const payload = env.payloads[i];
|
|
||||||
|
|
||||||
if (payload.dataname !== expectedDatanames[i]) {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Expected dataname '${expectedDatanames[i]}', got '${payload.dataname}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log(`✅ Payload ${i + 1}: Correct dataname`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.payload_type !== expectedTypes[i]) {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Expected type '${expectedTypes[i]}', got '${payload.payload_type}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log(`✅ Payload ${i + 1}: Correct type`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.transport !== 'direct') {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Expected transport 'direct', got '${payload.transport}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log(`✅ Payload ${i + 1}: Correct transport`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.encoding !== 'base64') {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Expected encoding 'base64', got '${payload.encoding}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log(`✅ Payload ${i + 1}: Correct encoding`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode and verify the data
|
|
||||||
const decodedData = JSON.parse(Buffer.from(payload.data, 'base64').toString('utf8'));
|
|
||||||
const originalData = testData[i][1];
|
|
||||||
|
|
||||||
const originalJson = JSON.stringify(originalData);
|
|
||||||
const decodedJson = JSON.stringify(decodedData);
|
|
||||||
|
|
||||||
if (originalJson !== decodedJson) {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Data integrity mismatch`);
|
|
||||||
console.log(` Expected: ${originalJson}`);
|
|
||||||
console.log(` Got: ${decodedJson}`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log(`✅ Payload ${i + 1}: Data integrity verified`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(` Size: ${payload.size} bytes\n`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test round-trip serialization
|
|
||||||
console.log('=== Round-trip Serialization Test ===');
|
|
||||||
const roundTripTestData = [
|
|
||||||
['roundtrip', { test: 'data', numbers: [1, 2, 3], nested: { a: 1, b: 2 } }, 'dictionary']
|
|
||||||
];
|
|
||||||
|
|
||||||
const [rtEnv, rtEnvJsonStr] = await NATSBridge.smartsend(
|
|
||||||
TEST_SUBJECT,
|
|
||||||
roundTripTestData,
|
|
||||||
{
|
|
||||||
broker_url: TEST_BROKER_URL,
|
|
||||||
fileserver_url: TEST_FILESERVER_URL,
|
|
||||||
correlation_id: 'roundtrip-' + correlationId,
|
|
||||||
is_publish: false
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const rtPayload = rtEnv.payloads[0];
|
|
||||||
const rtDecoded = JSON.parse(Buffer.from(rtPayload.data, 'base64').toString('utf8'));
|
|
||||||
|
|
||||||
if (JSON.stringify(rtDecoded) === JSON.stringify(roundTripTestData[0][1])) {
|
|
||||||
console.log('✅ Round-trip serialization successful');
|
|
||||||
} else {
|
|
||||||
console.log('❌ Round-trip serialization failed');
|
|
||||||
passed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test JSON string output
|
|
||||||
console.log('\n=== JSON String Output Test ===');
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(envJsonStr);
|
|
||||||
if (parsed.correlation_id === env.correlation_id &&
|
|
||||||
parsed.payloads.length === env.payloads.length) {
|
|
||||||
console.log('✅ JSON string is valid and matches envelope');
|
|
||||||
} else {
|
|
||||||
console.log('❌ JSON string does not match envelope');
|
|
||||||
passed = false;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log('❌ JSON string is invalid:', e.message);
|
|
||||||
passed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final result
|
|
||||||
console.log('\n=== Test Result ===');
|
|
||||||
if (passed) {
|
|
||||||
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();
|
|
||||||
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();
|
||||||
@@ -1,54 +1,190 @@
|
|||||||
/**
|
/**
|
||||||
* JavaScript Mix Payloads Sender Test
|
* JavaScript Mix Payloads Sender Test
|
||||||
* Tests the smartsend function with mixed payload types
|
* 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/natbridge.js');
|
const NATSBridge = require('../src/natsbridge.js');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
const TEST_SUBJECT = '/test/mix';
|
const TEST_SUBJECT = '/natsbridge';
|
||||||
const TEST_BROKER_URL = process.env.NATS_URL || 'nats://localhost:4222';
|
const TEST_BROKER_URL = process.env.NATS_URL || 'nats.yiem.cc';
|
||||||
const TEST_FILESERVER_URL = process.env.FILESERVER_URL || 'http://localhost:8080';
|
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() {
|
async function runTest() {
|
||||||
console.log('=== JavaScript Mix Payloads Sender Test ===\n');
|
console.log('=== JavaScript Mix Payloads Sender Test ===\n');
|
||||||
|
|
||||||
const correlationId = NATSBridge.uuidv4();
|
const correlationId = crypto.randomUUID();
|
||||||
console.log(`Correlation ID: ${correlationId}`);
|
console.log(`Correlation ID: ${correlationId}`);
|
||||||
console.log(`Subject: ${TEST_SUBJECT}`);
|
console.log(`Subject: ${TEST_SUBJECT}`);
|
||||||
console.log(`Broker URL: ${TEST_BROKER_URL}\n`);
|
console.log(`Broker URL: ${TEST_BROKER_URL}`);
|
||||||
|
console.log(`Fileserver URL: ${TEST_FILESERVER_URL}`);
|
||||||
|
console.log(`Size Threshold: ${SIZE_THRESHOLD} bytes (1MB)\n`);
|
||||||
|
|
||||||
// Test data - mixed payload types
|
// Helper: Log with correlation ID
|
||||||
const textData = 'Hello, NATSBridge!';
|
function logTrace(message) {
|
||||||
const dictData = { key1: 'value1', key2: 42, nested: { a: 1, b: 2 } };
|
const timestamp = new Date().toISOString();
|
||||||
const binaryData = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); // PNG header
|
console.log(`[${timestamp}] [Correlation: ${correlationId}] ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Table data
|
// Create sample data for each type (mirroring Julia test)
|
||||||
const tableData = [
|
const textData = 'Hello! This is a test chat message. 🎉\nHow are you doing today? 😊';
|
||||||
{ id: 1, name: 'Alice', age: 30 },
|
|
||||||
{ id: 2, name: 'Bob', age: 25 },
|
const dictData = {
|
||||||
{ id: 3, name: 'Charlie', age: 35 }
|
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 }
|
||||||
];
|
];
|
||||||
|
|
||||||
const testData = [
|
// Json table data (small - direct transport)
|
||||||
['message', textData, 'text'],
|
const jsonTableSmall = [
|
||||||
['config', dictData, 'dictionary'],
|
{ id: 1, name: 'Alice', score: 95, active: true },
|
||||||
['image', binaryData, 'image'],
|
{ id: 2, name: 'Bob', score: 88, active: false },
|
||||||
['users', tableData, 'table']
|
{ 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 {
|
try {
|
||||||
// Send the message
|
// Send the message
|
||||||
console.log('Sending mixed payloads...');
|
console.log('Sending mixed payloads...\n');
|
||||||
const [env, envJsonStr] = await NATSBridge.smartsend(
|
const [env, envJsonStr] = await NATSBridge.smartsend(
|
||||||
TEST_SUBJECT,
|
TEST_SUBJECT,
|
||||||
testData,
|
payloads,
|
||||||
{
|
{
|
||||||
broker_url: TEST_BROKER_URL,
|
broker_url: TEST_BROKER_URL,
|
||||||
fileserver_url: TEST_FILESERVER_URL,
|
fileserver_url: TEST_FILESERVER_URL,
|
||||||
|
fileserver_upload_handler: NATSBridge.plikOneshotUpload,
|
||||||
|
size_threshold: SIZE_THRESHOLD,
|
||||||
correlation_id: correlationId,
|
correlation_id: correlationId,
|
||||||
msg_purpose: 'test',
|
msg_purpose: 'chat',
|
||||||
sender_name: 'js-mix-test',
|
sender_name: 'js-mix-test',
|
||||||
is_publish: false
|
receiver_name: '',
|
||||||
|
receiver_id: '',
|
||||||
|
reply_to: '',
|
||||||
|
reply_to_msg_id: '',
|
||||||
|
is_publish: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -61,144 +197,11 @@ async function runTest() {
|
|||||||
console.log(`Sender: ${env.sender_name}`);
|
console.log(`Sender: ${env.sender_name}`);
|
||||||
console.log(`Payloads: ${env.payloads.length}\n`);
|
console.log(`Payloads: ${env.payloads.length}\n`);
|
||||||
|
|
||||||
// Validate envelope structure
|
|
||||||
console.log('=== Validation ===');
|
|
||||||
let passed = true;
|
|
||||||
|
|
||||||
if (env.payloads.length !== 4) {
|
|
||||||
console.log(`❌ Expected 4 payloads, got ${env.payloads.length}`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ Correct number of payloads');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test each payload
|
|
||||||
const expectedDatanames = ['message', 'config', 'image', 'users'];
|
|
||||||
const expectedTypes = ['text', 'dictionary', 'image', 'table'];
|
|
||||||
const expectedData = [textData, dictData, binaryData, tableData];
|
|
||||||
|
|
||||||
for (let i = 0; i < env.payloads.length; i++) {
|
|
||||||
const payload = env.payloads[i];
|
|
||||||
|
|
||||||
if (payload.dataname !== expectedDatanames[i]) {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Expected dataname '${expectedDatanames[i]}', got '${payload.dataname}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log(`✅ Payload ${i + 1}: Correct dataname`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.payload_type !== expectedTypes[i]) {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Expected type '${expectedTypes[i]}', got '${payload.payload_type}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log(`✅ Payload ${i + 1}: Correct type`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.transport !== 'direct') {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Expected transport 'direct', got '${payload.transport}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log(`✅ Payload ${i + 1}: Correct transport`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.encoding !== 'base64') {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Expected encoding 'base64', got '${payload.encoding}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log(`✅ Payload ${i + 1}: Correct encoding`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify data integrity based on type
|
|
||||||
if (expectedTypes[i] === 'text') {
|
|
||||||
const decodedData = Buffer.from(payload.data, 'base64').toString('utf8');
|
|
||||||
if (decodedData !== expectedData[i]) {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Data integrity mismatch`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log(`✅ Payload ${i + 1}: Data integrity verified`);
|
|
||||||
}
|
|
||||||
} else if (expectedTypes[i] === 'dictionary') {
|
|
||||||
const decodedData = JSON.parse(Buffer.from(payload.data, 'base64').toString('utf8'));
|
|
||||||
if (JSON.stringify(decodedData) !== JSON.stringify(expectedData[i])) {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Data integrity mismatch`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log(`✅ Payload ${i + 1}: Data integrity verified`);
|
|
||||||
}
|
|
||||||
} else if (expectedTypes[i] === 'image') {
|
|
||||||
const decodedData = Buffer.from(payload.data, 'base64');
|
|
||||||
if (decodedData.length !== expectedData[i].length) {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Length mismatch`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
let dataMatch = true;
|
|
||||||
for (let j = 0; j < expectedData[i].length; j++) {
|
|
||||||
if (decodedData[j] !== expectedData[i][j]) {
|
|
||||||
dataMatch = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (dataMatch) {
|
|
||||||
console.log(`✅ Payload ${i + 1}: Data integrity verified`);
|
|
||||||
} else {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Data integrity mismatch`);
|
|
||||||
passed = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (expectedTypes[i] === 'table') {
|
|
||||||
const decodedData = Buffer.from(payload.data, 'base64');
|
|
||||||
if (decodedData.length > 0) {
|
|
||||||
console.log(`✅ Payload ${i + 1}: Arrow IPC data present (${decodedData.length} bytes)`);
|
|
||||||
} else {
|
|
||||||
console.log(`❌ Payload ${i + 1}: Arrow IPC data is empty`);
|
|
||||||
passed = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(` Size: ${payload.size} bytes\n`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with chat-like payload (text + image + audio)
|
|
||||||
console.log('=== Chat-like Payload Test ===');
|
|
||||||
const chatData = [
|
|
||||||
['text', 'Hello!', 'text'],
|
|
||||||
['image', Buffer.from([0xFF, 0xD8, 0xFF, 0xE0]), 'image'],
|
|
||||||
['audio', Buffer.from([0x46, 0x4C, 0x41, 0x43]), 'audio']
|
|
||||||
];
|
|
||||||
|
|
||||||
const [chatEnv, _] = await NATSBridge.smartsend(
|
|
||||||
TEST_SUBJECT,
|
|
||||||
chatData,
|
|
||||||
{
|
|
||||||
broker_url: TEST_BROKER_URL,
|
|
||||||
fileserver_url: TEST_FILESERVER_URL,
|
|
||||||
correlation_id: 'chat-' + correlationId,
|
|
||||||
is_publish: false
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (chatEnv.payloads.length === 3) {
|
|
||||||
console.log('✅ Chat-like payloads handled correctly');
|
|
||||||
} else {
|
|
||||||
console.log('❌ Chat-like payloads handling failed');
|
|
||||||
passed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final result
|
|
||||||
console.log('\n=== Test Result ===');
|
|
||||||
if (passed) {
|
|
||||||
console.log('✅ ALL TESTS PASSED');
|
|
||||||
process.exit(0);
|
|
||||||
} else {
|
|
||||||
console.log('❌ SOME TESTS FAILED');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Test failed with error:', error.message);
|
console.error('\n❌ Test failed with error:', error.message);
|
||||||
console.error(error.stack);
|
console.error(error.stack);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
runTest();
|
runTest();
|
||||||
|
|||||||
@@ -1,172 +0,0 @@
|
|||||||
/**
|
|
||||||
* JavaScript Table Receiver Test
|
|
||||||
* Tests the smartreceive function with table (Arrow IPC) payloads
|
|
||||||
*/
|
|
||||||
|
|
||||||
const NATSBridge = require('../src/natbridge.js');
|
|
||||||
|
|
||||||
const TEST_BROKER_URL = process.env.NATS_URL || 'nats://localhost:4222';
|
|
||||||
const TEST_FILESERVER_URL = process.env.FILESERVER_URL || 'http://localhost:8080';
|
|
||||||
|
|
||||||
async function runTest() {
|
|
||||||
console.log('=== JavaScript Table Receiver Test ===\n');
|
|
||||||
|
|
||||||
// Create a mock NATS message with table payload
|
|
||||||
const tableData = [
|
|
||||||
{ id: 1, name: 'Alice', age: 30, active: true },
|
|
||||||
{ id: 2, name: 'Bob', age: 25, active: false },
|
|
||||||
{ id: 3, name: 'Charlie', age: 35, active: true }
|
|
||||||
];
|
|
||||||
|
|
||||||
// Convert to Arrow IPC format
|
|
||||||
const arrow = require('apache-arrow');
|
|
||||||
const fields = [
|
|
||||||
new arrow.Field('id', arrow.Int64, true),
|
|
||||||
new arrow.Field('name', arrow.Utf8, true),
|
|
||||||
new arrow.Field('age', arrow.Int64, true),
|
|
||||||
new arrow.Field('active', arrow.Boolean, true)
|
|
||||||
];
|
|
||||||
const schema = new arrow.Schema(fields);
|
|
||||||
const batches = [];
|
|
||||||
for (const row of tableData) {
|
|
||||||
const batch = arrow.recordBatch.fromObjects([row], schema);
|
|
||||||
batches.push(batch);
|
|
||||||
}
|
|
||||||
const buffers = arrow.ipc.recordBatchesToMessage(batches, schema).buffers;
|
|
||||||
const combined = new Uint8Array(buffers.reduce((acc, b) => acc + b.byteLength, 0));
|
|
||||||
let offset = 0;
|
|
||||||
for (const buf of buffers) {
|
|
||||||
combined.set(new Uint8Array(buf), offset);
|
|
||||||
offset += buf.byteLength;
|
|
||||||
}
|
|
||||||
const arrowBuffer = Buffer.from(combined);
|
|
||||||
|
|
||||||
const testData = {
|
|
||||||
correlation_id: 'js-table-receiver-' + Date.now(),
|
|
||||||
msg_id: 'msg-' + Date.now(),
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
send_to: '/test/table',
|
|
||||||
msg_purpose: 'test',
|
|
||||||
sender_name: 'js-table-test',
|
|
||||||
sender_id: 'sender-' + Date.now(),
|
|
||||||
receiver_name: 'js-receiver',
|
|
||||||
receiver_id: 'receiver-' + Date.now(),
|
|
||||||
reply_to: '',
|
|
||||||
reply_to_msg_id: '',
|
|
||||||
broker_url: TEST_BROKER_URL,
|
|
||||||
metadata: {},
|
|
||||||
payloads: [
|
|
||||||
{
|
|
||||||
id: 'payload-1',
|
|
||||||
dataname: 'users_table',
|
|
||||||
payload_type: 'table',
|
|
||||||
transport: 'direct',
|
|
||||||
encoding: 'base64',
|
|
||||||
size: arrowBuffer.length,
|
|
||||||
data: arrowBuffer.toString('base64'),
|
|
||||||
metadata: { payload_bytes: arrowBuffer.length }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockMsg = {
|
|
||||||
payload: JSON.stringify(testData)
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('Mock Message Created:');
|
|
||||||
console.log(` Correlation ID: ${testData.correlation_id}`);
|
|
||||||
console.log(` Payloads: ${testData.payloads.length}`);
|
|
||||||
console.log(` Payload type: ${testData.payloads[0].payload_type}`);
|
|
||||||
console.log(` Transport: ${testData.payloads[0].transport}\n`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Receive and process the message
|
|
||||||
console.log('Receiving and processing message...');
|
|
||||||
const env = await NATSBridge.smartreceive(
|
|
||||||
mockMsg,
|
|
||||||
{
|
|
||||||
max_retries: 3,
|
|
||||||
base_delay: 100,
|
|
||||||
max_delay: 1000
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('\n=== Received Envelope ===');
|
|
||||||
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(`Payloads: ${env.payloads.length}\n`);
|
|
||||||
|
|
||||||
// Validate received data
|
|
||||||
console.log('=== Validation ===');
|
|
||||||
let passed = true;
|
|
||||||
|
|
||||||
if (!env.correlation_id) {
|
|
||||||
console.log('❌ correlation_id is missing');
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ correlation_id present');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (env.payloads.length !== 1) {
|
|
||||||
console.log(`❌ Expected 1 payload, got ${env.payloads.length}`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ Correct number of payloads');
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = env.payloads[0];
|
|
||||||
if (payload[0] !== 'users_table') {
|
|
||||||
console.log(`❌ Expected dataname 'users_table', got '${payload[0]}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ Correct dataname');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload[2] !== 'table') {
|
|
||||||
console.log(`❌ Expected type 'table', got '${payload[2]}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ Correct type');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify table data is a Buffer (Arrow IPC format)
|
|
||||||
if (payload[1] instanceof Buffer || payload[1] instanceof Uint8Array) {
|
|
||||||
console.log('✅ Table data is Arrow IPC buffer');
|
|
||||||
console.log(` Buffer size: ${payload[1].length} bytes`);
|
|
||||||
} else {
|
|
||||||
console.log(`❌ Expected Buffer/Uint8Array, got ${typeof payload[1]}`);
|
|
||||||
passed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test round-trip with Arrow deserialization
|
|
||||||
console.log('\n=== Arrow Deserialization Test ===');
|
|
||||||
try {
|
|
||||||
const table = arrow.tableFromRawBytes(payload[1]);
|
|
||||||
console.log(`✅ Arrow table deserialized successfully`);
|
|
||||||
console.log(` Schema: ${table.schema.fields.map(f => f.name).join(', ')}`);
|
|
||||||
console.log(` Num rows: ${table.numRows}`);
|
|
||||||
} catch (e) {
|
|
||||||
console.log('❌ Arrow deserialization failed:', e.message);
|
|
||||||
passed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final result
|
|
||||||
console.log('\n=== Test Result ===');
|
|
||||||
if (passed) {
|
|
||||||
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();
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
/**
|
|
||||||
* JavaScript Table Sender Test
|
|
||||||
* Tests the smartsend function with table (Arrow IPC) payloads
|
|
||||||
*/
|
|
||||||
|
|
||||||
const NATSBridge = require('../src/natbridge.js');
|
|
||||||
|
|
||||||
const TEST_SUBJECT = '/test/table';
|
|
||||||
const TEST_BROKER_URL = process.env.NATS_URL || 'nats://localhost:4222';
|
|
||||||
const TEST_FILESERVER_URL = process.env.FILESERVER_URL || 'http://localhost:8080';
|
|
||||||
|
|
||||||
async function runTest() {
|
|
||||||
console.log('=== JavaScript Table Sender Test ===\n');
|
|
||||||
|
|
||||||
const correlationId = NATSBridge.uuidv4();
|
|
||||||
console.log(`Correlation ID: ${correlationId}`);
|
|
||||||
console.log(`Subject: ${TEST_SUBJECT}`);
|
|
||||||
console.log(`Broker URL: ${TEST_BROKER_URL}\n`);
|
|
||||||
|
|
||||||
// Test data - table data as array of objects
|
|
||||||
const tableData = [
|
|
||||||
{ id: 1, name: 'Alice', age: 30, active: true },
|
|
||||||
{ id: 2, name: 'Bob', age: 25, active: false },
|
|
||||||
{ id: 3, name: 'Charlie', age: 35, active: true },
|
|
||||||
{ id: 4, name: 'Diana', age: 28, active: true },
|
|
||||||
{ id: 5, name: 'Eve', age: 32, active: false }
|
|
||||||
];
|
|
||||||
|
|
||||||
const testData = [
|
|
||||||
['users_table', tableData, 'table']
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Send the message
|
|
||||||
console.log('Sending table payload...');
|
|
||||||
const [env, envJsonStr] = await NATSBridge.smartsend(
|
|
||||||
TEST_SUBJECT,
|
|
||||||
testData,
|
|
||||||
{
|
|
||||||
broker_url: TEST_BROKER_URL,
|
|
||||||
fileserver_url: TEST_FILESERVER_URL,
|
|
||||||
correlation_id: correlationId,
|
|
||||||
msg_purpose: 'test',
|
|
||||||
sender_name: 'js-table-test',
|
|
||||||
is_publish: false
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
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`);
|
|
||||||
|
|
||||||
// Validate envelope structure
|
|
||||||
console.log('=== Validation ===');
|
|
||||||
let passed = true;
|
|
||||||
|
|
||||||
if (env.payloads.length !== 1) {
|
|
||||||
console.log(`❌ Expected 1 payload, got ${env.payloads.length}`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ Correct number of payloads');
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = env.payloads[0];
|
|
||||||
if (payload.dataname !== 'users_table') {
|
|
||||||
console.log(`❌ Expected dataname 'users_table', got '${payload.dataname}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ Correct dataname');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.payload_type !== 'table') {
|
|
||||||
console.log(`❌ Expected payload_type 'table', got '${payload.payload_type}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ Correct payload_type');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.transport !== 'direct') {
|
|
||||||
console.log(`❌ Expected transport 'direct', got '${payload.transport}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ Correct transport');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.encoding !== 'base64') {
|
|
||||||
console.log(`❌ Expected encoding 'base64', got '${payload.encoding}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ Correct encoding');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify Arrow IPC data can be decoded
|
|
||||||
console.log('\n=== Arrow IPC Verification ===');
|
|
||||||
const decodedData = Buffer.from(payload.data, 'base64');
|
|
||||||
console.log(`Arrow IPC buffer size: ${decodedData.length} bytes`);
|
|
||||||
|
|
||||||
if (decodedData.length > 0) {
|
|
||||||
console.log('✅ Arrow IPC data present');
|
|
||||||
} else {
|
|
||||||
console.log('❌ Arrow IPC data is empty');
|
|
||||||
passed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with larger table
|
|
||||||
console.log('\n=== Larger Table Test ===');
|
|
||||||
const largeTableData = [];
|
|
||||||
for (let i = 1; i <= 100; i++) {
|
|
||||||
largeTableData.push({
|
|
||||||
id: i,
|
|
||||||
name: `User${i}`,
|
|
||||||
age: Math.floor(Math.random() * 100),
|
|
||||||
active: Math.random() > 0.5,
|
|
||||||
score: Math.random() * 100
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const largeTestData = [
|
|
||||||
['large_table', largeTableData, 'table']
|
|
||||||
];
|
|
||||||
|
|
||||||
const [largeEnv, _] = await NATSBridge.smartsend(
|
|
||||||
TEST_SUBJECT,
|
|
||||||
largeTestData,
|
|
||||||
{
|
|
||||||
broker_url: TEST_BROKER_URL,
|
|
||||||
fileserver_url: TEST_FILESERVER_URL,
|
|
||||||
correlation_id: 'large-' + correlationId,
|
|
||||||
is_publish: false
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (largeEnv.payloads.length === 1) {
|
|
||||||
console.log('✅ Large table handled correctly');
|
|
||||||
console.log(` Size: ${largeEnv.payloads[0].size} bytes`);
|
|
||||||
} else {
|
|
||||||
console.log('❌ Large table handling failed');
|
|
||||||
passed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test JSON string output
|
|
||||||
console.log('\n=== JSON String Output Test ===');
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(envJsonStr);
|
|
||||||
if (parsed.correlation_id === env.correlation_id &&
|
|
||||||
parsed.payloads.length === env.payloads.length) {
|
|
||||||
console.log('✅ JSON string is valid and matches envelope');
|
|
||||||
} else {
|
|
||||||
console.log('❌ JSON string does not match envelope');
|
|
||||||
passed = false;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log('❌ JSON string is invalid:', e.message);
|
|
||||||
passed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final result
|
|
||||||
console.log('\n=== Test Result ===');
|
|
||||||
if (passed) {
|
|
||||||
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();
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
/**
|
|
||||||
* JavaScript Text Receiver Test
|
|
||||||
* Tests the smartreceive function with text payloads
|
|
||||||
*/
|
|
||||||
|
|
||||||
const NATSBridge = require('../src/natbridge.js');
|
|
||||||
|
|
||||||
const TEST_BROKER_URL = process.env.NATS_URL || 'nats://localhost:4222';
|
|
||||||
const TEST_FILESERVER_URL = process.env.FILESERVER_URL || 'http://localhost:8080';
|
|
||||||
|
|
||||||
async function runTest() {
|
|
||||||
console.log('=== JavaScript Text Receiver Test ===\n');
|
|
||||||
|
|
||||||
// Create a mock NATS message with text payload
|
|
||||||
const testData = {
|
|
||||||
correlation_id: 'test-receiver-' + Date.now(),
|
|
||||||
msg_id: 'msg-' + Date.now(),
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
send_to: '/test/text',
|
|
||||||
msg_purpose: 'test',
|
|
||||||
sender_name: 'js-text-test',
|
|
||||||
sender_id: 'sender-' + Date.now(),
|
|
||||||
receiver_name: 'js-receiver',
|
|
||||||
receiver_id: 'receiver-' + Date.now(),
|
|
||||||
reply_to: '',
|
|
||||||
reply_to_msg_id: '',
|
|
||||||
broker_url: TEST_BROKER_URL,
|
|
||||||
metadata: {},
|
|
||||||
payloads: [
|
|
||||||
{
|
|
||||||
id: 'payload-' + Date.now(),
|
|
||||||
dataname: 'message',
|
|
||||||
payload_type: 'text',
|
|
||||||
transport: 'direct',
|
|
||||||
encoding: 'base64',
|
|
||||||
size: 38,
|
|
||||||
data: Buffer.from('Hello, NATSBridge! This is a test message.').toString('base64'),
|
|
||||||
metadata: { payload_bytes: 38 }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockMsg = {
|
|
||||||
payload: JSON.stringify(testData)
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('Mock Message Created:');
|
|
||||||
console.log(` Correlation ID: ${testData.correlation_id}`);
|
|
||||||
console.log(` Payloads: ${testData.payloads.length}`);
|
|
||||||
console.log(` Payload dataname: ${testData.payloads[0].dataname}`);
|
|
||||||
console.log(` Payload type: ${testData.payloads[0].payload_type}`);
|
|
||||||
console.log(` Transport: ${testData.payloads[0].transport}\n`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Receive and process the message
|
|
||||||
console.log('Receiving and processing message...');
|
|
||||||
const env = await NATSBridge.smartreceive(
|
|
||||||
mockMsg,
|
|
||||||
{
|
|
||||||
max_retries: 3,
|
|
||||||
base_delay: 100,
|
|
||||||
max_delay: 1000
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('\n=== Received Envelope ===');
|
|
||||||
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(`Payloads: ${env.payloads.length}\n`);
|
|
||||||
|
|
||||||
// Validate received data
|
|
||||||
console.log('=== Validation ===');
|
|
||||||
let passed = true;
|
|
||||||
|
|
||||||
if (!env.correlation_id) {
|
|
||||||
console.log('❌ correlation_id is missing');
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ correlation_id present');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (env.payloads.length !== 1) {
|
|
||||||
console.log(`❌ Expected 1 payload, got ${env.payloads.length}`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ Correct number of payloads');
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = env.payloads[0];
|
|
||||||
if (payload[0] !== 'message') {
|
|
||||||
console.log(`❌ Expected dataname 'message', got '${payload[0]}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ Correct dataname');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload[2] !== 'text') {
|
|
||||||
console.log(`❌ Expected type 'text', got '${payload[2]}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ Correct type');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload[1] !== 'Hello, NATSBridge! This is a test message.') {
|
|
||||||
console.log(`❌ Data mismatch`);
|
|
||||||
console.log(` Expected: Hello, NATSBridge! This is a test message.`);
|
|
||||||
console.log(` Got: ${payload[1]}`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ Data correctly deserialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test with multiple text payloads
|
|
||||||
console.log('\n=== Multiple Text Payloads Test ===');
|
|
||||||
const multiTestData = {
|
|
||||||
correlation_id: 'multi-receiver-' + Date.now(),
|
|
||||||
msg_id: 'msg-' + Date.now(),
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
send_to: '/test/text',
|
|
||||||
msg_purpose: 'test',
|
|
||||||
sender_name: 'js-text-test',
|
|
||||||
sender_id: 'sender-' + Date.now(),
|
|
||||||
receiver_name: 'js-receiver',
|
|
||||||
receiver_id: 'receiver-' + Date.now(),
|
|
||||||
reply_to: '',
|
|
||||||
reply_to_msg_id: '',
|
|
||||||
broker_url: TEST_BROKER_URL,
|
|
||||||
metadata: {},
|
|
||||||
payloads: [
|
|
||||||
{
|
|
||||||
id: 'payload-1',
|
|
||||||
dataname: 'msg1',
|
|
||||||
payload_type: 'text',
|
|
||||||
transport: 'direct',
|
|
||||||
encoding: 'base64',
|
|
||||||
size: 16,
|
|
||||||
data: Buffer.from('First message').toString('base64'),
|
|
||||||
metadata: { payload_bytes: 16 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'payload-2',
|
|
||||||
dataname: 'msg2',
|
|
||||||
payload_type: 'text',
|
|
||||||
transport: 'direct',
|
|
||||||
encoding: 'base64',
|
|
||||||
size: 16,
|
|
||||||
data: Buffer.from('Second message').toString('base64'),
|
|
||||||
metadata: { payload_bytes: 16 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'payload-3',
|
|
||||||
dataname: 'msg3',
|
|
||||||
payload_type: 'text',
|
|
||||||
transport: 'direct',
|
|
||||||
encoding: 'base64',
|
|
||||||
size: 16,
|
|
||||||
data: Buffer.from('Third message').toString('base64'),
|
|
||||||
metadata: { payload_bytes: 16 }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockMultiMsg = {
|
|
||||||
payload: JSON.stringify(multiTestData)
|
|
||||||
};
|
|
||||||
|
|
||||||
const multiEnv = await NATSBridge.smartreceive(mockMultiMsg);
|
|
||||||
|
|
||||||
if (multiEnv.payloads.length === 3) {
|
|
||||||
console.log('✅ Multiple payloads handled correctly');
|
|
||||||
|
|
||||||
// Verify each payload
|
|
||||||
const expectedMessages = ['First message', 'Second message', 'Third message'];
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
if (multiEnv.payloads[i][1] === expectedMessages[i]) {
|
|
||||||
console.log(`✅ Payload ${i + 1} correctly deserialized`);
|
|
||||||
} else {
|
|
||||||
console.log(`❌ Payload ${i + 1} mismatch`);
|
|
||||||
passed = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`❌ Expected 3 payloads, got ${multiEnv.payloads.length}`);
|
|
||||||
passed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final result
|
|
||||||
console.log('\n=== Test Result ===');
|
|
||||||
if (passed) {
|
|
||||||
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();
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
/**
|
|
||||||
* JavaScript Text Sender Test
|
|
||||||
* Tests the smartsend function with text payloads
|
|
||||||
*/
|
|
||||||
|
|
||||||
const NATSBridge = require('../src/natbridge.js');
|
|
||||||
|
|
||||||
const TEST_SUBJECT = '/test/text';
|
|
||||||
const TEST_BROKER_URL = process.env.NATS_URL || 'nats://localhost:4222';
|
|
||||||
const TEST_FILESERVER_URL = process.env.FILESERVER_URL || 'http://localhost:8080';
|
|
||||||
|
|
||||||
async function runTest() {
|
|
||||||
console.log('=== JavaScript Text Sender Test ===\n');
|
|
||||||
|
|
||||||
const correlationId = NATSBridge.uuidv4();
|
|
||||||
console.log(`Correlation ID: ${correlationId}`);
|
|
||||||
console.log(`Subject: ${TEST_SUBJECT}`);
|
|
||||||
console.log(`Broker URL: ${TEST_BROKER_URL}\n`);
|
|
||||||
|
|
||||||
// Test data
|
|
||||||
const textData = 'Hello, NATSBridge! This is a test message.';
|
|
||||||
const testData = [
|
|
||||||
['message', textData, 'text']
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Send the message
|
|
||||||
console.log('Sending text payload...');
|
|
||||||
const [env, envJsonStr] = await NATSBridge.smartsend(
|
|
||||||
TEST_SUBJECT,
|
|
||||||
testData,
|
|
||||||
{
|
|
||||||
broker_url: TEST_BROKER_URL,
|
|
||||||
fileserver_url: TEST_FILESERVER_URL,
|
|
||||||
correlation_id: correlationId,
|
|
||||||
msg_purpose: 'test',
|
|
||||||
sender_name: 'js-text-test',
|
|
||||||
is_publish: false // Don't actually publish for this test
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
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`);
|
|
||||||
|
|
||||||
// Validate envelope structure
|
|
||||||
console.log('=== Validation ===');
|
|
||||||
let passed = true;
|
|
||||||
|
|
||||||
if (!env.correlation_id) {
|
|
||||||
console.log('❌ correlation_id is missing');
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ correlation_id present');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!env.msg_id) {
|
|
||||||
console.log('❌ msg_id is missing');
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ msg_id present');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!env.timestamp) {
|
|
||||||
console.log('❌ timestamp is missing');
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ timestamp present');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (env.payloads.length !== 1) {
|
|
||||||
console.log(`❌ Expected 1 payload, got ${env.payloads.length}`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ Correct number of payloads');
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = env.payloads[0];
|
|
||||||
if (payload.dataname !== 'message') {
|
|
||||||
console.log(`❌ Expected dataname 'message', got '${payload.dataname}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ Correct dataname');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.payload_type !== 'text') {
|
|
||||||
console.log(`❌ Expected payload_type 'text', got '${payload.payload_type}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ Correct payload_type');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.transport !== 'direct') {
|
|
||||||
console.log(`❌ Expected transport 'direct', got '${payload.transport}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ Correct transport');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload.encoding !== 'base64') {
|
|
||||||
console.log(`❌ Expected encoding 'base64', got '${payload.encoding}'`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ Correct encoding');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode and verify the data
|
|
||||||
const decodedData = Buffer.from(payload.data, 'base64').toString('utf8');
|
|
||||||
if (decodedData !== textData) {
|
|
||||||
console.log(`❌ Decoded data mismatch`);
|
|
||||||
console.log(` Expected: ${textData}`);
|
|
||||||
console.log(` Got: ${decodedData}`);
|
|
||||||
passed = false;
|
|
||||||
} else {
|
|
||||||
console.log('✅ Data integrity verified');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\nPayload size: ${payload.size} bytes`);
|
|
||||||
console.log(`Base64 data length: ${payload.data.length} chars`);
|
|
||||||
|
|
||||||
// Test with multiple text payloads
|
|
||||||
console.log('\n=== Multiple Text Payloads Test ===');
|
|
||||||
const multiTestData = [
|
|
||||||
['msg1', 'First message', 'text'],
|
|
||||||
['msg2', 'Second message', 'text'],
|
|
||||||
['msg3', 'Third message', 'text']
|
|
||||||
];
|
|
||||||
|
|
||||||
const [multiEnv, multiEnvJsonStr] = await NATSBridge.smartsend(
|
|
||||||
TEST_SUBJECT,
|
|
||||||
multiTestData,
|
|
||||||
{
|
|
||||||
broker_url: TEST_BROKER_URL,
|
|
||||||
fileserver_url: TEST_FILESERVER_URL,
|
|
||||||
correlation_id: 'multi-test-' + correlationId,
|
|
||||||
is_publish: false
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (multiEnv.payloads.length === 3) {
|
|
||||||
console.log('✅ Multiple payloads handled correctly');
|
|
||||||
} else {
|
|
||||||
console.log(`❌ Expected 3 payloads, got ${multiEnv.payloads.length}`);
|
|
||||||
passed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final result
|
|
||||||
console.log('\n=== Test Result ===');
|
|
||||||
if (passed) {
|
|
||||||
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();
|
|
||||||
@@ -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,137 +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, env_json_str = NATSBridge.smartsend(
|
|
||||||
SUBJECT,
|
|
||||||
[data1, data2]; # List of (dataname, data, type) tuples
|
|
||||||
broker_url = NATS_URL,
|
|
||||||
fileserver_url = FILESERVER_URL,
|
|
||||||
fileserver_upload_handler = 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 = "",
|
|
||||||
is_publish = true # Publish the message to NATS
|
|
||||||
)
|
|
||||||
|
|
||||||
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.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,123 +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, env_json_str = NATSBridge.smartsend(
|
|
||||||
SUBJECT,
|
|
||||||
[data1, data2]; # List of (dataname, data, type) tuples
|
|
||||||
broker_url = NATS_URL;
|
|
||||||
fileserver_url = FILESERVER_URL,
|
|
||||||
fileserver_upload_handler = 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 = "",
|
|
||||||
is_publish = true # Publish the message to NATS
|
|
||||||
)
|
|
||||||
|
|
||||||
log_trace("Sent message with transport: $(env.payloads[1].transport)")
|
|
||||||
log_trace("Envelope type: $(env.payloads[1].payload_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.")
|
|
||||||
@@ -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,19 +169,35 @@ 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"),
|
||||||
# Large data (link transport) - large table, large image, large audio, large video, large binary
|
(filename_small_image, file_data_small_image, "binary"),
|
||||||
("chat_table_large", table_data_large, "table"),
|
|
||||||
("user_image_large", large_image_data, "image"),
|
# Large data (link transport) - large arrowtable, large jsontable, large image, large audio, large video, large binary
|
||||||
|
# ("arrow_table_large", arrow_table_large, "arrowtable"),
|
||||||
|
("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")
|
||||||
@@ -237,4 +255,4 @@ println("start smartsend for mixed content")
|
|||||||
test_mix_send()
|
test_mix_send()
|
||||||
|
|
||||||
println("\nTest completed.")
|
println("\nTest completed.")
|
||||||
println("Note: Run test_julia_to_julia_mix_receiver.jl to receive the messages.")
|
println("Note: Run test_julia_to_julia_mix_receiver.jl to receive the messages.")
|
||||||
|
|||||||
@@ -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,135 +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, env_json_str = NATSBridge.smartsend(
|
|
||||||
SUBJECT,
|
|
||||||
[data1, data2]; # List of (dataname, data, type) tuples
|
|
||||||
broker_url = NATS_URL,
|
|
||||||
fileserver_url = FILESERVER_URL,
|
|
||||||
fileserver_upload_handler = 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 = "",
|
|
||||||
is_publish = true # Publish the message to NATS
|
|
||||||
)
|
|
||||||
|
|
||||||
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.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,120 +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, env_json_str = NATSBridge.smartsend(
|
|
||||||
SUBJECT,
|
|
||||||
[data1, data2]; # List of (dataname, data, type) tuples
|
|
||||||
broker_url = NATS_URL,
|
|
||||||
fileserver_url = FILESERVER_URL,
|
|
||||||
fileserver_upload_handler = 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 = "",
|
|
||||||
is_publish = true # Publish the message to NATS
|
|
||||||
)
|
|
||||||
|
|
||||||
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.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,185 +0,0 @@
|
|||||||
"""
|
|
||||||
MicroPython Binary Receiver Test
|
|
||||||
Tests the smartreceive function with binary/image/audio/video payloads
|
|
||||||
|
|
||||||
Note: This test is designed for both MicroPython and desktop Python
|
|
||||||
for compatibility testing.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import base64
|
|
||||||
|
|
||||||
# Add parent directory to path
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
from natbridge_mpy import smartreceive, DEFAULT_BROKER_URL, DEFAULT_FILESERVER_URL
|
|
||||||
|
|
||||||
TEST_BROKER_URL = os.environ.get('NATS_URL', 'nats://localhost:4222')
|
|
||||||
TEST_FILESERVER_URL = os.environ.get('FILESERVER_URL', 'http://localhost:8080')
|
|
||||||
|
|
||||||
|
|
||||||
def run_test():
|
|
||||||
print('=== MicroPython Binary Receiver Test ===\n')
|
|
||||||
|
|
||||||
from natbridge_mpy import _generate_uuid
|
|
||||||
|
|
||||||
# Create mock NATS message with binary payloads
|
|
||||||
image_data = bytes([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) # PNG header
|
|
||||||
audio_data = bytes([0x46, 0x4C, 0x41, 0x43, 0x00, 0x00, 0x00, 0x00]) # FLAC header
|
|
||||||
video_data = bytes([0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70]) # MP4 header
|
|
||||||
generic_binary = bytes([0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE])
|
|
||||||
|
|
||||||
test_data = {
|
|
||||||
'correlation_id': 'mpy-binary-receiver-' + _generate_uuid(),
|
|
||||||
'msg_id': _generate_uuid(),
|
|
||||||
'timestamp': '2024-01-15T10:30:00Z',
|
|
||||||
'send_to': '/test/binary',
|
|
||||||
'msg_purpose': 'test',
|
|
||||||
'sender_name': 'mpy-binary-test',
|
|
||||||
'sender_id': _generate_uuid(),
|
|
||||||
'receiver_name': 'mpy-receiver',
|
|
||||||
'receiver_id': _generate_uuid(),
|
|
||||||
'reply_to': '',
|
|
||||||
'reply_to_msg_id': '',
|
|
||||||
'broker_url': TEST_BROKER_URL,
|
|
||||||
'metadata': {},
|
|
||||||
'payloads': [
|
|
||||||
{
|
|
||||||
'id': _generate_uuid(),
|
|
||||||
'dataname': 'image',
|
|
||||||
'payload_type': 'image',
|
|
||||||
'transport': 'direct',
|
|
||||||
'encoding': 'base64',
|
|
||||||
'size': len(image_data),
|
|
||||||
'data': base64.b64encode(image_data).decode('ascii'),
|
|
||||||
'metadata': {'payload_bytes': len(image_data)}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': _generate_uuid(),
|
|
||||||
'dataname': 'audio',
|
|
||||||
'payload_type': 'audio',
|
|
||||||
'transport': 'direct',
|
|
||||||
'encoding': 'base64',
|
|
||||||
'size': len(audio_data),
|
|
||||||
'data': base64.b64encode(audio_data).decode('ascii'),
|
|
||||||
'metadata': {'payload_bytes': len(audio_data)}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': _generate_uuid(),
|
|
||||||
'dataname': 'video',
|
|
||||||
'payload_type': 'video',
|
|
||||||
'transport': 'direct',
|
|
||||||
'encoding': 'base64',
|
|
||||||
'size': len(video_data),
|
|
||||||
'data': base64.b64encode(video_data).decode('ascii'),
|
|
||||||
'metadata': {'payload_bytes': len(video_data)}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': _generate_uuid(),
|
|
||||||
'dataname': 'binary',
|
|
||||||
'payload_type': 'binary',
|
|
||||||
'transport': 'direct',
|
|
||||||
'encoding': 'base64',
|
|
||||||
'size': len(generic_binary),
|
|
||||||
'data': base64.b64encode(generic_binary).decode('ascii'),
|
|
||||||
'metadata': {'payload_bytes': len(generic_binary)}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
mock_msg = {
|
|
||||||
'payload': json.dumps(test_data)
|
|
||||||
}
|
|
||||||
|
|
||||||
print('Mock Message Created:')
|
|
||||||
print(f' Correlation ID: {test_data["correlation_id"]}')
|
|
||||||
print(f' Payloads: {len(test_data["payloads"])}')
|
|
||||||
print(f' Payload types: {", ".join(p["payload_type"] for p in test_data["payloads"])}\n')
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Receive and process the message
|
|
||||||
print('Receiving and processing message...')
|
|
||||||
env = smartreceive(
|
|
||||||
mock_msg,
|
|
||||||
max_retries=3,
|
|
||||||
base_delay=100,
|
|
||||||
max_delay=1000
|
|
||||||
)
|
|
||||||
|
|
||||||
print('\n=== Received Envelope ===')
|
|
||||||
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'Payloads: {len(env["payloads"])}\n')
|
|
||||||
|
|
||||||
# Validate received data
|
|
||||||
print('=== Validation ===')
|
|
||||||
passed = True
|
|
||||||
|
|
||||||
if not env.get('correlation_id'):
|
|
||||||
print('❌ correlation_id is missing')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ correlation_id present')
|
|
||||||
|
|
||||||
if len(env['payloads']) != 4:
|
|
||||||
print(f'❌ Expected 4 payloads, got {len(env["payloads"])}')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct number of payloads')
|
|
||||||
|
|
||||||
# Expected data
|
|
||||||
expected_data = [
|
|
||||||
('image', image_data, 'image'),
|
|
||||||
('audio', audio_data, 'audio'),
|
|
||||||
('video', video_data, 'video'),
|
|
||||||
('binary', generic_binary, 'binary')
|
|
||||||
]
|
|
||||||
|
|
||||||
for i in range(len(env['payloads'])):
|
|
||||||
payload = env['payloads'][i]
|
|
||||||
expected = expected_data[i]
|
|
||||||
|
|
||||||
if payload[0] != expected[0]:
|
|
||||||
print(f"❌ Payload {i + 1}: Expected dataname '{expected[0]}', got '{payload[0]}'")
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print(f'✅ Payload {i + 1}: Correct dataname')
|
|
||||||
|
|
||||||
if payload[2] != expected[2]:
|
|
||||||
print(f"❌ Payload {i + 1}: Expected type '{expected[2]}', got '{payload[2]}'")
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print(f'✅ Payload {i + 1}: Correct type')
|
|
||||||
|
|
||||||
# Verify binary data integrity
|
|
||||||
received_data = payload[1]
|
|
||||||
if received_data != expected[1]:
|
|
||||||
print(f'❌ Payload {i + 1}: Data mismatch')
|
|
||||||
print(f' Expected: {expected[1]}')
|
|
||||||
print(f' Got: {received_data}')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print(f'✅ Payload {i + 1}: Data correctly deserialized')
|
|
||||||
|
|
||||||
# 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__':
|
|
||||||
run_test()
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
"""
|
|
||||||
MicroPython Binary Sender Test
|
|
||||||
Tests the smartsend function with binary/image/audio/video payloads
|
|
||||||
|
|
||||||
Note: This test is designed for both MicroPython and desktop Python
|
|
||||||
for compatibility testing.
|
|
||||||
"""
|
|
||||||
|
|
||||||
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 natbridge_mpy import smartsend, DEFAULT_BROKER_URL, DEFAULT_FILESERVER_URL, DEFAULT_SIZE_THRESHOLD, MAX_PAYLOAD_SIZE
|
|
||||||
|
|
||||||
TEST_SUBJECT = '/test/binary'
|
|
||||||
TEST_BROKER_URL = os.environ.get('NATS_URL', 'nats://localhost:4222')
|
|
||||||
TEST_FILESERVER_URL = os.environ.get('FILESERVER_URL', 'http://localhost:8080')
|
|
||||||
|
|
||||||
|
|
||||||
def run_test():
|
|
||||||
print('=== MicroPython Binary Sender Test ===\n')
|
|
||||||
|
|
||||||
from natbridge_mpy import _generate_uuid
|
|
||||||
correlation_id = 'mpy-binary-test-' + _generate_uuid()
|
|
||||||
print(f'Correlation ID: {correlation_id}')
|
|
||||||
print(f'Subject: {TEST_SUBJECT}')
|
|
||||||
print(f'Broker URL: {TEST_BROKER_URL}')
|
|
||||||
print(f'Default Size Threshold: {DEFAULT_SIZE_THRESHOLD} bytes')
|
|
||||||
print(f'Max Payload Size: {MAX_PAYLOAD_SIZE} bytes\n')
|
|
||||||
|
|
||||||
# Test data - binary data for different types
|
|
||||||
image_data = bytearray([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) # PNG header
|
|
||||||
audio_data = bytearray([0x46, 0x4C, 0x41, 0x43, 0x00, 0x00, 0x00, 0x00]) # FLAC header
|
|
||||||
video_data = bytearray([0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70]) # MP4 header
|
|
||||||
generic_binary = bytearray([0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE])
|
|
||||||
|
|
||||||
test_data = [
|
|
||||||
('image', bytes(image_data), 'image'),
|
|
||||||
('audio', bytes(audio_data), 'audio'),
|
|
||||||
('video', bytes(video_data), 'video'),
|
|
||||||
('binary', bytes(generic_binary), 'binary')
|
|
||||||
]
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Send the message
|
|
||||||
print('Sending binary payloads...')
|
|
||||||
env, env_json_str = smartsend(
|
|
||||||
TEST_SUBJECT,
|
|
||||||
test_data,
|
|
||||||
broker_url=TEST_BROKER_URL,
|
|
||||||
fileserver_url=TEST_FILESERVER_URL,
|
|
||||||
correlation_id=correlation_id,
|
|
||||||
msg_purpose='test',
|
|
||||||
sender_name='mpy-binary-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
|
|
||||||
|
|
||||||
if len(env['payloads']) != 4:
|
|
||||||
print(f'❌ Expected 4 payloads, got {len(env["payloads"])}')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct number of payloads')
|
|
||||||
|
|
||||||
# Test each payload
|
|
||||||
expected_datanames = ['image', 'audio', 'video', 'binary']
|
|
||||||
expected_types = ['image', 'audio', 'video', 'binary']
|
|
||||||
expected_data = [bytes(image_data), bytes(audio_data), bytes(video_data), bytes(generic_binary)]
|
|
||||||
|
|
||||||
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')
|
|
||||||
|
|
||||||
# Decode and verify the data
|
|
||||||
decoded_data = base64.b64decode(payload['data'])
|
|
||||||
original_data = expected_data[i]
|
|
||||||
|
|
||||||
if decoded_data != original_data:
|
|
||||||
print(f'❌ Payload {i + 1}: Data integrity mismatch')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print(f'✅ Payload {i + 1}: Data integrity verified')
|
|
||||||
|
|
||||||
print(f' Size: {payload["size"]} bytes\n')
|
|
||||||
|
|
||||||
# Test with larger binary data
|
|
||||||
print('=== Large Binary Data Test ===')
|
|
||||||
large_data = bytes([0xFF] * 1000) # 1KB of binary data
|
|
||||||
large_test_data = [
|
|
||||||
('large_binary', large_data, 'binary')
|
|
||||||
]
|
|
||||||
|
|
||||||
large_env, _ = smartsend(
|
|
||||||
TEST_SUBJECT,
|
|
||||||
large_test_data,
|
|
||||||
broker_url=TEST_BROKER_URL,
|
|
||||||
fileserver_url=TEST_FILESERVER_URL,
|
|
||||||
correlation_id='large-' + correlation_id,
|
|
||||||
is_publish=False
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(large_env['payloads']) == 1 and large_env['payloads'][0]['size'] == 1000:
|
|
||||||
print('✅ Large binary data handled correctly')
|
|
||||||
else:
|
|
||||||
print('❌ Large binary data 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__':
|
|
||||||
run_test()
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
"""
|
|
||||||
MicroPython Dictionary Receiver Test
|
|
||||||
Tests the smartreceive function with dictionary payloads
|
|
||||||
|
|
||||||
Note: This test is designed for both MicroPython and desktop Python
|
|
||||||
for compatibility testing.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
|
|
||||||
# Add parent directory to path
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
from natbridge_mpy import smartreceive, DEFAULT_BROKER_URL, DEFAULT_FILESERVER_URL
|
|
||||||
|
|
||||||
TEST_BROKER_URL = os.environ.get('NATS_URL', 'nats://localhost:4222')
|
|
||||||
TEST_FILESERVER_URL = os.environ.get('FILESERVER_URL', 'http://localhost:8080')
|
|
||||||
|
|
||||||
|
|
||||||
def run_test():
|
|
||||||
print('=== MicroPython Dictionary Receiver Test ===\n')
|
|
||||||
|
|
||||||
from natbridge_mpy import _generate_uuid
|
|
||||||
|
|
||||||
# Create a mock NATS message with dictionary payloads
|
|
||||||
import base64
|
|
||||||
|
|
||||||
simple_dict = {'key1': 'value1', 'key2': 'value2'}
|
|
||||||
nested_dict = {'outer': {'inner': 'value', 'number': 42}}
|
|
||||||
array_dict = {'items': [1, 2, 3, 'four', 'five']}
|
|
||||||
mixed_dict = {'string': 'text', 'number': 123, 'boolean': True, 'null_val': None}
|
|
||||||
|
|
||||||
test_data = {
|
|
||||||
'correlation_id': 'mpy-receiver-dict-' + _generate_uuid(),
|
|
||||||
'msg_id': _generate_uuid(),
|
|
||||||
'timestamp': '2024-01-15T10:30:00Z',
|
|
||||||
'send_to': '/test/dictionary',
|
|
||||||
'msg_purpose': 'test',
|
|
||||||
'sender_name': 'mpy-dict-test',
|
|
||||||
'sender_id': _generate_uuid(),
|
|
||||||
'receiver_name': 'mpy-receiver',
|
|
||||||
'receiver_id': _generate_uuid(),
|
|
||||||
'reply_to': '',
|
|
||||||
'reply_to_msg_id': '',
|
|
||||||
'broker_url': TEST_BROKER_URL,
|
|
||||||
'metadata': {},
|
|
||||||
'payloads': [
|
|
||||||
{
|
|
||||||
'id': _generate_uuid(),
|
|
||||||
'dataname': 'simple_dict',
|
|
||||||
'payload_type': 'dictionary',
|
|
||||||
'transport': 'direct',
|
|
||||||
'encoding': 'base64',
|
|
||||||
'size': len(json.dumps(simple_dict).encode('utf8')),
|
|
||||||
'data': base64.b64encode(json.dumps(simple_dict).encode('utf8')).decode('ascii'),
|
|
||||||
'metadata': {'payload_bytes': len(json.dumps(simple_dict).encode('utf8'))}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': _generate_uuid(),
|
|
||||||
'dataname': 'nested_dict',
|
|
||||||
'payload_type': 'dictionary',
|
|
||||||
'transport': 'direct',
|
|
||||||
'encoding': 'base64',
|
|
||||||
'size': len(json.dumps(nested_dict).encode('utf8')),
|
|
||||||
'data': base64.b64encode(json.dumps(nested_dict).encode('utf8')).decode('ascii'),
|
|
||||||
'metadata': {'payload_bytes': len(json.dumps(nested_dict).encode('utf8'))}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': _generate_uuid(),
|
|
||||||
'dataname': 'array_dict',
|
|
||||||
'payload_type': 'dictionary',
|
|
||||||
'transport': 'direct',
|
|
||||||
'encoding': 'base64',
|
|
||||||
'size': len(json.dumps(array_dict).encode('utf8')),
|
|
||||||
'data': base64.b64encode(json.dumps(array_dict).encode('utf8')).decode('ascii'),
|
|
||||||
'metadata': {'payload_bytes': len(json.dumps(array_dict).encode('utf8'))}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': _generate_uuid(),
|
|
||||||
'dataname': 'mixed_dict',
|
|
||||||
'payload_type': 'dictionary',
|
|
||||||
'transport': 'direct',
|
|
||||||
'encoding': 'base64',
|
|
||||||
'size': len(json.dumps(mixed_dict).encode('utf8')),
|
|
||||||
'data': base64.b64encode(json.dumps(mixed_dict).encode('utf8')).decode('ascii'),
|
|
||||||
'metadata': {'payload_bytes': len(json.dumps(mixed_dict).encode('utf8'))}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
mock_msg = {
|
|
||||||
'payload': json.dumps(test_data)
|
|
||||||
}
|
|
||||||
|
|
||||||
print('Mock Message Created:')
|
|
||||||
print(f' Correlation ID: {test_data["correlation_id"]}')
|
|
||||||
print(f' Payloads: {len(test_data["payloads"])}')
|
|
||||||
print(f' Payload types: {", ".join(p["payload_type"] for p in test_data["payloads"])}\n')
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Receive and process the message
|
|
||||||
print('Receiving and processing message...')
|
|
||||||
env = smartreceive(
|
|
||||||
mock_msg,
|
|
||||||
max_retries=3,
|
|
||||||
base_delay=100,
|
|
||||||
max_delay=1000
|
|
||||||
)
|
|
||||||
|
|
||||||
print('\n=== Received Envelope ===')
|
|
||||||
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'Payloads: {len(env["payloads"])}\n')
|
|
||||||
|
|
||||||
# Validate received data
|
|
||||||
print('=== Validation ===')
|
|
||||||
passed = True
|
|
||||||
|
|
||||||
if not env.get('correlation_id'):
|
|
||||||
print('❌ correlation_id is missing')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ correlation_id present')
|
|
||||||
|
|
||||||
if len(env['payloads']) != 4:
|
|
||||||
print(f'❌ Expected 4 payloads, got {len(env["payloads"])}')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct number of payloads')
|
|
||||||
|
|
||||||
# Expected data
|
|
||||||
expected_data = [
|
|
||||||
('simple_dict', simple_dict, 'dictionary'),
|
|
||||||
('nested_dict', nested_dict, 'dictionary'),
|
|
||||||
('array_dict', array_dict, 'dictionary'),
|
|
||||||
('mixed_dict', mixed_dict, 'dictionary')
|
|
||||||
]
|
|
||||||
|
|
||||||
for i in range(len(env['payloads'])):
|
|
||||||
payload = env['payloads'][i]
|
|
||||||
expected = expected_data[i]
|
|
||||||
|
|
||||||
if payload[0] != expected[0]:
|
|
||||||
print(f"❌ Payload {i + 1}: Expected dataname '{expected[0]}', got '{payload[0]}'")
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print(f'✅ Payload {i + 1}: Correct dataname')
|
|
||||||
|
|
||||||
if payload[2] != expected[2]:
|
|
||||||
print(f"❌ Payload {i + 1}: Expected type '{expected[2]}', got '{payload[2]}'")
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print(f'✅ Payload {i + 1}: Correct type')
|
|
||||||
|
|
||||||
data_match = json.dumps(payload[1], sort_keys=True) == json.dumps(expected[1], sort_keys=True)
|
|
||||||
if not data_match:
|
|
||||||
print(f'❌ Payload {i + 1}: Data mismatch')
|
|
||||||
print(f' Expected: {json.dumps(expected[1], sort_keys=True)}')
|
|
||||||
print(f' Got: {json.dumps(payload[1], sort_keys=True)}')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print(f'✅ Payload {i + 1}: Data correctly deserialized')
|
|
||||||
|
|
||||||
# Test round-trip with receive
|
|
||||||
print('\n=== Round-trip Test ===')
|
|
||||||
round_trip_data = {
|
|
||||||
'correlation_id': 'roundtrip-' + _generate_uuid(),
|
|
||||||
'msg_id': _generate_uuid(),
|
|
||||||
'timestamp': '2024-01-15T10:30:00Z',
|
|
||||||
'send_to': '/test/dictionary',
|
|
||||||
'msg_purpose': 'test',
|
|
||||||
'sender_name': 'mpy-test',
|
|
||||||
'sender_id': _generate_uuid(),
|
|
||||||
'receiver_name': 'mpy-receiver',
|
|
||||||
'receiver_id': _generate_uuid(),
|
|
||||||
'reply_to': '',
|
|
||||||
'reply_to_msg_id': '',
|
|
||||||
'broker_url': TEST_BROKER_URL,
|
|
||||||
'metadata': {},
|
|
||||||
'payloads': [
|
|
||||||
{
|
|
||||||
'id': _generate_uuid(),
|
|
||||||
'dataname': 'roundtrip',
|
|
||||||
'payload_type': 'dictionary',
|
|
||||||
'transport': 'direct',
|
|
||||||
'encoding': 'base64',
|
|
||||||
'size': len(json.dumps({'test': 'data', 'nested': {'a': 1, 'b': 2}}).encode('utf8')),
|
|
||||||
'data': base64.b64encode(json.dumps({'test': 'data', 'nested': {'a': 1, 'b': 2}}).encode('utf8')).decode('ascii'),
|
|
||||||
'metadata': {'payload_bytes': len(json.dumps({'test': 'data', 'nested': {'a': 1, 'b': 2}}).encode('utf8'))}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
mock_rt_msg = {'payload': json.dumps(round_trip_data)}
|
|
||||||
rt_env = smartreceive(mock_rt_msg)
|
|
||||||
|
|
||||||
if rt_env['payloads'][0][0] == 'roundtrip' and rt_env['payloads'][0][2] == 'dictionary':
|
|
||||||
print('✅ Round-trip test successful')
|
|
||||||
else:
|
|
||||||
print('❌ Round-trip test 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__':
|
|
||||||
run_test()
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
"""
|
|
||||||
MicroPython Dictionary Sender Test
|
|
||||||
Tests the smartsend function with dictionary payloads
|
|
||||||
|
|
||||||
Note: This test is designed for both MicroPython and desktop Python
|
|
||||||
for compatibility testing.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
|
|
||||||
# Add parent directory to path
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
from natbridge_mpy import smartsend, DEFAULT_BROKER_URL, DEFAULT_FILESERVER_URL, DEFAULT_SIZE_THRESHOLD, MAX_PAYLOAD_SIZE
|
|
||||||
|
|
||||||
TEST_SUBJECT = '/test/dictionary'
|
|
||||||
TEST_BROKER_URL = os.environ.get('NATS_URL', 'nats://localhost:4222')
|
|
||||||
TEST_FILESERVER_URL = os.environ.get('FILESERVER_URL', 'http://localhost:8080')
|
|
||||||
|
|
||||||
|
|
||||||
def run_test():
|
|
||||||
print('=== MicroPython Dictionary Sender Test ===\n')
|
|
||||||
|
|
||||||
from natbridge_mpy import _generate_uuid
|
|
||||||
correlation_id = 'mpy-dict-test-' + _generate_uuid()
|
|
||||||
print(f'Correlation ID: {correlation_id}')
|
|
||||||
print(f'Subject: {TEST_SUBJECT}')
|
|
||||||
print(f'Broker URL: {TEST_BROKER_URL}')
|
|
||||||
print(f'Default Size Threshold: {DEFAULT_SIZE_THRESHOLD} bytes')
|
|
||||||
print(f'Max Payload Size: {MAX_PAYLOAD_SIZE} bytes\n')
|
|
||||||
|
|
||||||
# Test data - various dictionary structures
|
|
||||||
test_data = [
|
|
||||||
('simple_dict', {'key1': 'value1', 'key2': 'value2'}, 'dictionary'),
|
|
||||||
('nested_dict', {'outer': {'inner': 'value', 'number': 42}}, 'dictionary'),
|
|
||||||
('array_dict', {'items': [1, 2, 3, 'four', 'five']}, 'dictionary'),
|
|
||||||
('mixed_dict', {'string': 'text', 'number': 123, 'boolean': True, 'null_val': None}, 'dictionary')
|
|
||||||
]
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Send the message
|
|
||||||
print('Sending dictionary payloads...')
|
|
||||||
env, env_json_str = smartsend(
|
|
||||||
TEST_SUBJECT,
|
|
||||||
test_data,
|
|
||||||
broker_url=TEST_BROKER_URL,
|
|
||||||
fileserver_url=TEST_FILESERVER_URL,
|
|
||||||
correlation_id=correlation_id,
|
|
||||||
msg_purpose='test',
|
|
||||||
sender_name='mpy-dict-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
|
|
||||||
|
|
||||||
if len(env['payloads']) != 4:
|
|
||||||
print(f'❌ Expected 4 payloads, got {len(env["payloads"])}')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct number of payloads')
|
|
||||||
|
|
||||||
# Test each payload
|
|
||||||
expected_datanames = ['simple_dict', 'nested_dict', 'array_dict', 'mixed_dict']
|
|
||||||
expected_types = ['dictionary', 'dictionary', 'dictionary', 'dictionary']
|
|
||||||
|
|
||||||
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')
|
|
||||||
|
|
||||||
# Decode and verify the data
|
|
||||||
import base64
|
|
||||||
decoded_data = json.loads(base64.b64decode(payload['data']).decode('ascii'))
|
|
||||||
original_data = test_data[i][1]
|
|
||||||
|
|
||||||
# Normalize for comparison
|
|
||||||
if json.dumps(decoded_data, sort_keys=True) != json.dumps(original_data, sort_keys=True):
|
|
||||||
print(f'❌ Payload {i + 1}: Data integrity mismatch')
|
|
||||||
print(f' Expected: {json.dumps(original_data)}')
|
|
||||||
print(f' Got: {json.dumps(decoded_data)}')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print(f'✅ Payload {i + 1}: Data integrity verified')
|
|
||||||
|
|
||||||
print(f' Size: {payload["size"]} bytes\n')
|
|
||||||
|
|
||||||
# Test round-trip serialization
|
|
||||||
print('=== Round-trip Serialization Test ===')
|
|
||||||
round_trip_data = [
|
|
||||||
('roundtrip', {'test': 'data', 'numbers': [1, 2, 3], 'nested': {'a': 1, 'b': 2}}, 'dictionary')
|
|
||||||
]
|
|
||||||
|
|
||||||
rt_env, _ = smartsend(
|
|
||||||
TEST_SUBJECT,
|
|
||||||
round_trip_data,
|
|
||||||
broker_url=TEST_BROKER_URL,
|
|
||||||
fileserver_url=TEST_FILESERVER_URL,
|
|
||||||
correlation_id='roundtrip-' + correlation_id,
|
|
||||||
is_publish=False
|
|
||||||
)
|
|
||||||
|
|
||||||
rt_payload = rt_env['payloads'][0]
|
|
||||||
rt_decoded = json.loads(base64.b64decode(rt_payload['data']).decode('ascii'))
|
|
||||||
|
|
||||||
if json.dumps(rt_decoded, sort_keys=True) == json.dumps(round_trip_data[0][1], sort_keys=True):
|
|
||||||
print('✅ Round-trip serialization successful')
|
|
||||||
else:
|
|
||||||
print('❌ Round-trip serialization failed')
|
|
||||||
passed = False
|
|
||||||
|
|
||||||
# Test JSON string output
|
|
||||||
print('\n=== JSON String Output Test ===')
|
|
||||||
try:
|
|
||||||
parsed = json.loads(env_json_str)
|
|
||||||
if parsed['correlation_id'] == env['correlation_id'] and \
|
|
||||||
len(parsed['payloads']) == len(env['payloads']):
|
|
||||||
print('✅ JSON string is valid and matches envelope')
|
|
||||||
else:
|
|
||||||
print('❌ JSON string does not match envelope')
|
|
||||||
passed = False
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
print(f'❌ JSON string is invalid: {e}')
|
|
||||||
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__':
|
|
||||||
run_test()
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
"""
|
|
||||||
MicroPython Text Receiver Test
|
|
||||||
Tests the smartreceive function with text payloads
|
|
||||||
|
|
||||||
Note: This test is designed for both MicroPython and desktop Python
|
|
||||||
for compatibility testing.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
|
|
||||||
# Add parent directory to path
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
from natbridge_mpy import smartreceive, DEFAULT_BROKER_URL, DEFAULT_FILESERVER_URL
|
|
||||||
|
|
||||||
TEST_BROKER_URL = os.environ.get('NATS_URL', 'nats://localhost:4222')
|
|
||||||
TEST_FILESERVER_URL = os.environ.get('FILESERVER_URL', 'http://localhost:8080')
|
|
||||||
|
|
||||||
|
|
||||||
def run_test():
|
|
||||||
print('=== MicroPython Text Receiver Test ===\n')
|
|
||||||
|
|
||||||
from natbridge_mpy import _generate_uuid
|
|
||||||
|
|
||||||
# Create a mock NATS message with text payload
|
|
||||||
test_text = 'Hello, NATSBridge! This is a test message.'
|
|
||||||
import base64
|
|
||||||
|
|
||||||
test_data = {
|
|
||||||
'correlation_id': 'mpy-receiver-test-' + _generate_uuid(),
|
|
||||||
'msg_id': _generate_uuid(),
|
|
||||||
'timestamp': '2024-01-15T10:30:00Z',
|
|
||||||
'send_to': '/test/text',
|
|
||||||
'msg_purpose': 'test',
|
|
||||||
'sender_name': 'mpy-text-test',
|
|
||||||
'sender_id': _generate_uuid(),
|
|
||||||
'receiver_name': 'mpy-receiver',
|
|
||||||
'receiver_id': _generate_uuid(),
|
|
||||||
'reply_to': '',
|
|
||||||
'reply_to_msg_id': '',
|
|
||||||
'broker_url': TEST_BROKER_URL,
|
|
||||||
'metadata': {},
|
|
||||||
'payloads': [
|
|
||||||
{
|
|
||||||
'id': _generate_uuid(),
|
|
||||||
'dataname': 'message',
|
|
||||||
'payload_type': 'text',
|
|
||||||
'transport': 'direct',
|
|
||||||
'encoding': 'base64',
|
|
||||||
'size': len(test_text.encode('utf8')),
|
|
||||||
'data': base64.b64encode(test_text.encode('utf8')).decode('ascii'),
|
|
||||||
'metadata': {'payload_bytes': len(test_text.encode('utf8'))}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
mock_msg = {
|
|
||||||
'payload': json.dumps(test_data)
|
|
||||||
}
|
|
||||||
|
|
||||||
print('Mock Message Created:')
|
|
||||||
print(f' Correlation ID: {test_data["correlation_id"]}')
|
|
||||||
print(f' Payloads: {len(test_data["payloads"])}')
|
|
||||||
print(f' Payload dataname: {test_data["payloads"][0]["dataname"]}')
|
|
||||||
print(f' Payload type: {test_data["payloads"][0]["payload_type"]}')
|
|
||||||
print(f' Transport: {test_data["payloads"][0]["transport"]}\n')
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Receive and process the message
|
|
||||||
print('Receiving and processing message...')
|
|
||||||
env = smartreceive(
|
|
||||||
mock_msg,
|
|
||||||
max_retries=3,
|
|
||||||
base_delay=100,
|
|
||||||
max_delay=1000
|
|
||||||
)
|
|
||||||
|
|
||||||
print('\n=== Received Envelope ===')
|
|
||||||
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'Payloads: {len(env["payloads"])}\n')
|
|
||||||
|
|
||||||
# Validate received data
|
|
||||||
print('=== Validation ===')
|
|
||||||
passed = True
|
|
||||||
|
|
||||||
if not env.get('correlation_id'):
|
|
||||||
print('❌ correlation_id is missing')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ correlation_id present')
|
|
||||||
|
|
||||||
if len(env['payloads']) != 1:
|
|
||||||
print(f'❌ Expected 1 payload, got {len(env["payloads"])}')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct number of payloads')
|
|
||||||
|
|
||||||
payload = env['payloads'][0]
|
|
||||||
if payload[0] != 'message':
|
|
||||||
print(f"❌ Expected dataname 'message', got '{payload[0]}'")
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct dataname')
|
|
||||||
|
|
||||||
if payload[2] != 'text':
|
|
||||||
print(f"❌ Expected type 'text', got '{payload[2]}'")
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct type')
|
|
||||||
|
|
||||||
if payload[1] != test_text:
|
|
||||||
print('❌ Data mismatch')
|
|
||||||
print(f' Expected: {test_text}')
|
|
||||||
print(f' Got: {payload[1]}')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Data correctly deserialized')
|
|
||||||
|
|
||||||
# Test with multiple text payloads
|
|
||||||
print('\n=== Multiple Text Payloads Test ===')
|
|
||||||
multi_test_data = {
|
|
||||||
'correlation_id': 'multi-receiver-' + _generate_uuid(),
|
|
||||||
'msg_id': _generate_uuid(),
|
|
||||||
'timestamp': '2024-01-15T10:30:00Z',
|
|
||||||
'send_to': '/test/text',
|
|
||||||
'msg_purpose': 'test',
|
|
||||||
'sender_name': 'mpy-text-test',
|
|
||||||
'sender_id': _generate_uuid(),
|
|
||||||
'receiver_name': 'mpy-receiver',
|
|
||||||
'receiver_id': _generate_uuid(),
|
|
||||||
'reply_to': '',
|
|
||||||
'reply_to_msg_id': '',
|
|
||||||
'broker_url': TEST_BROKER_URL,
|
|
||||||
'metadata': {},
|
|
||||||
'payloads': [
|
|
||||||
{
|
|
||||||
'id': _generate_uuid(),
|
|
||||||
'dataname': 'msg1',
|
|
||||||
'payload_type': 'text',
|
|
||||||
'transport': 'direct',
|
|
||||||
'encoding': 'base64',
|
|
||||||
'size': 16,
|
|
||||||
'data': base64.b64encode(b'First message').decode('ascii'),
|
|
||||||
'metadata': {'payload_bytes': 16}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': _generate_uuid(),
|
|
||||||
'dataname': 'msg2',
|
|
||||||
'payload_type': 'text',
|
|
||||||
'transport': 'direct',
|
|
||||||
'encoding': 'base64',
|
|
||||||
'size': 16,
|
|
||||||
'data': base64.b64encode(b'Second message').decode('ascii'),
|
|
||||||
'metadata': {'payload_bytes': 16}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': _generate_uuid(),
|
|
||||||
'dataname': 'msg3',
|
|
||||||
'payload_type': 'text',
|
|
||||||
'transport': 'direct',
|
|
||||||
'encoding': 'base64',
|
|
||||||
'size': 16,
|
|
||||||
'data': base64.b64encode(b'Third message').decode('ascii'),
|
|
||||||
'metadata': {'payload_bytes': 16}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
mock_multi_msg = {'payload': json.dumps(multi_test_data)}
|
|
||||||
multi_env = smartreceive(mock_multi_msg)
|
|
||||||
|
|
||||||
if len(multi_env['payloads']) == 3:
|
|
||||||
print('✅ Multiple payloads handled correctly')
|
|
||||||
|
|
||||||
# Verify each payload
|
|
||||||
expected_messages = ['First message', 'Second message', 'Third message']
|
|
||||||
for i in range(3):
|
|
||||||
if multi_env['payloads'][i][1] == expected_messages[i]:
|
|
||||||
print(f'✅ Payload {i + 1} correctly deserialized')
|
|
||||||
else:
|
|
||||||
print(f'❌ Payload {i + 1} mismatch')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print(f'❌ Expected 3 payloads, got {len(multi_env["payloads"])}')
|
|
||||||
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__':
|
|
||||||
run_test()
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
"""
|
|
||||||
MicroPython Text Sender Test
|
|
||||||
Tests the smartsend function with text payloads
|
|
||||||
|
|
||||||
Note: This test is designed for both MicroPython and desktop Python
|
|
||||||
for compatibility testing.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Add parent directory to path
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
from natbridge_mpy import smartsend, DEFAULT_BROKER_URL, DEFAULT_FILESERVER_URL, DEFAULT_SIZE_THRESHOLD, MAX_PAYLOAD_SIZE
|
|
||||||
|
|
||||||
TEST_SUBJECT = '/test/text'
|
|
||||||
TEST_BROKER_URL = os.environ.get('NATS_URL', 'nats://localhost:4222')
|
|
||||||
TEST_FILESERVER_URL = os.environ.get('FILESERVER_URL', 'http://localhost:8080')
|
|
||||||
|
|
||||||
|
|
||||||
def run_test():
|
|
||||||
print('=== MicroPython Text Sender Test ===\n')
|
|
||||||
|
|
||||||
from natbridge_mpy import _generate_uuid
|
|
||||||
correlation_id = 'mpy-text-test-' + _generate_uuid()
|
|
||||||
print(f'Correlation ID: {correlation_id}')
|
|
||||||
print(f'Subject: {TEST_SUBJECT}')
|
|
||||||
print(f'Broker URL: {TEST_BROKER_URL}')
|
|
||||||
print(f'Default Size Threshold: {DEFAULT_SIZE_THRESHOLD} bytes')
|
|
||||||
print(f'Max Payload Size: {MAX_PAYLOAD_SIZE} bytes\n')
|
|
||||||
|
|
||||||
# Test data
|
|
||||||
text_data = 'Hello, NATSBridge! This is a test message.'
|
|
||||||
test_data = [
|
|
||||||
('message', text_data, 'text')
|
|
||||||
]
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Send the message
|
|
||||||
print('Sending text payload...')
|
|
||||||
env, env_json_str = smartsend(
|
|
||||||
TEST_SUBJECT,
|
|
||||||
test_data,
|
|
||||||
broker_url=TEST_BROKER_URL,
|
|
||||||
fileserver_url=TEST_FILESERVER_URL,
|
|
||||||
correlation_id=correlation_id,
|
|
||||||
msg_purpose='test',
|
|
||||||
sender_name='mpy-text-test',
|
|
||||||
is_publish=False # Don't actually publish for this test
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
if not env.get('correlation_id'):
|
|
||||||
print('❌ correlation_id is missing')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ correlation_id present')
|
|
||||||
|
|
||||||
if not env.get('msg_id'):
|
|
||||||
print('❌ msg_id is missing')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ msg_id present')
|
|
||||||
|
|
||||||
if not env.get('timestamp'):
|
|
||||||
print('❌ timestamp is missing')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ timestamp present')
|
|
||||||
|
|
||||||
if len(env['payloads']) != 1:
|
|
||||||
print(f'❌ Expected 1 payload, got {len(env["payloads"])}')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct number of payloads')
|
|
||||||
|
|
||||||
payload = env['payloads'][0]
|
|
||||||
if payload['dataname'] != 'message':
|
|
||||||
print(f"❌ Expected dataname 'message', got '{payload['dataname']}'")
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct dataname')
|
|
||||||
|
|
||||||
if payload['payload_type'] != 'text':
|
|
||||||
print(f"❌ Expected payload_type 'text', got '{payload['payload_type']}'")
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct payload_type')
|
|
||||||
|
|
||||||
if payload['transport'] != 'direct':
|
|
||||||
print(f"❌ Expected transport 'direct', got '{payload['transport']}'")
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct transport')
|
|
||||||
|
|
||||||
if payload['encoding'] != 'base64':
|
|
||||||
print(f"❌ Expected encoding 'base64', got '{payload['encoding']}'")
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct encoding')
|
|
||||||
|
|
||||||
# Decode and verify the data
|
|
||||||
import base64
|
|
||||||
decoded_data = base64.b64decode(payload['data']).decode('ascii')
|
|
||||||
if decoded_data != text_data:
|
|
||||||
print('❌ Decoded data mismatch')
|
|
||||||
print(f' Expected: {text_data}')
|
|
||||||
print(f' Got: {decoded_data}')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Data integrity verified')
|
|
||||||
|
|
||||||
print(f'\nPayload size: {payload["size"]} bytes')
|
|
||||||
print(f'Base64 data length: {len(payload["data"])} chars')
|
|
||||||
|
|
||||||
# Test with multiple text payloads
|
|
||||||
print('\n=== Multiple Text Payloads Test ===')
|
|
||||||
multi_test_data = [
|
|
||||||
('msg1', 'First message', 'text'),
|
|
||||||
('msg2', 'Second message', 'text'),
|
|
||||||
('msg3', 'Third message', 'text')
|
|
||||||
]
|
|
||||||
|
|
||||||
multi_env, _ = smartsend(
|
|
||||||
TEST_SUBJECT,
|
|
||||||
multi_test_data,
|
|
||||||
broker_url=TEST_BROKER_URL,
|
|
||||||
fileserver_url=TEST_FILESERVER_URL,
|
|
||||||
correlation_id='multi-test-' + correlation_id,
|
|
||||||
is_publish=False
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(multi_env['payloads']) == 3:
|
|
||||||
print('✅ Multiple payloads handled correctly')
|
|
||||||
else:
|
|
||||||
print(f'❌ Expected 3 payloads, got {len(multi_env["payloads"])}')
|
|
||||||
passed = False
|
|
||||||
|
|
||||||
# Test size threshold enforcement
|
|
||||||
print('\n=== Size Threshold Test ===')
|
|
||||||
small_text = 'small'
|
|
||||||
large_text = 'x' * (DEFAULT_SIZE_THRESHOLD - 100) # Just under threshold
|
|
||||||
|
|
||||||
small_env, _ = smartsend(
|
|
||||||
TEST_SUBJECT,
|
|
||||||
[('small', small_text, 'text')],
|
|
||||||
broker_url=TEST_BROKER_URL,
|
|
||||||
is_publish=False
|
|
||||||
)
|
|
||||||
|
|
||||||
if small_env['payloads'][0]['transport'] == 'direct':
|
|
||||||
print('✅ Small payload uses direct transport')
|
|
||||||
else:
|
|
||||||
print('❌ Small payload should use direct transport')
|
|
||||||
passed = False
|
|
||||||
|
|
||||||
# Test that large text (> MAX_PAYLOAD_SIZE) raises error
|
|
||||||
print('\n=== Max Payload Size Test ===')
|
|
||||||
try:
|
|
||||||
too_large_text = 'x' * (MAX_PAYLOAD_SIZE + 1000)
|
|
||||||
large_env, _ = smartsend(
|
|
||||||
TEST_SUBJECT,
|
|
||||||
[('large', too_large_text, 'text')],
|
|
||||||
broker_url=TEST_BROKER_URL,
|
|
||||||
is_publish=False
|
|
||||||
)
|
|
||||||
print('❌ Should have raised MemoryError for payload exceeding MAX_PAYLOAD_SIZE')
|
|
||||||
passed = False
|
|
||||||
except MemoryError as e:
|
|
||||||
print(f'✅ Correctly raised MemoryError: {e}')
|
|
||||||
except Exception as e:
|
|
||||||
print(f'❌ Unexpected error: {type(e).__name__}: {e}')
|
|
||||||
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__':
|
|
||||||
run_test()
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
"""
|
|
||||||
Python Binary Receiver Test
|
|
||||||
Tests the smartreceive function with binary/image/audio/video/table payloads
|
|
||||||
"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import base64
|
|
||||||
|
|
||||||
# Add parent directory to path
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
from natbridge import smartreceive, DEFAULT_BROKER_URL, DEFAULT_FILESERVER_URL
|
|
||||||
|
|
||||||
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 Binary Receiver Test ===\n')
|
|
||||||
|
|
||||||
# Create mock NATS message with binary payloads
|
|
||||||
image_data = bytes([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) # PNG header
|
|
||||||
audio_data = bytes([0x46, 0x4C, 0x41, 0x43, 0x00, 0x00, 0x00, 0x00]) # FLAC header
|
|
||||||
video_data = bytes([0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70]) # MP4 header
|
|
||||||
generic_binary = bytes([0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE])
|
|
||||||
|
|
||||||
test_data = {
|
|
||||||
'correlation_id': 'py-binary-receiver-' + str(asyncio.get_event_loop().time() * 1000000),
|
|
||||||
'msg_id': 'msg-' + str(asyncio.get_event_loop().time() * 1000000),
|
|
||||||
'timestamp': asyncio.get_event_loop().time().isoformat(),
|
|
||||||
'send_to': '/test/binary',
|
|
||||||
'msg_purpose': 'test',
|
|
||||||
'sender_name': 'py-binary-test',
|
|
||||||
'sender_id': 'sender-' + str(asyncio.get_event_loop().time() * 1000000),
|
|
||||||
'receiver_name': 'py-receiver',
|
|
||||||
'receiver_id': 'receiver-' + str(asyncio.get_event_loop().time() * 1000000),
|
|
||||||
'reply_to': '',
|
|
||||||
'reply_to_msg_id': '',
|
|
||||||
'broker_url': TEST_BROKER_URL,
|
|
||||||
'metadata': {},
|
|
||||||
'payloads': [
|
|
||||||
{
|
|
||||||
'id': 'payload-1',
|
|
||||||
'dataname': 'image',
|
|
||||||
'payload_type': 'image',
|
|
||||||
'transport': 'direct',
|
|
||||||
'encoding': 'base64',
|
|
||||||
'size': len(image_data),
|
|
||||||
'data': base64.b64encode(image_data).decode('ascii'),
|
|
||||||
'metadata': {'payload_bytes': len(image_data)}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 'payload-2',
|
|
||||||
'dataname': 'audio',
|
|
||||||
'payload_type': 'audio',
|
|
||||||
'transport': 'direct',
|
|
||||||
'encoding': 'base64',
|
|
||||||
'size': len(audio_data),
|
|
||||||
'data': base64.b64encode(audio_data).decode('ascii'),
|
|
||||||
'metadata': {'payload_bytes': len(audio_data)}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 'payload-3',
|
|
||||||
'dataname': 'video',
|
|
||||||
'payload_type': 'video',
|
|
||||||
'transport': 'direct',
|
|
||||||
'encoding': 'base64',
|
|
||||||
'size': len(video_data),
|
|
||||||
'data': base64.b64encode(video_data).decode('ascii'),
|
|
||||||
'metadata': {'payload_bytes': len(video_data)}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 'payload-4',
|
|
||||||
'dataname': 'binary',
|
|
||||||
'payload_type': 'binary',
|
|
||||||
'transport': 'direct',
|
|
||||||
'encoding': 'base64',
|
|
||||||
'size': len(generic_binary),
|
|
||||||
'data': base64.b64encode(generic_binary).decode('ascii'),
|
|
||||||
'metadata': {'payload_bytes': len(generic_binary)}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
mock_msg = {
|
|
||||||
'payload': json.dumps(test_data)
|
|
||||||
}
|
|
||||||
|
|
||||||
print('Mock Message Created:')
|
|
||||||
print(f' Correlation ID: {test_data["correlation_id"]}')
|
|
||||||
print(f' Payloads: {len(test_data["payloads"])}')
|
|
||||||
print(f' Payload types: {", ".join(p["payload_type"] for p in test_data["payloads"])}\n')
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Receive and process the message
|
|
||||||
print('Receiving and processing message...')
|
|
||||||
env = await smartreceive(
|
|
||||||
mock_msg,
|
|
||||||
max_retries=3,
|
|
||||||
base_delay=100,
|
|
||||||
max_delay=1000
|
|
||||||
)
|
|
||||||
|
|
||||||
print('\n=== Received Envelope ===')
|
|
||||||
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'Payloads: {len(env["payloads"])}\n')
|
|
||||||
|
|
||||||
# Validate received data
|
|
||||||
print('=== Validation ===')
|
|
||||||
passed = True
|
|
||||||
|
|
||||||
if not env.get('correlation_id'):
|
|
||||||
print('❌ correlation_id is missing')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ correlation_id present')
|
|
||||||
|
|
||||||
if len(env['payloads']) != 4:
|
|
||||||
print(f'❌ Expected 4 payloads, got {len(env["payloads"])}')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct number of payloads')
|
|
||||||
|
|
||||||
# Expected data
|
|
||||||
expected_data = [
|
|
||||||
('image', image_data, 'image'),
|
|
||||||
('audio', audio_data, 'audio'),
|
|
||||||
('video', video_data, 'video'),
|
|
||||||
('binary', generic_binary, 'binary')
|
|
||||||
]
|
|
||||||
|
|
||||||
for i in range(len(env['payloads'])):
|
|
||||||
payload = env['payloads'][i]
|
|
||||||
expected = expected_data[i]
|
|
||||||
|
|
||||||
if payload[0] != expected[0]:
|
|
||||||
print(f"❌ Payload {i + 1}: Expected dataname '{expected[0]}', got '{payload[0]}'")
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print(f'✅ Payload {i + 1}: Correct dataname')
|
|
||||||
|
|
||||||
if payload[2] != expected[2]:
|
|
||||||
print(f"❌ Payload {i + 1}: Expected type '{expected[2]}', got '{payload[2]}'")
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print(f'✅ Payload {i + 1}: Correct type')
|
|
||||||
|
|
||||||
# Verify binary data integrity
|
|
||||||
received_data = payload[1]
|
|
||||||
if not isinstance(received_data, (bytes, bytearray)):
|
|
||||||
print(f'❌ Payload {i + 1}: Expected bytes/bytearray, got {type(received_data)}')
|
|
||||||
passed = False
|
|
||||||
elif received_data != expected[1]:
|
|
||||||
print(f'❌ Payload {i + 1}: Data mismatch')
|
|
||||||
print(f' Expected: {expected[1]}')
|
|
||||||
print(f' Got: {received_data}')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print(f'✅ Payload {i + 1}: Data correctly deserialized')
|
|
||||||
|
|
||||||
# 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())
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
"""
|
|
||||||
Python Binary Sender Test
|
|
||||||
Tests the smartsend function with binary/image/audio/video/table payloads
|
|
||||||
"""
|
|
||||||
|
|
||||||
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 natbridge import smartsend, DEFAULT_BROKER_URL, DEFAULT_FILESERVER_URL
|
|
||||||
|
|
||||||
TEST_SUBJECT = '/test/binary'
|
|
||||||
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 Binary Sender Test ===\n')
|
|
||||||
|
|
||||||
correlation_id = 'py-binary-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 - binary data for different types
|
|
||||||
image_data = bytes([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) # PNG header
|
|
||||||
audio_data = bytes([0x46, 0x4C, 0x41, 0x43, 0x00, 0x00, 0x00, 0x00]) # FLAC header
|
|
||||||
video_data = bytes([0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70]) # MP4 header
|
|
||||||
generic_binary = bytes([0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE])
|
|
||||||
|
|
||||||
# Test table data
|
|
||||||
try:
|
|
||||||
import pandas as pd
|
|
||||||
table_data = pd.DataFrame({
|
|
||||||
'id': [1, 2, 3, 4, 5],
|
|
||||||
'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
|
|
||||||
'value': [10.5, 20.3, 30.1, 40.9, 50.7]
|
|
||||||
})
|
|
||||||
table_available = True
|
|
||||||
except ImportError:
|
|
||||||
table_available = False
|
|
||||||
table_data = None
|
|
||||||
|
|
||||||
test_data = [
|
|
||||||
('image', image_data, 'image'),
|
|
||||||
('audio', audio_data, 'audio'),
|
|
||||||
('video', video_data, 'video'),
|
|
||||||
('binary', generic_binary, 'binary')
|
|
||||||
]
|
|
||||||
|
|
||||||
if table_available:
|
|
||||||
test_data.append(('table', table_data, 'table'))
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Send the message
|
|
||||||
print('Sending binary 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-binary-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 = 5 if table_available else 4
|
|
||||||
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 = ['image', 'audio', 'video', 'binary']
|
|
||||||
expected_types = ['image', 'audio', 'video', 'binary']
|
|
||||||
expected_data = [image_data, audio_data, video_data, generic_binary]
|
|
||||||
|
|
||||||
if table_available:
|
|
||||||
expected_datanames.append('table')
|
|
||||||
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')
|
|
||||||
|
|
||||||
# Decode and verify the data
|
|
||||||
decoded_data = base64.b64decode(payload['data'])
|
|
||||||
|
|
||||||
if i < len(expected_data):
|
|
||||||
original_data = expected_data[i]
|
|
||||||
if decoded_data != original_data:
|
|
||||||
print(f'❌ Payload {i + 1}: Data integrity mismatch')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print(f'✅ Payload {i + 1}: Data integrity verified')
|
|
||||||
else:
|
|
||||||
# Table payload - just verify it's present
|
|
||||||
print(f'✅ Payload {i + 1}: Table data present (size: {payload["size"]} bytes)')
|
|
||||||
|
|
||||||
print(f' Size: {payload["size"]} bytes\n')
|
|
||||||
|
|
||||||
# Test with larger binary data
|
|
||||||
print('=== Large Binary Data Test ===')
|
|
||||||
large_data = bytes([0xFF] * 10000) # 10KB of binary data
|
|
||||||
large_test_data = [
|
|
||||||
('large_binary', large_data, 'binary')
|
|
||||||
]
|
|
||||||
|
|
||||||
large_env, _ = await smartsend(
|
|
||||||
TEST_SUBJECT,
|
|
||||||
large_test_data,
|
|
||||||
broker_url=TEST_BROKER_URL,
|
|
||||||
fileserver_url=TEST_FILESERVER_URL,
|
|
||||||
correlation_id='large-' + correlation_id,
|
|
||||||
is_publish=False
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(large_env['payloads']) == 1 and large_env['payloads'][0]['size'] == 10000:
|
|
||||||
print('✅ Large binary data handled correctly')
|
|
||||||
else:
|
|
||||||
print('❌ Large binary data 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())
|
|
||||||
@@ -1,220 +0,0 @@
|
|||||||
"""
|
|
||||||
Python Dictionary Receiver Test
|
|
||||||
Tests the smartreceive function with dictionary payloads
|
|
||||||
"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
|
|
||||||
# Add parent directory to path
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
from natbridge import smartreceive, DEFAULT_BROKER_URL, DEFAULT_FILESERVER_URL
|
|
||||||
|
|
||||||
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 Dictionary Receiver Test ===\n')
|
|
||||||
|
|
||||||
# Create a mock NATS message with dictionary payloads
|
|
||||||
import base64
|
|
||||||
|
|
||||||
simple_dict = {'key1': 'value1', 'key2': 'value2'}
|
|
||||||
nested_dict = {'outer': {'inner': 'value', 'number': 42}}
|
|
||||||
array_dict = {'items': [1, 2, 3, 'four', 'five']}
|
|
||||||
mixed_dict = {'string': 'text', 'number': 123, 'boolean': True, 'null_val': None}
|
|
||||||
|
|
||||||
test_data = {
|
|
||||||
'correlation_id': 'py-receiver-dict-' + str(asyncio.get_event_loop().time() * 1000000),
|
|
||||||
'msg_id': 'msg-' + str(asyncio.get_event_loop().time() * 1000000),
|
|
||||||
'timestamp': asyncio.get_event_loop().time().isoformat(),
|
|
||||||
'send_to': '/test/dictionary',
|
|
||||||
'msg_purpose': 'test',
|
|
||||||
'sender_name': 'py-dict-test',
|
|
||||||
'sender_id': 'sender-' + str(asyncio.get_event_loop().time() * 1000000),
|
|
||||||
'receiver_name': 'py-receiver',
|
|
||||||
'receiver_id': 'receiver-' + str(asyncio.get_event_loop().time() * 1000000),
|
|
||||||
'reply_to': '',
|
|
||||||
'reply_to_msg_id': '',
|
|
||||||
'broker_url': TEST_BROKER_URL,
|
|
||||||
'metadata': {},
|
|
||||||
'payloads': [
|
|
||||||
{
|
|
||||||
'id': 'payload-1',
|
|
||||||
'dataname': 'simple_dict',
|
|
||||||
'payload_type': 'dictionary',
|
|
||||||
'transport': 'direct',
|
|
||||||
'encoding': 'base64',
|
|
||||||
'size': len(json.dumps(simple_dict).encode('utf8')),
|
|
||||||
'data': base64.b64encode(json.dumps(simple_dict).encode('utf8')).decode('ascii'),
|
|
||||||
'metadata': {'payload_bytes': len(json.dumps(simple_dict).encode('utf8'))}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 'payload-2',
|
|
||||||
'dataname': 'nested_dict',
|
|
||||||
'payload_type': 'dictionary',
|
|
||||||
'transport': 'direct',
|
|
||||||
'encoding': 'base64',
|
|
||||||
'size': len(json.dumps(nested_dict).encode('utf8')),
|
|
||||||
'data': base64.b64encode(json.dumps(nested_dict).encode('utf8')).decode('ascii'),
|
|
||||||
'metadata': {'payload_bytes': len(json.dumps(nested_dict).encode('utf8'))}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 'payload-3',
|
|
||||||
'dataname': 'array_dict',
|
|
||||||
'payload_type': 'dictionary',
|
|
||||||
'transport': 'direct',
|
|
||||||
'encoding': 'base64',
|
|
||||||
'size': len(json.dumps(array_dict).encode('utf8')),
|
|
||||||
'data': base64.b64encode(json.dumps(array_dict).encode('utf8')).decode('ascii'),
|
|
||||||
'metadata': {'payload_bytes': len(json.dumps(array_dict).encode('utf8'))}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 'payload-4',
|
|
||||||
'dataname': 'mixed_dict',
|
|
||||||
'payload_type': 'dictionary',
|
|
||||||
'transport': 'direct',
|
|
||||||
'encoding': 'base64',
|
|
||||||
'size': len(json.dumps(mixed_dict).encode('utf8')),
|
|
||||||
'data': base64.b64encode(json.dumps(mixed_dict).encode('utf8')).decode('ascii'),
|
|
||||||
'metadata': {'payload_bytes': len(json.dumps(mixed_dict).encode('utf8'))}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
mock_msg = {
|
|
||||||
'payload': json.dumps(test_data)
|
|
||||||
}
|
|
||||||
|
|
||||||
print('Mock Message Created:')
|
|
||||||
print(f' Correlation ID: {test_data["correlation_id"]}')
|
|
||||||
print(f' Payloads: {len(test_data["payloads"])}')
|
|
||||||
print(f' Payload types: {", ".join(p["payload_type"] for p in test_data["payloads"])}\n')
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Receive and process the message
|
|
||||||
print('Receiving and processing message...')
|
|
||||||
env = await smartreceive(
|
|
||||||
mock_msg,
|
|
||||||
max_retries=3,
|
|
||||||
base_delay=100,
|
|
||||||
max_delay=1000
|
|
||||||
)
|
|
||||||
|
|
||||||
print('\n=== Received Envelope ===')
|
|
||||||
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'Payloads: {len(env["payloads"])}\n')
|
|
||||||
|
|
||||||
# Validate received data
|
|
||||||
print('=== Validation ===')
|
|
||||||
passed = True
|
|
||||||
|
|
||||||
if not env.get('correlation_id'):
|
|
||||||
print('❌ correlation_id is missing')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ correlation_id present')
|
|
||||||
|
|
||||||
if len(env['payloads']) != 4:
|
|
||||||
print(f'❌ Expected 4 payloads, got {len(env["payloads"])}')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct number of payloads')
|
|
||||||
|
|
||||||
# Expected data
|
|
||||||
expected_data = [
|
|
||||||
('simple_dict', simple_dict, 'dictionary'),
|
|
||||||
('nested_dict', nested_dict, 'dictionary'),
|
|
||||||
('array_dict', array_dict, 'dictionary'),
|
|
||||||
('mixed_dict', mixed_dict, 'dictionary')
|
|
||||||
]
|
|
||||||
|
|
||||||
for i in range(len(env['payloads'])):
|
|
||||||
payload = env['payloads'][i]
|
|
||||||
expected = expected_data[i]
|
|
||||||
|
|
||||||
if payload[0] != expected[0]:
|
|
||||||
print(f"❌ Payload {i + 1}: Expected dataname '{expected[0]}', got '{payload[0]}'")
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print(f'✅ Payload {i + 1}: Correct dataname')
|
|
||||||
|
|
||||||
if payload[2] != expected[2]:
|
|
||||||
print(f"❌ Payload {i + 1}: Expected type '{expected[2]}', got '{payload[2]}'")
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print(f'✅ Payload {i + 1}: Correct type')
|
|
||||||
|
|
||||||
data_match = json.dumps(payload[1], sort_keys=True) == json.dumps(expected[1], sort_keys=True)
|
|
||||||
if not data_match:
|
|
||||||
print(f'❌ Payload {i + 1}: Data mismatch')
|
|
||||||
print(f' Expected: {json.dumps(expected[1], sort_keys=True)}')
|
|
||||||
print(f' Got: {json.dumps(payload[1], sort_keys=True)}')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print(f'✅ Payload {i + 1}: Data correctly deserialized')
|
|
||||||
|
|
||||||
# Test round-trip with receive
|
|
||||||
print('\n=== Round-trip Test ===')
|
|
||||||
round_trip_data = {
|
|
||||||
'correlation_id': 'roundtrip-' + str(asyncio.get_event_loop().time() * 1000000),
|
|
||||||
'msg_id': 'msg-' + str(asyncio.get_event_loop().time() * 1000000),
|
|
||||||
'timestamp': asyncio.get_event_loop().time().isoformat(),
|
|
||||||
'send_to': '/test/dictionary',
|
|
||||||
'msg_purpose': 'test',
|
|
||||||
'sender_name': 'py-test',
|
|
||||||
'sender_id': 'sender-' + str(asyncio.get_event_loop().time() * 1000000),
|
|
||||||
'receiver_name': 'py-receiver',
|
|
||||||
'receiver_id': 'receiver-' + str(asyncio.get_event_loop().time() * 1000000),
|
|
||||||
'reply_to': '',
|
|
||||||
'reply_to_msg_id': '',
|
|
||||||
'broker_url': TEST_BROKER_URL,
|
|
||||||
'metadata': {},
|
|
||||||
'payloads': [
|
|
||||||
{
|
|
||||||
'id': 'payload-rt',
|
|
||||||
'dataname': 'roundtrip',
|
|
||||||
'payload_type': 'dictionary',
|
|
||||||
'transport': 'direct',
|
|
||||||
'encoding': 'base64',
|
|
||||||
'size': len(json.dumps({'test': 'data', 'nested': {'a': 1, 'b': 2}}).encode('utf8')),
|
|
||||||
'data': base64.b64encode(json.dumps({'test': 'data', 'nested': {'a': 1, 'b': 2}}).encode('utf8')).decode('ascii'),
|
|
||||||
'metadata': {'payload_bytes': len(json.dumps({'test': 'data', 'nested': {'a': 1, 'b': 2}}).encode('utf8'))}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
mock_rt_msg = {'payload': json.dumps(round_trip_data)}
|
|
||||||
rt_env = await smartreceive(mock_rt_msg)
|
|
||||||
|
|
||||||
if rt_env['payloads'][0][0] == 'roundtrip' and rt_env['payloads'][0][2] == 'dictionary':
|
|
||||||
print('✅ Round-trip test successful')
|
|
||||||
else:
|
|
||||||
print('❌ Round-trip test 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())
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
"""
|
|
||||||
Python Dictionary Sender Test
|
|
||||||
Tests the smartsend function with dictionary payloads
|
|
||||||
"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
|
|
||||||
# Add parent directory to path
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
from natbridge import smartsend, DEFAULT_BROKER_URL, DEFAULT_FILESERVER_URL
|
|
||||||
|
|
||||||
TEST_SUBJECT = '/test/dictionary'
|
|
||||||
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 Dictionary Sender Test ===\n')
|
|
||||||
|
|
||||||
correlation_id = 'py-dict-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 - various dictionary structures
|
|
||||||
test_data = [
|
|
||||||
('simple_dict', {'key1': 'value1', 'key2': 'value2'}, 'dictionary'),
|
|
||||||
('nested_dict', {'outer': {'inner': 'value', 'number': 42}}, 'dictionary'),
|
|
||||||
('array_dict', {'items': [1, 2, 3, 'four', 'five']}, 'dictionary'),
|
|
||||||
('mixed_dict', {'string': 'text', 'number': 123, 'boolean': True, 'null_val': None}, 'dictionary')
|
|
||||||
]
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Send the message
|
|
||||||
print('Sending dictionary 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-dict-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
|
|
||||||
|
|
||||||
if len(env['payloads']) != 4:
|
|
||||||
print(f'❌ Expected 4 payloads, got {len(env["payloads"])}')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct number of payloads')
|
|
||||||
|
|
||||||
# Test each payload
|
|
||||||
expected_datanames = ['simple_dict', 'nested_dict', 'array_dict', 'mixed_dict']
|
|
||||||
expected_types = ['dictionary', 'dictionary', 'dictionary', 'dictionary']
|
|
||||||
|
|
||||||
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')
|
|
||||||
|
|
||||||
# Decode and verify the data
|
|
||||||
import base64
|
|
||||||
decoded_data = json.loads(base64.b64decode(payload['data']).decode('utf8'))
|
|
||||||
original_data = test_data[i][1]
|
|
||||||
|
|
||||||
# Normalize for comparison (None vs null, True vs true, etc.)
|
|
||||||
if json.dumps(decoded_data, sort_keys=True) != json.dumps(original_data, sort_keys=True):
|
|
||||||
print(f'❌ Payload {i + 1}: Data integrity mismatch')
|
|
||||||
print(f' Expected: {json.dumps(original_data)}')
|
|
||||||
print(f' Got: {json.dumps(decoded_data)}')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print(f'✅ Payload {i + 1}: Data integrity verified')
|
|
||||||
|
|
||||||
print(f' Size: {payload["size"]} bytes\n')
|
|
||||||
|
|
||||||
# Test round-trip serialization
|
|
||||||
print('=== Round-trip Serialization Test ===')
|
|
||||||
round_trip_data = [
|
|
||||||
('roundtrip', {'test': 'data', 'numbers': [1, 2, 3], 'nested': {'a': 1, 'b': 2}}, 'dictionary')
|
|
||||||
]
|
|
||||||
|
|
||||||
rt_env, _ = await smartsend(
|
|
||||||
TEST_SUBJECT,
|
|
||||||
round_trip_data,
|
|
||||||
broker_url=TEST_BROKER_URL,
|
|
||||||
fileserver_url=TEST_FILESERVER_URL,
|
|
||||||
correlation_id='roundtrip-' + correlation_id,
|
|
||||||
is_publish=False
|
|
||||||
)
|
|
||||||
|
|
||||||
rt_payload = rt_env['payloads'][0]
|
|
||||||
rt_decoded = json.loads(base64.b64decode(rt_payload['data']).decode('utf8'))
|
|
||||||
|
|
||||||
if json.dumps(rt_decoded, sort_keys=True) == json.dumps(round_trip_data[0][1], sort_keys=True):
|
|
||||||
print('✅ Round-trip serialization successful')
|
|
||||||
else:
|
|
||||||
print('❌ Round-trip serialization failed')
|
|
||||||
passed = False
|
|
||||||
|
|
||||||
# Test JSON string output
|
|
||||||
print('\n=== JSON String Output Test ===')
|
|
||||||
try:
|
|
||||||
parsed = json.loads(env_json_str)
|
|
||||||
if parsed['correlation_id'] == env['correlation_id'] and \
|
|
||||||
len(parsed['payloads']) == len(env['payloads']):
|
|
||||||
print('✅ JSON string is valid and matches envelope')
|
|
||||||
else:
|
|
||||||
print('❌ JSON string does not match envelope')
|
|
||||||
passed = False
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
print(f'❌ JSON string is invalid: {e}')
|
|
||||||
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())
|
|
||||||
@@ -11,7 +11,7 @@ import base64
|
|||||||
# Add parent directory to path
|
# Add parent directory to path
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
from natbridge import smartsend, DEFAULT_BROKER_URL, DEFAULT_FILESERVER_URL
|
from natsbridge import smartsend, DEFAULT_BROKER_URL, DEFAULT_FILESERVER_URL
|
||||||
|
|
||||||
TEST_SUBJECT = '/test/mix'
|
TEST_SUBJECT = '/test/mix'
|
||||||
TEST_BROKER_URL = os.environ.get('NATS_URL', 'nats://localhost:4222')
|
TEST_BROKER_URL = os.environ.get('NATS_URL', 'nats://localhost:4222')
|
||||||
|
|||||||
@@ -1,167 +0,0 @@
|
|||||||
"""
|
|
||||||
Python Table Sender Test
|
|
||||||
Tests the smartsend function with table (Arrow IPC) payloads
|
|
||||||
"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Add parent directory to path
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
from natbridge import smartsend, DEFAULT_BROKER_URL, DEFAULT_FILESERVER_URL
|
|
||||||
|
|
||||||
TEST_SUBJECT = '/test/table'
|
|
||||||
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 Table Sender Test ===\n')
|
|
||||||
|
|
||||||
correlation_id = 'py-table-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 - pandas DataFrame
|
|
||||||
try:
|
|
||||||
import pandas as pd
|
|
||||||
table_data = pd.DataFrame({
|
|
||||||
'id': [1, 2, 3, 4, 5],
|
|
||||||
'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
|
|
||||||
'age': [30, 25, 35, 28, 32],
|
|
||||||
'active': [True, False, True, True, False]
|
|
||||||
})
|
|
||||||
table_available = True
|
|
||||||
except ImportError:
|
|
||||||
print('❌ pandas not available - skipping table tests')
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
test_data = [
|
|
||||||
('users_table', table_data, 'table')
|
|
||||||
]
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Send the message
|
|
||||||
print('Sending table payload...')
|
|
||||||
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-table-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
|
|
||||||
|
|
||||||
if len(env['payloads']) != 1:
|
|
||||||
print(f'❌ Expected 1 payload, got {len(env["payloads"])}')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct number of payloads')
|
|
||||||
|
|
||||||
payload = env['payloads'][0]
|
|
||||||
if payload['dataname'] != 'users_table':
|
|
||||||
print(f"❌ Expected dataname 'users_table', got '{payload['dataname']}'")
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct dataname')
|
|
||||||
|
|
||||||
if payload['payload_type'] != 'table':
|
|
||||||
print(f"❌ Expected payload_type 'table', got '{payload['payload_type']}'")
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct payload_type')
|
|
||||||
|
|
||||||
if payload['transport'] != 'direct':
|
|
||||||
print(f"❌ Expected transport 'direct', got '{payload['transport']}'")
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct transport')
|
|
||||||
|
|
||||||
if payload['encoding'] != 'base64':
|
|
||||||
print(f"❌ Expected encoding 'base64', got '{payload['encoding']}'")
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct encoding')
|
|
||||||
|
|
||||||
print(f'\nPayload size: {payload["size"]} bytes')
|
|
||||||
|
|
||||||
# Test with larger table
|
|
||||||
print('\n=== Larger Table Test ===')
|
|
||||||
large_table_data = pd.DataFrame({
|
|
||||||
'id': range(100),
|
|
||||||
'name': [f'User{i}' for i in range(100)],
|
|
||||||
'age': [20 + (i % 50) for i in range(100)],
|
|
||||||
'active': [i % 2 == 0 for i in range(100)]
|
|
||||||
})
|
|
||||||
|
|
||||||
large_test_data = [
|
|
||||||
('large_table', large_table_data, 'table')
|
|
||||||
]
|
|
||||||
|
|
||||||
large_env, _ = await smartsend(
|
|
||||||
TEST_SUBJECT,
|
|
||||||
large_test_data,
|
|
||||||
broker_url=TEST_BROKER_URL,
|
|
||||||
fileserver_url=TEST_FILESERVER_URL,
|
|
||||||
correlation_id='large-' + correlation_id,
|
|
||||||
is_publish=False
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(large_env['payloads']) == 1:
|
|
||||||
print('✅ Large table handled correctly')
|
|
||||||
print(f' Size: {large_env["payloads"][0]["size"]} bytes')
|
|
||||||
else:
|
|
||||||
print('❌ Large table handling failed')
|
|
||||||
passed = False
|
|
||||||
|
|
||||||
# Test JSON string output
|
|
||||||
print('\n=== JSON String Output Test ===')
|
|
||||||
import json
|
|
||||||
try:
|
|
||||||
parsed = json.loads(env_json_str)
|
|
||||||
if parsed['correlation_id'] == env['correlation_id'] and \
|
|
||||||
len(parsed['payloads']) == len(env['payloads']):
|
|
||||||
print('✅ JSON string is valid and matches envelope')
|
|
||||||
else:
|
|
||||||
print('❌ JSON string does not match envelope')
|
|
||||||
passed = False
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
print(f'❌ JSON string is invalid: {e}')
|
|
||||||
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())
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
"""
|
|
||||||
Python Text Receiver Test
|
|
||||||
Tests the smartreceive function with text payloads
|
|
||||||
"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
|
|
||||||
# Add parent directory to path
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
from natbridge import smartreceive, DEFAULT_BROKER_URL, DEFAULT_FILESERVER_URL
|
|
||||||
|
|
||||||
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 Text Receiver Test ===\n')
|
|
||||||
|
|
||||||
# Create a mock NATS message with text payload
|
|
||||||
test_text = 'Hello, NATSBridge! This is a test message.'
|
|
||||||
import base64
|
|
||||||
|
|
||||||
test_data = {
|
|
||||||
'correlation_id': 'py-receiver-test-' + str(asyncio.get_event_loop().time() * 1000000),
|
|
||||||
'msg_id': 'msg-' + str(asyncio.get_event_loop().time() * 1000000),
|
|
||||||
'timestamp': asyncio.get_event_loop().time().isoformat(),
|
|
||||||
'send_to': '/test/text',
|
|
||||||
'msg_purpose': 'test',
|
|
||||||
'sender_name': 'py-text-test',
|
|
||||||
'sender_id': 'sender-' + str(asyncio.get_event_loop().time() * 1000000),
|
|
||||||
'receiver_name': 'py-receiver',
|
|
||||||
'receiver_id': 'receiver-' + str(asyncio.get_event_loop().time() * 1000000),
|
|
||||||
'reply_to': '',
|
|
||||||
'reply_to_msg_id': '',
|
|
||||||
'broker_url': TEST_BROKER_URL,
|
|
||||||
'metadata': {},
|
|
||||||
'payloads': [
|
|
||||||
{
|
|
||||||
'id': 'payload-' + str(asyncio.get_event_loop().time() * 1000000),
|
|
||||||
'dataname': 'message',
|
|
||||||
'payload_type': 'text',
|
|
||||||
'transport': 'direct',
|
|
||||||
'encoding': 'base64',
|
|
||||||
'size': len(test_text.encode('utf8')),
|
|
||||||
'data': base64.b64encode(test_text.encode('utf8')).decode('ascii'),
|
|
||||||
'metadata': {'payload_bytes': len(test_text.encode('utf8'))}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
mock_msg = {
|
|
||||||
'payload': json.dumps(test_data)
|
|
||||||
}
|
|
||||||
|
|
||||||
print('Mock Message Created:')
|
|
||||||
print(f' Correlation ID: {test_data["correlation_id"]}')
|
|
||||||
print(f' Payloads: {len(test_data["payloads"])}')
|
|
||||||
print(f' Payload dataname: {test_data["payloads"][0]["dataname"]}')
|
|
||||||
print(f' Payload type: {test_data["payloads"][0]["payload_type"]}')
|
|
||||||
print(f' Transport: {test_data["payloads"][0]["transport"]}\n')
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Receive and process the message
|
|
||||||
print('Receiving and processing message...')
|
|
||||||
env = await smartreceive(
|
|
||||||
mock_msg,
|
|
||||||
max_retries=3,
|
|
||||||
base_delay=100,
|
|
||||||
max_delay=1000
|
|
||||||
)
|
|
||||||
|
|
||||||
print('\n=== Received Envelope ===')
|
|
||||||
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'Payloads: {len(env["payloads"])}\n')
|
|
||||||
|
|
||||||
# Validate received data
|
|
||||||
print('=== Validation ===')
|
|
||||||
passed = True
|
|
||||||
|
|
||||||
if not env.get('correlation_id'):
|
|
||||||
print('❌ correlation_id is missing')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ correlation_id present')
|
|
||||||
|
|
||||||
if len(env['payloads']) != 1:
|
|
||||||
print(f'❌ Expected 1 payload, got {len(env["payloads"])}')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct number of payloads')
|
|
||||||
|
|
||||||
payload = env['payloads'][0]
|
|
||||||
if payload[0] != 'message':
|
|
||||||
print(f"❌ Expected dataname 'message', got '{payload[0]}'")
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct dataname')
|
|
||||||
|
|
||||||
if payload[2] != 'text':
|
|
||||||
print(f"❌ Expected type 'text', got '{payload[2]}'")
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct type')
|
|
||||||
|
|
||||||
if payload[1] != test_text:
|
|
||||||
print('❌ Data mismatch')
|
|
||||||
print(f' Expected: {test_text}')
|
|
||||||
print(f' Got: {payload[1]}')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Data correctly deserialized')
|
|
||||||
|
|
||||||
# Test with multiple text payloads
|
|
||||||
print('\n=== Multiple Text Payloads Test ===')
|
|
||||||
multi_test_data = {
|
|
||||||
'correlation_id': 'multi-receiver-' + str(asyncio.get_event_loop().time() * 1000000),
|
|
||||||
'msg_id': 'msg-' + str(asyncio.get_event_loop().time() * 1000000),
|
|
||||||
'timestamp': asyncio.get_event_loop().time().isoformat(),
|
|
||||||
'send_to': '/test/text',
|
|
||||||
'msg_purpose': 'test',
|
|
||||||
'sender_name': 'py-text-test',
|
|
||||||
'sender_id': 'sender-' + str(asyncio.get_event_loop().time() * 1000000),
|
|
||||||
'receiver_name': 'py-receiver',
|
|
||||||
'receiver_id': 'receiver-' + str(asyncio.get_event_loop().time() * 1000000),
|
|
||||||
'reply_to': '',
|
|
||||||
'reply_to_msg_id': '',
|
|
||||||
'broker_url': TEST_BROKER_URL,
|
|
||||||
'metadata': {},
|
|
||||||
'payloads': [
|
|
||||||
{
|
|
||||||
'id': 'payload-1',
|
|
||||||
'dataname': 'msg1',
|
|
||||||
'payload_type': 'text',
|
|
||||||
'transport': 'direct',
|
|
||||||
'encoding': 'base64',
|
|
||||||
'size': 16,
|
|
||||||
'data': base64.b64encode(b'First message').decode('ascii'),
|
|
||||||
'metadata': {'payload_bytes': 16}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 'payload-2',
|
|
||||||
'dataname': 'msg2',
|
|
||||||
'payload_type': 'text',
|
|
||||||
'transport': 'direct',
|
|
||||||
'encoding': 'base64',
|
|
||||||
'size': 16,
|
|
||||||
'data': base64.b64encode(b'Second message').decode('ascii'),
|
|
||||||
'metadata': {'payload_bytes': 16}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 'payload-3',
|
|
||||||
'dataname': 'msg3',
|
|
||||||
'payload_type': 'text',
|
|
||||||
'transport': 'direct',
|
|
||||||
'encoding': 'base64',
|
|
||||||
'size': 16,
|
|
||||||
'data': base64.b64encode(b'Third message').decode('ascii'),
|
|
||||||
'metadata': {'payload_bytes': 16}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
mock_multi_msg = {'payload': json.dumps(multi_test_data)}
|
|
||||||
multi_env = await smartreceive(mock_multi_msg)
|
|
||||||
|
|
||||||
if len(multi_env['payloads']) == 3:
|
|
||||||
print('✅ Multiple payloads handled correctly')
|
|
||||||
|
|
||||||
# Verify each payload
|
|
||||||
expected_messages = ['First message', 'Second message', 'Third message']
|
|
||||||
for i in range(3):
|
|
||||||
if multi_env['payloads'][i][1] == expected_messages[i]:
|
|
||||||
print(f'✅ Payload {i + 1} correctly deserialized')
|
|
||||||
else:
|
|
||||||
print(f'❌ Payload {i + 1} mismatch')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print(f"❌ Expected 3 payloads, got {len(multi_env['payloads'])}")
|
|
||||||
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())
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
"""
|
|
||||||
Python Text Sender Test
|
|
||||||
Tests the smartsend function with text payloads
|
|
||||||
"""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Add parent directory to path
|
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
|
|
||||||
from natbridge import smartsend, DEFAULT_BROKER_URL, DEFAULT_FILESERVER_URL
|
|
||||||
|
|
||||||
TEST_SUBJECT = '/test/text'
|
|
||||||
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 Text Sender Test ===\n')
|
|
||||||
|
|
||||||
correlation_id = 'py-text-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
|
|
||||||
text_data = 'Hello, NATSBridge! This is a test message.'
|
|
||||||
test_data = [
|
|
||||||
('message', text_data, 'text')
|
|
||||||
]
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Send the message
|
|
||||||
print('Sending text payload...')
|
|
||||||
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-text-test',
|
|
||||||
is_publish=False # Don't actually publish for this test
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
if not env.get('correlation_id'):
|
|
||||||
print('❌ correlation_id is missing')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ correlation_id present')
|
|
||||||
|
|
||||||
if not env.get('msg_id'):
|
|
||||||
print('❌ msg_id is missing')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ msg_id present')
|
|
||||||
|
|
||||||
if not env.get('timestamp'):
|
|
||||||
print('❌ timestamp is missing')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ timestamp present')
|
|
||||||
|
|
||||||
if len(env['payloads']) != 1:
|
|
||||||
print(f'❌ Expected 1 payload, got {len(env["payloads"])}')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct number of payloads')
|
|
||||||
|
|
||||||
payload = env['payloads'][0]
|
|
||||||
if payload['dataname'] != 'message':
|
|
||||||
print(f"❌ Expected dataname 'message', got '{payload['dataname']}'")
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct dataname')
|
|
||||||
|
|
||||||
if payload['payload_type'] != 'text':
|
|
||||||
print(f"❌ Expected payload_type 'text', got '{payload['payload_type']}'")
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct payload_type')
|
|
||||||
|
|
||||||
if payload['transport'] != 'direct':
|
|
||||||
print(f"❌ Expected transport 'direct', got '{payload['transport']}'")
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct transport')
|
|
||||||
|
|
||||||
if payload['encoding'] != 'base64':
|
|
||||||
print(f"❌ Expected encoding 'base64', got '{payload['encoding']}'")
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Correct encoding')
|
|
||||||
|
|
||||||
# Decode and verify the data
|
|
||||||
import base64
|
|
||||||
decoded_data = base64.b64decode(payload['data']).decode('utf8')
|
|
||||||
if decoded_data != text_data:
|
|
||||||
print('❌ Decoded data mismatch')
|
|
||||||
print(f' Expected: {text_data}')
|
|
||||||
print(f' Got: {decoded_data}')
|
|
||||||
passed = False
|
|
||||||
else:
|
|
||||||
print('✅ Data integrity verified')
|
|
||||||
|
|
||||||
print(f"\nPayload size: {payload['size']} bytes")
|
|
||||||
print(f'Base64 data length: {len(payload["data"])} chars')
|
|
||||||
|
|
||||||
# Test with multiple text payloads
|
|
||||||
print('\n=== Multiple Text Payloads Test ===')
|
|
||||||
multi_test_data = [
|
|
||||||
('msg1', 'First message', 'text'),
|
|
||||||
('msg2', 'Second message', 'text'),
|
|
||||||
('msg3', 'Third message', 'text')
|
|
||||||
]
|
|
||||||
|
|
||||||
multi_env, _ = await smartsend(
|
|
||||||
TEST_SUBJECT,
|
|
||||||
multi_test_data,
|
|
||||||
broker_url=TEST_BROKER_URL,
|
|
||||||
fileserver_url=TEST_FILESERVER_URL,
|
|
||||||
correlation_id='multi-test-' + correlation_id,
|
|
||||||
is_publish=False
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(multi_env['payloads']) == 3:
|
|
||||||
print('✅ Multiple payloads handled correctly')
|
|
||||||
else:
|
|
||||||
print(f"❌ Expected 3 payloads, got {len(multi_env['payloads'])}")
|
|
||||||
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())
|
|
||||||
Reference in New Issue
Block a user