1255 lines
32 KiB
Markdown
1255 lines
32 KiB
Markdown
# msghandler - Cross-Platform Bi-Directional Data Bridge
|
|
|
|
A high-performance, bi-directional data bridge for **Julia**, **JavaScript**, **Python**, and **MicroPython** applications using NATS (Core & JetStream), implementing the Claim-Check pattern for large payloads.
|
|
|
|
[](https://opensource.org/licenses/MIT)
|
|
[](https://nats.io)
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
- [Overview](#overview)
|
|
- [Cross-Platform Support](#cross-platform-support)
|
|
- [Features](#features)
|
|
- [Quick Start](#quick-start)
|
|
- [API Reference](#api-reference)
|
|
- [Cross-Platform Examples](#cross-platform-examples)
|
|
- [Testing](#testing)
|
|
- [Documentation](#documentation)
|
|
- [License](#license)
|
|
|
|
---
|
|
|
|
## Quick Data Format Reference
|
|
|
|
All platforms use the same input format for `smartpack()`:
|
|
|
|
### Format: `[(dataname, data, type), ...]`
|
|
|
|
| Element | Type | Description |
|
|
|---------|------|-------------|
|
|
| `dataname` | String | Name for the payload (e.g., `"message"`, `"config.json"`) |
|
|
| `data` | Any | The actual data (type depends on the `type` parameter below) |
|
|
| `type` | String | Payload type identifier |
|
|
|
|
### Supported Payload Types
|
|
|
|
| Type | Julia Data | JavaScript Data | Python Data | Description |
|
|
|------|------------|-----------------|-------------|-------------|
|
|
| `"text"` | `String` | `string` | `str` | Plain text |
|
|
| `"dictionary"` | `Dict` | `Object` | `dict` | JSON object |
|
|
| `"arrowtable"` | `DataFrame` | ❌ | `pandas.DataFrame` | Arrow IPC table (Desktop only) |
|
|
| `"jsontable"` | `DataFrame` | `Array<Object>` | `list[dict]` | JSON table |
|
|
| `"image"` | `Vector{UInt8}` | `Uint8Array` | `bytes` | Image data |
|
|
| `"audio"` | `Vector{UInt8}` | `Uint8Array` | `bytes` | Audio data |
|
|
| `"video"` | `Vector{UInt8}` | `Uint8Array` | `bytes` | Video data |
|
|
| `"binary"` | `Vector{UInt8}` | `Uint8Array` | `bytes` | Binary data |
|
|
|
|
### Examples
|
|
|
|
**Sending multiple payloads:**
|
|
```julia
|
|
# Julia
|
|
data = [
|
|
("message", "Hello!", "text"),
|
|
("config", Dict("key" => "value"), "dictionary"),
|
|
("file", file_bytes, "binary")
|
|
]
|
|
env, json_str = msghandler.smartpack("subject", data, options...)
|
|
```
|
|
|
|
```python
|
|
# Python
|
|
data = [
|
|
("message", "Hello!", "text"),
|
|
("config", {"key": "value"}, "dictionary"),
|
|
("file", file_bytes, "binary")
|
|
]
|
|
env, json_str = await smartpack("subject", data, **options)
|
|
```
|
|
|
|
```javascript
|
|
// JavaScript
|
|
const data = [
|
|
["message", "Hello!", "text"],
|
|
["config", {key: "value"}, "dictionary"],
|
|
["file", fileBytes, "binary"]
|
|
];
|
|
const [env, json_str] = await smartpack("subject", data, options);
|
|
```
|
|
|
|
**Important Notes:**
|
|
- Always wrap payloads in a list `[]`, even for single payloads
|
|
- Type must be one of the supported types above
|
|
- Large payloads (≥500KB) automatically use link transport
|
|
- Browser only supports: `"text"`, `"dictionary"`, `"jsontable"`, `"image"`, `"audio"`, `"video"`, `"binary"`
|
|
(No Arrow IPC due to browser incompatibility)
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
msghandler enables seamless communication across multiple platforms through NATS, with intelligent transport selection based on payload size:
|
|
|
|
| Transport | Payload Size | Method |
|
|
|-----------|--------------|--------|
|
|
| **Direct** | < 500KB | Sent directly via NATS (Base64 encoded) |
|
|
| **Link** | ≥ 500KB | Uploaded to HTTP file server, URL sent via NATS |
|
|
|
|
### Use Cases
|
|
|
|
- **Chat Applications**: Text, images, audio, video in a single message
|
|
- **File Transfer**: Efficient transfer of large files using claim-check pattern
|
|
- **IoT/Embedded**: Sensor data, telemetry, and analytics pipelines (MicroPython)
|
|
- **Cross-Platform Communication**: Interoperability between Julia, JavaScript, Python, and MicroPython systems
|
|
|
|
---
|
|
|
|
## Cross-Platform Support
|
|
|
|
| Platform | Implementation | Features |
|
|
|----------|----------------|----------|
|
|
| **Julia** | [`src/msghandler.jl`](src/msghandler.jl) | Full feature set, Arrow IPC, multiple dispatch |
|
|
| **JavaScript (Node.js)** | [`src/msghandler_ssr.js`](src/msghandler_ssr.js) | Node.js, async/await, Arrow IPC |
|
|
| **JavaScript (Browser)** | [`src/msghandler_csr.js`](src/msghandler_csr.js) | Browser, WebSocket NATS, async/await, JSON table only |
|
|
| **Python** | [`src/msghandler.py`](src/msghandler.py) | Desktop Python, asyncio, type hints, Arrow IPC |
|
|
| **MicroPython** | [`src/msghandler_mpy.py`](src/msghandler_mpy.py) | Memory-constrained, synchronous API |
|
|
|
|
### Platform Comparison
|
|
|
|
| Feature | Julia | JavaScript | JavaScript (Browser) | Python | MicroPython |
|
|
|---------|-------|------------|----------------------|--------|-------------|
|
|
| Multiple Dispatch | ✅ Native | ❌ | ❌ | ❌ | ❌ |
|
|
| Async/Await | ❌ | ✅ Native | ✅ Native | ✅ Native | ⚠️ (uasyncio) |
|
|
| Type Safety | ✅ Strong | ⚠️ (TypeScript) | ⚠️ (TypeScript) | ✅ (Type hints) | ❌ |
|
|
| Arrow IPC | ✅ Native | ✅ Native | ❌ (Browser incompatible) | ✅ Native | ❌ |
|
|
| JSON Table | ✅ | ✅ | ✅ (Only table type) | ✅ | ⚠️ (Limited) |
|
|
| Direct Transport | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
| Link Transport | ✅ | ✅ | ✅ | ✅ | ⚠️ (Limited) |
|
|
| Handler Functions | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
| Cross-Platform API | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
| WebSocket NATS | ❌ | ❌ | ✅ | ❌ | ❌ |
|
|
|
|
---
|
|
|
|
## Features
|
|
|
|
- ✅ **Cross-platform messaging** for Julia, JavaScript, Python, and MicroPython applications
|
|
- ✅ **Bi-directional messaging** with request-reply patterns
|
|
- ✅ **Multi-payload support** - send multiple payloads with different types in one message
|
|
- ✅ **Automatic transport selection** - direct vs link based on payload size
|
|
- ✅ **Claim-Check pattern** for payloads ≥ 500KB
|
|
- ✅ **Apache Arrow IPC** support for tabular data (Desktop: Julia/Python/Node.js)
|
|
- ✅ **JSON Table** support for tabular data (All platforms including Browser)
|
|
- ✅ **Exponential backoff** for reliable file server downloads
|
|
- ✅ **Correlation ID tracking** for message tracing
|
|
- ✅ **Reply-to support** for request-response patterns
|
|
- ✅ **Handler function abstraction** - pluggable file server implementations (Plik, AWS S3, custom)
|
|
|
|
---
|
|
|
|
## Quick Start
|
|
|
|
### Prerequisites
|
|
|
|
1. **NATS Server** - Install and run a NATS server:
|
|
```bash
|
|
docker run -p 4222:4222 nats:latest
|
|
```
|
|
|
|
2. **HTTP File Server** (optional, for large payloads) - Install and run a file server:
|
|
```bash
|
|
# Using Plik
|
|
docker run -p 8080:8080 -v /tmp/fileserver:/var/lib/plik -e PLIK_ADMIN_PASSWORD=admin plik/plik
|
|
|
|
# OR using simple Python HTTP server
|
|
mkdir -p /tmp/fileserver
|
|
python3 -m http.server 8080 --directory /tmp/fileserver
|
|
```
|
|
|
|
### Send Your First Message
|
|
|
|
#### Julia
|
|
|
|
```julia
|
|
using msghandler, NATS
|
|
|
|
# Data format: [(dataname, data, type), ...]
|
|
# Each tuple contains:
|
|
# 1. dataname: String name for the payload (e.g., "message")
|
|
# 2. data: The actual data (String, bytes, DataFrame, etc.)
|
|
# 3. type: String type indicator (e.g., "text", "binary", "dictionary")
|
|
# Supported types: "text", "dictionary", "arrowtable", "jsontable",
|
|
# "image", "audio", "video", "binary"
|
|
|
|
payload_1 = ("test_message", "Hello World", "text")
|
|
|
|
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)
|
|
payload_2 = (filename_large_image, file_data_large_image, "binary")
|
|
|
|
payloads = [payload_1, payload_2] # List of tuples
|
|
envelope, envelope_json_str = msghandler.smartpack("test.topic",
|
|
payloads;
|
|
broker_url="nats.yiem.cc",
|
|
fileserver_url="http://192.168.88.104:8080")
|
|
conn = NATS.connect("nats.yiem.cc") # Create NATS connection
|
|
NATS.publish(conn, "test.topic", envelope_json_str) # Publish message to NATS
|
|
NATS.drain(conn)
|
|
```
|
|
|
|
#### JavaScript (Node.js)
|
|
|
|
```javascript
|
|
import msghandler from './src/msghandler_ssr.js';
|
|
|
|
const data = [["message", "Hello World", "text"]];
|
|
const [env, env_json_str] = await msghandler.smartpack(
|
|
"/chat/room1",
|
|
data,
|
|
{ broker_url: "nats://localhost:4222" }
|
|
);
|
|
console.log("Message sent!");
|
|
```
|
|
|
|
#### JavaScript (Browser)
|
|
|
|
```javascript
|
|
import msghandler from './src/msghandler_csr.js';
|
|
|
|
const data = [["message", "Hello World", "text"]];
|
|
const [env, env_json_str] = await msghandler.smartpack(
|
|
"/chat/room1",
|
|
data,
|
|
{ broker_url: "ws://localhost:4222" }
|
|
);
|
|
console.log("Message sent!");
|
|
```
|
|
|
|
#### Python
|
|
|
|
```python
|
|
from msghandler import smartpack
|
|
|
|
# Data format: [(dataname, data, type), ...]
|
|
# Each tuple contains:
|
|
# 1. dataname: String name for the payload (e.g., "message")
|
|
# 2. data: The actual data (str, bytes, dict, list, etc.)
|
|
# 3. type: String type indicator (e.g., "text", "binary", "dictionary")
|
|
# Supported types: "text", "dictionary", "jsontable", "image", "audio", "video", "binary"
|
|
|
|
data = [("message", "Hello World", "text")]
|
|
env, env_json_str = await smartpack(
|
|
"/chat/room1",
|
|
data,
|
|
broker_url="nats://localhost:4222"
|
|
)
|
|
print("Message sent!")
|
|
```
|
|
|
|
#### MicroPython
|
|
|
|
```python
|
|
from msghandler import smartpack
|
|
|
|
data = [("message", "Hello World", "text")]
|
|
env, env_json_str = smartpack(
|
|
"/chat/room1",
|
|
data,
|
|
broker_url="nats://localhost:4222",
|
|
size_threshold=100000
|
|
)
|
|
print("Message sent!")
|
|
```
|
|
|
|
### Receive Your First Message
|
|
|
|
#### Julia
|
|
|
|
```julia
|
|
using msghandler, NATS
|
|
|
|
conn = NATS.connect("nats.yiem.cc")
|
|
NATS.subscribe(conn, "test.topic") do msg
|
|
println("Received message on $(msg.subject)")
|
|
envelope_json_str = String(msg.payload)
|
|
|
|
envelope = msghandler.smartunpack(
|
|
envelope_json_str;
|
|
max_retries = 5,
|
|
base_delay = 100,
|
|
max_delay = 5000
|
|
)
|
|
println(envelope.payloads[1])
|
|
end
|
|
```
|
|
|
|
---
|
|
|
|
## API Reference
|
|
|
|
### Unified API Standard
|
|
|
|
All platforms use the same input/output format for payloads.
|
|
|
|
#### Input Format for `smartpack`
|
|
|
|
**Format:** `[(dataname1, data1, type1), (dataname2, data2, type2), ...]`
|
|
|
|
**Each tuple contains 3 elements:**
|
|
1. **`dataname`** (String) - Name for the payload (e.g., `"message"`, `"config.json"`, `"avatar.png"`)
|
|
2. **`data`** (Any) - The actual data to send (type depends on `type`):
|
|
- `text` → String
|
|
- `dictionary` → Dict/Object
|
|
- `arrowtable` → DataFrame/Arrow.Table
|
|
- `jsontable` → List of dicts/Vector{Dict}
|
|
- `image/audio/video/binary` → bytes/Uint8Array/Vector{UInt8}
|
|
3. **`type`** (String) - Payload type (required):
|
|
- `"text"` - Plain text string
|
|
- `"dictionary"` - JSON-serializable dictionary
|
|
- `"arrowtable"` - Apache Arrow IPC table (Desktop only)
|
|
- `"jsontable"` - JSON table (all platforms)
|
|
- `"image"` - Image data (PNG, JPG)
|
|
- `"audio"` - Audio data (WAV, MP3)
|
|
- `"video"` - Video data (MP4, AVI)
|
|
- `"binary"` - Generic binary data
|
|
|
|
**Important:**
|
|
- Always wrap payloads in a list, even for single payloads
|
|
- Type must be one of the supported types above
|
|
- Large payloads (≥500KB by default) automatically use link transport
|
|
|
|
**Example:**
|
|
```julia
|
|
# Julia
|
|
data = [
|
|
("message", "Hello World", "text"),
|
|
("config", Dict("key" => "value"), "dictionary"),
|
|
("file", file_bytes, "binary")
|
|
]
|
|
```
|
|
|
|
```python
|
|
# Python
|
|
data = [
|
|
("message", "Hello World", "text"),
|
|
("config", {"key": "value"}, "dictionary"),
|
|
("file", file_bytes, "binary")
|
|
]
|
|
```
|
|
|
|
```javascript
|
|
// JavaScript
|
|
const data = [
|
|
["message", "Hello World", "text"],
|
|
["config", {key: "value"}, "dictionary"],
|
|
["file", fileBytes, "binary"]
|
|
];
|
|
```
|
|
|
|
### Common Payload Examples
|
|
|
|
| Use Case | Data Format | Type |
|
|
|----------|-------------|------|
|
|
| Simple text message | `("message", "Hello World", "text")` | `"text"` |
|
|
| Configuration JSON | `("config", Dict("key" => "value"), "dictionary")` | `"dictionary"` |
|
|
| Table as Arrow | `("data", dataframe, "arrowtable")` | `"arrowtable"` |
|
|
| Table as JSON | `("data", [{"col": 1}], "jsontable")` | `"jsontable"` |
|
|
| Image file | `("avatar.png", read("file.png"), "image")` | `"image"` |
|
|
| Audio file | `("audio.mp3", audio_bytes, "audio")` | `"audio"` |
|
|
| Video file | `("video.mp4", video_bytes, "video")` | `"video"` |
|
|
| Binary file | `("data.bin", file_bytes, "binary")` | `"binary"` |
|
|
|
|
#### Output Format for `smartunpack`
|
|
```json
|
|
{
|
|
"correlation_id": "...",
|
|
"msg_id": "...",
|
|
"timestamp": "...",
|
|
"send_to": "...",
|
|
"msg_purpose": "...",
|
|
"sender_name": "...",
|
|
"sender_id": "...",
|
|
"receiver_name": "...",
|
|
"receiver_id": "...",
|
|
"reply_to": "...",
|
|
"reply_to_msg_id": "...",
|
|
"broker_url": "...",
|
|
"metadata": {...},
|
|
"payloads": [(dataname1, data1, type1), (dataname2, data2, type2), ...]
|
|
}
|
|
```
|
|
|
|
### smartpack
|
|
|
|
Sends data either directly via NATS or via a fileserver URL, depending on payload size.
|
|
|
|
#### Julia
|
|
|
|
```julia
|
|
using msghandler
|
|
|
|
env, env_json_str = msghandler.smartpack(
|
|
subject::String,
|
|
data::AbstractArray{Tuple{String, Any, String}};
|
|
broker_url::String = "nats://localhost:4222",
|
|
fileserver_url = "http://localhost:8080",
|
|
fileserver_upload_handler::Function = plik_oneshot_upload,
|
|
size_threshold::Int = 500_000,
|
|
correlation_id::String = string(uuid4()),
|
|
msg_purpose::String = "chat",
|
|
sender_name::String = "msghandler",
|
|
receiver_name::String = "",
|
|
receiver_id::String = "",
|
|
reply_to::String = "",
|
|
reply_to_msg_id::String = "",
|
|
is_publish::Bool = true,
|
|
NATS_connection::Union{NATS.Connection, Nothing} = nothing,
|
|
msg_id::String = string(uuid4()),
|
|
sender_id::String = string(uuid4())
|
|
)
|
|
# Returns: ::Tuple{msg_envelope_v1, String}
|
|
```
|
|
|
|
#### JavaScript (Node.js)
|
|
|
|
```javascript
|
|
import msghandler from './src/msghandler_ssr.js';
|
|
|
|
const [env, env_json_str] = await msghandler.smartpack(
|
|
subject,
|
|
data, // Array of [dataname, data, type] tuples
|
|
{
|
|
broker_url: 'nats://localhost:4222',
|
|
fileserver_url: 'http://localhost:8080',
|
|
fileserver_upload_handler: msghandler.plikOneshotUpload,
|
|
size_threshold: 500_000,
|
|
correlation_id: uuidv4(),
|
|
msg_purpose: 'chat',
|
|
sender_name: 'msghandler',
|
|
receiver_name: '',
|
|
receiver_id: '',
|
|
reply_to: '',
|
|
reply_to_msg_id: '',
|
|
is_publish: true,
|
|
nats_connection: null,
|
|
msg_id: uuidv4(),
|
|
sender_id: uuidv4()
|
|
}
|
|
);
|
|
// Returns: Promise<[env, env_json_str]>
|
|
```
|
|
|
|
#### JavaScript (Browser)
|
|
|
|
```javascript
|
|
import msghandler from './src/msghandler_csr.js';
|
|
|
|
const [env, env_json_str] = await msghandler.smartpack(
|
|
subject,
|
|
data,
|
|
{
|
|
broker_url: 'ws://localhost:4222',
|
|
fileserver_url: 'http://localhost:8080',
|
|
fileserver_upload_handler: msghandler.plikOneshotUpload,
|
|
size_threshold: 500_000,
|
|
correlation_id: uuidv4(),
|
|
msg_purpose: 'chat',
|
|
sender_name: 'msghandler',
|
|
receiver_name: '',
|
|
receiver_id: '',
|
|
reply_to: '',
|
|
reply_to_msg_id: '',
|
|
is_publish: true,
|
|
nats_connection: null,
|
|
msg_id: uuidv4(),
|
|
sender_id: uuidv4()
|
|
}
|
|
);
|
|
// Returns: Promise<[env, env_json_str]>
|
|
```
|
|
|
|
#### Python
|
|
|
|
```python
|
|
from msghandler import msghandler
|
|
|
|
env, env_json_str = await msghandler.smartpack(
|
|
subject: str,
|
|
data: List[Tuple[str, Any, str]],
|
|
broker_url: str = "nats://localhost:4222",
|
|
fileserver_url: str = "http://localhost:8080",
|
|
fileserver_upload_handler: Callable = plik_oneshot_upload,
|
|
size_threshold: int = 500_000,
|
|
correlation_id: str = None,
|
|
msg_purpose: str = "chat",
|
|
sender_name: str = "msghandler",
|
|
receiver_name: str = "",
|
|
receiver_id: str = "",
|
|
reply_to: str = "",
|
|
reply_to_msg_id: str = "",
|
|
is_publish: bool = True,
|
|
nats_connection: Any = None,
|
|
msg_id: str = None,
|
|
sender_id: str = None
|
|
)
|
|
# Returns: Tuple[Dict, str]
|
|
```
|
|
|
|
#### MicroPython
|
|
|
|
```python
|
|
from msghandler import smartpack
|
|
|
|
# Limited to direct transport (< 100KB threshold)
|
|
env, env_json_str = smartpack(
|
|
"/device/config",
|
|
[("config", config, "dictionary")],
|
|
broker_url="nats://localhost:4222",
|
|
size_threshold=100000
|
|
)
|
|
print("Message sent!")
|
|
```
|
|
|
|
### smartunpack
|
|
|
|
Receives and processes messages from NATS, handling both direct and link transport.
|
|
|
|
#### Julia
|
|
|
|
```julia
|
|
using msghandler
|
|
|
|
env = msghandler.smartunpack(
|
|
msg::NATS.Msg;
|
|
fileserver_download_handler::Function = _fetch_with_backoff,
|
|
max_retries::Int = 5,
|
|
base_delay::Int = 100,
|
|
max_delay::Int = 5000
|
|
)
|
|
# Returns: ::JSON.Object{String, Any}
|
|
```
|
|
|
|
#### JavaScript (Node.js)
|
|
|
|
```javascript
|
|
import msghandler from './src/msghandler_ssr.js';
|
|
|
|
const env = await msghandler.smartunpack(
|
|
msg,
|
|
{
|
|
fileserver_download_handler: msghandler.fetchWithBackoff,
|
|
max_retries: 5,
|
|
base_delay: 100,
|
|
max_delay: 5000
|
|
}
|
|
);
|
|
// Returns: Promise<env_object>
|
|
```
|
|
|
|
#### JavaScript (Browser)
|
|
|
|
```javascript
|
|
import msghandler from './src/msghandler_csr.js';
|
|
|
|
const env = await msghandler.smartunpack(
|
|
msg,
|
|
{
|
|
fileserver_download_handler: msghandler.fetchWithBackoff,
|
|
max_retries: 5,
|
|
base_delay: 100,
|
|
max_delay: 5000
|
|
}
|
|
);
|
|
// Returns: Promise<env_object>
|
|
```
|
|
|
|
#### Python
|
|
|
|
```python
|
|
from msghandler import msghandler
|
|
|
|
env = await msghandler.smartunpack(
|
|
msg,
|
|
fileserver_download_handler=fetch_with_backoff,
|
|
max_retries=5,
|
|
base_delay=100,
|
|
max_delay=5000
|
|
)
|
|
# Returns: Dict with "payloads" key
|
|
```
|
|
|
|
#### MicroPython
|
|
|
|
```python
|
|
from msghandler import smartunpack
|
|
|
|
env = smartunpack(
|
|
msg,
|
|
fileserver_download_handler=_sync_fileserver_download,
|
|
max_retries=3,
|
|
base_delay=100,
|
|
max_delay=1000
|
|
)
|
|
print("Received payloads:", env["payloads"])
|
|
```
|
|
|
|
---
|
|
|
|
## Cross-Platform Examples
|
|
|
|
### Example 1: Chat with Mixed Content
|
|
|
|
Send text, image, and large file in one message.
|
|
|
|
#### Julia
|
|
|
|
```julia
|
|
using msghandler
|
|
|
|
data = [
|
|
("message_text", "Hello!", "text"),
|
|
("user_avatar", image_data, "image"),
|
|
("large_document", large_file_data, "binary")
|
|
]
|
|
|
|
env, env_json_str = smartpack("/chat/room1", data; fileserver_url="http://localhost:8080")
|
|
```
|
|
|
|
#### JavaScript (Node.js)
|
|
|
|
```javascript
|
|
import msghandler from './src/msghandler_ssr.js';
|
|
|
|
const data = [
|
|
["message_text", "Hello!", "text"],
|
|
["user_avatar", imageData, "image"],
|
|
["large_document", largeFileData, "binary"]
|
|
];
|
|
|
|
const [env, env_json_str] = await msghandler.smartpack(
|
|
"/chat/room1",
|
|
data,
|
|
{ fileserver_url: 'http://localhost:8080' }
|
|
);
|
|
```
|
|
|
|
#### JavaScript (Browser)
|
|
|
|
```javascript
|
|
import msghandler from './src/msghandler_csr.js';
|
|
|
|
const data = [
|
|
["message_text", "Hello!", "text"],
|
|
["user_avatar", image_data, "image"]
|
|
];
|
|
|
|
const [env, env_json_str] = await msghandler.smartpack(
|
|
"/chat/room1",
|
|
data,
|
|
{ broker_url: 'ws://localhost:4222' }
|
|
);
|
|
console.log("Message sent!");
|
|
```
|
|
|
|
#### Python
|
|
|
|
```python
|
|
import asyncio
|
|
from msghandler import smartpack
|
|
|
|
async def main():
|
|
image_data = open("user_avatar.png", "rb").read()
|
|
large_file_data = open("large_document.pdf", "rb").read()
|
|
|
|
data = [
|
|
("message_text", "Hello!", "text"),
|
|
("user_avatar", image_data, "image"),
|
|
("large_document", large_file_data, "binary")
|
|
]
|
|
|
|
env, env_json_str = await smartpack(
|
|
"/chat/room1",
|
|
data,
|
|
fileserver_url="http://localhost:8080"
|
|
)
|
|
print("Message sent!")
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|
|
```
|
|
|
|
#### MicroPython
|
|
|
|
```python
|
|
from msghandler import smartpack
|
|
|
|
# Note: MicroPython only supports direct transport (< 100KB threshold)
|
|
# Large files must be uploaded via file server first
|
|
data = [
|
|
("message_text", "Hello!", "text"),
|
|
("user_avatar", image_bytes, "image")
|
|
]
|
|
|
|
env, env_json_str = smartpack(
|
|
"/chat/room1",
|
|
data,
|
|
broker_url="nats://localhost:4222",
|
|
size_threshold=100000
|
|
)
|
|
print("Message sent!")
|
|
```
|
|
|
|
### Example 2: Dictionary Exchange
|
|
|
|
Send configuration data between platforms.
|
|
|
|
#### Julia
|
|
|
|
```julia
|
|
using msghandler
|
|
|
|
config = Dict(
|
|
"wifi_ssid" => "MyNetwork",
|
|
"wifi_password" => "password123",
|
|
"update_interval" => 60
|
|
)
|
|
|
|
data = [("config", config, "dictionary")]
|
|
env, env_json_str = smartpack("/device/config", data)
|
|
```
|
|
|
|
#### JavaScript (Node.js)
|
|
|
|
```javascript
|
|
import msghandler from './src/msghandler_ssr.js';
|
|
|
|
const config = {
|
|
wifi_ssid: "MyNetwork",
|
|
wifi_password: "password123",
|
|
update_interval: 60
|
|
};
|
|
|
|
const [env, env_json_str] = await msghandler.smartpack(
|
|
"/device/config",
|
|
[["config", config, "dictionary"]]
|
|
);
|
|
console.log("Message sent!");
|
|
```
|
|
|
|
#### Python
|
|
|
|
```python
|
|
import asyncio
|
|
from msghandler import smartpack
|
|
|
|
async def main():
|
|
config = {
|
|
"wifi_ssid": "MyNetwork",
|
|
"wifi_password": "password123",
|
|
"update_interval": 60
|
|
}
|
|
|
|
data = [("config", config, "dictionary")]
|
|
env, env_json_str = await smartpack("/device/config", data)
|
|
print("Message sent!")
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|
|
```
|
|
|
|
#### MicroPython
|
|
|
|
```python
|
|
from msghandler import smartpack
|
|
|
|
config = {
|
|
"wifi_ssid": "MyNetwork",
|
|
"wifi_password": "password123",
|
|
"update_interval": 60
|
|
}
|
|
|
|
data = [("config", config, "dictionary")]
|
|
env, env_json_str = smartpack(
|
|
"/device/config",
|
|
data,
|
|
broker_url="nats://localhost:4222",
|
|
size_threshold=100000
|
|
)
|
|
print("Message sent!")
|
|
```
|
|
|
|
### Example 3: Table Data (Arrow IPC)
|
|
|
|
Send tabular data using Apache Arrow IPC format.
|
|
|
|
#### Julia
|
|
|
|
```julia
|
|
using msghandler
|
|
using DataFrames
|
|
|
|
df = DataFrame(
|
|
id = [1, 2, 3],
|
|
name = ["Alice", "Bob", "Charlie"],
|
|
score = [95, 88, 92]
|
|
)
|
|
|
|
data = [("students", df, "arrowtable")]
|
|
env, env_json_str = smartpack("/data/analysis", data)
|
|
```
|
|
|
|
#### JavaScript (Node.js)
|
|
|
|
```javascript
|
|
import msghandler from './src/msghandler_ssr.js';
|
|
|
|
const df = [
|
|
{ id: 1, name: "Alice", score: 95 },
|
|
{ id: 2, name: "Bob", score: 88 },
|
|
{ id: 3, name: "Charlie", score: 92 }
|
|
];
|
|
|
|
const [env, env_json_str] = await msghandler.smartpack(
|
|
"/data/analysis",
|
|
[["students", df, "arrowtable"]]
|
|
);
|
|
```
|
|
|
|
#### Python
|
|
|
|
```python
|
|
from msghandler import msghandler
|
|
import pandas as pd
|
|
|
|
df = pd.DataFrame({
|
|
"id": [1, 2, 3],
|
|
"name": ["Alice", "Bob", "Charlie"],
|
|
"score": [95, 88, 92]
|
|
})
|
|
|
|
data = [("students", df, "arrowtable")]
|
|
env, env_json_str = await msghandler.smartpack("/data/analysis", data)
|
|
```
|
|
|
|
#### JavaScript (Browser)
|
|
|
|
```javascript
|
|
import msghandler from './src/msghandler_csr.js';
|
|
|
|
const df = [
|
|
{ id: 1, name: "Alice", score: 95 },
|
|
{ id: 2, name: "Bob", score: 88 },
|
|
{ id: 3, name: "Charlie", score: 92 }
|
|
];
|
|
|
|
const [env, env_json_str] = await msghandler.smartpack(
|
|
"/data/analysis",
|
|
[["students", df, "jsontable"]],
|
|
{ broker_url: 'ws://localhost:4222' }
|
|
);
|
|
console.log("Message sent!");
|
|
```
|
|
|
|
#### MicroPython
|
|
|
|
```python
|
|
from msghandler import smartpack
|
|
|
|
# Note: MicroPython only supports direct transport (< 100KB threshold)
|
|
# MicroPython doesn't support Arrow IPC, only jsontable
|
|
df = [
|
|
{"id": 1, "name": "Alice", "score": 95},
|
|
{"id": 2, "name": "Bob", "score": 88},
|
|
{"id": 3, "name": "Charlie", "score": 92}
|
|
]
|
|
|
|
data = [("students", df, "jsontable")]
|
|
env, env_json_str = smartpack(
|
|
"/data/analysis",
|
|
data,
|
|
broker_url="nats://localhost:4222",
|
|
size_threshold=100000
|
|
)
|
|
print("Message sent!")
|
|
```
|
|
|
|
### Example 4: Request-Response Pattern
|
|
|
|
Bi-directional communication with reply-to support.
|
|
|
|
#### Julia
|
|
|
|
```julia
|
|
using msghandler, NATS
|
|
|
|
# Requester
|
|
env, env_json_str = msghandler.smartpack(
|
|
"/device/command",
|
|
[("command", Dict("action" => "read_sensor"), "dictionary")];
|
|
broker_url="nats://localhost:4222",
|
|
reply_to="/device/response"
|
|
)
|
|
|
|
conn = NATS.connect("nats://localhost:4222")
|
|
NATS.publish(conn, "/device/command", env_json_str)
|
|
NATS.drain(conn)
|
|
|
|
# Receiver (in separate application)
|
|
conn = NATS.connect("nats://localhost:4222")
|
|
NATS.subscribe(conn, "/device/command") do msg
|
|
env = msghandler.smartunpack(msg)
|
|
println("Received command: ", env["payloads"])
|
|
|
|
result = Dict("value" => 42)
|
|
response_env, response_json = msghandler.smartpack(
|
|
"/device/response",
|
|
[("result", result, "dictionary")],
|
|
reply_to="/device/command",
|
|
reply_to_msg_id=env["msg_id"]
|
|
)
|
|
|
|
NATS.publish(conn, "/device/response", response_json)
|
|
NATS.drain(conn)
|
|
end
|
|
```
|
|
|
|
#### JavaScript (Node.js)
|
|
|
|
```javascript
|
|
import msghandler from './src/msghandler_ssr.js';
|
|
import { connect } from 'nats';
|
|
|
|
// Requester
|
|
const [env, env_json_str] = await msghandler.smartpack(
|
|
"/device/command",
|
|
[["command", { action: "read_sensor" }, "dictionary"]],
|
|
{ broker_url: 'nats://localhost:4222', reply_to: '/device/response' }
|
|
);
|
|
const nc = await connect({ port: 4222 });
|
|
nc.publish("/device/command", env_json_str);
|
|
await nc.flush();
|
|
|
|
// Receiver (in separate application)
|
|
const nc = await connect({ port: 4222 });
|
|
const sub = nc.subscribe("/device/command");
|
|
for await (const msg of sub) {
|
|
const env = await msghandler.smartunpack(msg);
|
|
console.log("Received command:", env.payloads);
|
|
|
|
const response_env, response_json = await msghandler.smartpack(
|
|
"/device/response",
|
|
[["result", { value: 42 }, "dictionary"]],
|
|
{ reply_to: '/device/command', reply_to_msg_id: env.msg_id }
|
|
);
|
|
|
|
nc.publish("/device/response", response_json);
|
|
await nc.flush();
|
|
}
|
|
```
|
|
|
|
#### Python
|
|
|
|
```python
|
|
import asyncio
|
|
from msghandler import smartpack
|
|
|
|
async def main():
|
|
# Requester
|
|
env, env_json_str = await smartpack(
|
|
"/device/command",
|
|
[("command", {"action": "read_sensor"}, "dictionary")],
|
|
broker_url="nats://localhost:4222",
|
|
reply_to="/device/response"
|
|
)
|
|
print("Request sent!")
|
|
|
|
# Receiver (in separate application)
|
|
# await nats_consumer.next()
|
|
# env = await smartunpack(msg)
|
|
# Process request and send response
|
|
# response_env, response_json = await smartpack(
|
|
# "/device/response",
|
|
# [("result", {"value": 42}, "dictionary")],
|
|
# reply_to="/device/command",
|
|
# reply_to_msg_id=env["msg_id"]
|
|
# )
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|
|
```
|
|
|
|
#### JavaScript (Browser)
|
|
|
|
```javascript
|
|
import msghandler from './src/msghandler_csr.js';
|
|
|
|
// Requester
|
|
const [env, env_json_str] = await msghandler.smartpack(
|
|
"/device/command",
|
|
[["command", { action: "read_sensor" }, "dictionary"]],
|
|
{ broker_url: 'ws://localhost:4222', reply_to: '/device/response' }
|
|
);
|
|
console.log("Request sent!");
|
|
|
|
// Receiver (in separate application)
|
|
// const msg = await natsConsumer.next();
|
|
// const env = await msghandler.smartunpack(msg);
|
|
// console.log("Received command:", env.payloads);
|
|
// const response_env, response_json = await msghandler.smartpack(
|
|
// "/device/response",
|
|
// [["result", { value: 42 }, "dictionary"]],
|
|
// { reply_to: '/device/command', reply_to_msg_id: env.msg_id }
|
|
// );
|
|
```
|
|
|
|
#### MicroPython
|
|
|
|
```python
|
|
from msghandler import smartpack
|
|
|
|
# Requester
|
|
env, env_json_str = smartpack(
|
|
"/device/command",
|
|
[("command", {"action": "read_sensor"}, "dictionary")],
|
|
broker_url="nats://localhost:4222",
|
|
reply_to="/device/response",
|
|
size_threshold=100000
|
|
)
|
|
print("Request sent!")
|
|
```
|
|
|
|
---
|
|
|
|
## Testing
|
|
|
|
### Test File Organization
|
|
|
|
| Platform | Sender Tests | Receiver Tests |
|
|
|----------|--------------|----------------|
|
|
| **Julia** | `test/test_julia_*_sender.jl` | `test/test_julia_*_receiver.jl` |
|
|
| **JavaScript** | `test/test_js_*_sender.js` | `test/test_js_*_receiver.js` |
|
|
| **Python** | `test/test_py_*_sender.py` | `test/test_py_*_receiver.py` |
|
|
|
|
### Run Tests
|
|
|
|
#### Julia
|
|
|
|
```bash
|
|
# Text message exchange
|
|
julia test/test_julia_text_sender.jl
|
|
julia test/test_julia_text_receiver.jl
|
|
|
|
# Dictionary exchange
|
|
julia test/test_julia_dict_sender.jl
|
|
julia test/test_julia_dict_receiver.jl
|
|
|
|
# File transfer
|
|
julia test/test_julia_file_sender.jl
|
|
julia test/test_julia_file_receiver.jl
|
|
|
|
# Mixed payload types
|
|
julia test/test_julia_mix_payloads_sender.jl
|
|
julia test/test_julia_mix_payloads_receiver.jl
|
|
|
|
# Table exchange
|
|
julia test/test_julia_table_sender.jl
|
|
julia test/test_julia_table_receiver.jl
|
|
```
|
|
|
|
#### JavaScript (Node.js)
|
|
|
|
```bash
|
|
# Text message exchange
|
|
node test/test_js_text_sender.js
|
|
node test/test_js_text_receiver.js
|
|
|
|
# Dictionary exchange
|
|
node test/test_js_dictionary_sender.js
|
|
node test/test_js_dictionary_receiver.js
|
|
|
|
# Binary transfer
|
|
node test/test_js_binary_sender.js
|
|
node test/test_js_binary_receiver.js
|
|
|
|
# Table exchange
|
|
node test/test_js_table_sender.js
|
|
node test/test_js_table_receiver.js
|
|
```
|
|
|
|
#### Python
|
|
|
|
```bash
|
|
# Text message exchange
|
|
python3 test/test_py_text_sender.py
|
|
python3 test/test_py_text_receiver.py
|
|
|
|
# Dictionary exchange
|
|
python3 test/test_py_dictionary_sender.py
|
|
python3 test/test_py_dictionary_receiver.py
|
|
|
|
# Binary transfer
|
|
python3 test/test_py_binary_sender.py
|
|
python3 test/test_py_binary_receiver.py
|
|
|
|
# Table exchange
|
|
python3 test/test_py_table_sender.py
|
|
python3 test/test_py_table_receiver.py
|
|
```
|
|
|
|
---
|
|
|
|
## Browser Deployment
|
|
|
|
### Using with Node.js Build Tools
|
|
|
|
The browser implementation (`src/msghandler_csr.js`) can be bundled for production deployment using modern JavaScript build tools.
|
|
|
|
#### Prerequisites
|
|
|
|
```bash
|
|
# Install the browser-compatible NATS client
|
|
npm install nats.ws
|
|
```
|
|
|
|
#### Vite (Recommended)
|
|
|
|
```bash
|
|
npm create vite@latest my-app -- --template vanilla
|
|
cd my-app
|
|
npm install nats.ws
|
|
```
|
|
|
|
In `vite.config.js`:
|
|
```javascript
|
|
import { defineConfig } from 'vite';
|
|
export default defineConfig({
|
|
resolve: {
|
|
alias: {
|
|
'nats.ws': 'nats.ws/dist/esm/browser.js'
|
|
}
|
|
}
|
|
});
|
|
```
|
|
|
|
Build command:
|
|
```bash
|
|
npm run build # Outputs to dist/ folder
|
|
```
|
|
|
|
#### Webpack
|
|
|
|
```bash
|
|
npm install webpack webpack-cli --save-dev
|
|
npm install nats.ws
|
|
```
|
|
|
|
In `webpack.config.js`:
|
|
```javascript
|
|
module.exports = {
|
|
entry: './src/index.js',
|
|
output: {
|
|
filename: 'bundle.js',
|
|
path: __dirname + '/dist'
|
|
},
|
|
resolve: {
|
|
alias: {
|
|
'nats.ws': 'nats.ws/dist/esm/browser.js'
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
Build command:
|
|
```bash
|
|
npx webpack
|
|
```
|
|
|
|
#### esbuild (Simple & Fast)
|
|
|
|
```bash
|
|
npm install esbuild nats.ws --save-dev
|
|
```
|
|
|
|
Create `build.js`:
|
|
```javascript
|
|
import esbuild from 'esbuild';
|
|
|
|
esbuild.buildSync({
|
|
entryPoints: ['src/msghandler_csr.js'],
|
|
bundle: true,
|
|
outfile: 'dist/msghandler-csr-bundle.js',
|
|
format: 'esm',
|
|
platform: 'browser',
|
|
target: 'es2020'
|
|
});
|
|
```
|
|
|
|
Build command:
|
|
```bash
|
|
node build.js
|
|
```
|
|
|
|
### Using in Your HTML
|
|
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>My App</title>
|
|
</head>
|
|
<body>
|
|
<script type="module" src="dist/msghandler-csr-bundle.js"></script>
|
|
<script type="module">
|
|
import msghandlerCSR from './dist/msghandler-csr-bundle.js';
|
|
|
|
// Use the library
|
|
const [env, envJson] = await msghandlerCSR.smartpack(
|
|
"/chat/user/v1/message",
|
|
[["msg", "Hello", "text"]],
|
|
{ broker_url: "wss://nats.example.com" }
|
|
);
|
|
</script>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
---
|
|
|
|
## Documentation
|
|
|
|
For detailed architecture and implementation information, see:
|
|
|
|
- [`docs/architecture.md`](docs/architecture.md) - Cross-platform architecture, API parity, platform-specific patterns
|
|
- [`docs/requirements.md`](docs/requirements.md) - Business requirements and user stories
|
|
- [`docs/spec.md`](docs/spec.md) - Technical specification and contracts
|
|
- [`docs/walkthrough.md`](docs/walkthrough.md) - Real-world application building guides
|
|
|
|
---
|
|
|
|
## License
|
|
|
|
MIT License
|
|
|
|
Copyright (c) 2026 msghandler Contributors
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in all
|
|
copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
SOFTWARE.
|