# msghandler - Cross-Platform Communication Layer

A high-performance, **transport-agnostic** communication layer for both **Julia** (backend) and **JavaScript** (CSR webapp) applications. Implements the Claim-Check pattern for efficient payload transport (direct for small payloads, URL-based for large payloads).

- 🟢 **Julia Backend**: Full-featured implementation with Arrow IPC support
- 🔵 **JavaScript Frontend**: Browser-compatible implementation for CSR webapps

[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)

---

## Table of Contents

- [Quick Start](#quick-start)
  - [Julia Backend](#julia-backend)
  - [JavaScript CSR Webapp](#javascript-csr-webapp)
- [Overview](#overview)
- [Features](#features)
- [Installation](#installation)
- [API Reference](#api-reference)
- [Examples](#examples)
- [Testing](#testing)
- [Documentation](#documentation)
- [License](#license)

---

## Quick Data Format Reference

### 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 | Description |
|------|------------|-------------|
| `"text"` | `String` | Plain text |
| `"dictionary"` | `Dict` | JSON object |
| `"arrowtable"` | `DataFrame`, `Arrow.Table` | Arrow IPC table |
| `"jsontable"` | `DataFrame`, `Vector{Dict}`, `Vector{NamedTuple}` | JSON table |
| `"image"` | `Vector{UInt8}` | Image data |
| `"audio"` | `Vector{UInt8}` | Audio data |
| `"video"` | `Vector{UInt8}` | Video data |
| `"binary"` | `Vector{UInt8}`, `IOBuffer` | Binary data |

### Message Envelope Structure (JSON) with Multiple Payloads

After calling `smartpack()`, the data is serialized into a JSON message envelope with the following structure:

```json
{
  "correlation_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "msg_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "timestamp": "2026-05-25T08:12:40.000Z",
  "send_to": "/chat/room1",
  "msg_purpose": "chat",
  "sender_name": "msghandler",
  "sender_id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
  "receiver_name": "",
  "receiver_id": "",
  "reply_to": "",
  "reply_to_msg_id": "",
  "broker_url": "nats.yiem.cc",
  "metadata": {},
  "payloads": [
    {
      "id": "d4e5f6a7-b8c9-0123-defa-234567890123",
      "dataname": "message",
      "payload_type": "text",
      "transport": "direct",
      "encoding": "base64",
      "size": 6,
      "data": "SGVsbG8h",
      "metadata": {}
    },
    {
      "id": "e5f6a7b8-c9d0-1234-efab-345678901234",
      "dataname": "config",
      "payload_type": "dictionary",
      "transport": "direct",
      "encoding": "base64",
      "size": 18,
      "data": "eyJrZXkiOiJ2YWx1ZSJ9",
      "metadata": {}
    },
    {
      "id": "f6a7b8c9-d0e1-2345-fabc-456789012345",
      "dataname": "small_file",
      "payload_type": "binary",
      "transport": "direct",
      "encoding": "base64",
      "size": 1024,
      "data": "base64encodedbinarydata...",
      "metadata": {}
    },
    {
      "id": "a7b8c9d0-e1f2-3456-abcd-567890123456",
      "dataname": "large_image",
      "payload_type": "image",
      "transport": "link",
      "encoding": "none",
      "size": 2048576,
      "data": "http://localhost:8080/file/ABC12/XYZ99/image.png",
      "metadata": {}
    }
  ]
}
```

#### Payload Transport Types

| Transport | Data Format | Use Case |
|-----------|-------------|----------|
| `direct` | Base64-encoded string | Payloads < 500KB sent directly |
| `link` | HTTP URL | Payloads ≥ 500KB uploaded to file server |

#### Payload Encoding Types

| Encoding | Payload Types |
|----------|---------------|
| `base64` | text, image, audio, video, binary |
| `json` | dictionary, jsontable |
| `arrow-ipc` | arrowtable |
| `none` | link transport (URL only) |

#### Payload Field Definitions

| Field | Type | Description |
|-------|------|-------------|
| `id` | `string` (UUID) | Unique payload identifier |
| `dataname` | `string` | Name/filename of the payload |
| `payload_type` | `string` | Type: text, dictionary, arrowtable, jsontable, image, audio, video, binary |
| `transport` | `string` | `direct` or `link` |
| `encoding` | `string` | base64, json, arrow-ipc, or none |
| `size` | `integer` | Size in bytes |
| `data` | `string` | Base64 string (direct) or URL (link) |
| `metadata` | `object` | Optional metadata |

#### Example: Multiple Payloads with Mixed Types

```julia
using msghandler

data = [
    ("message", "Hello!", "text"),
    ("config", Dict("key" => "value"), "dictionary"),
    ("small_file", file_bytes, "binary"),
    ("large_image", image_bytes, "image")
]

message_envelope, message_envelope_json_str = msghandler.smartpack("/chat/room1", data; 
    broker_url="nats.yiem.cc",
    fileserver_url="http://localhost:8080"
)

# message_envelope_json_str contains the JSON message_envelope shown above
# Payloads < 500KB use 'transport': 'direct' with Base64 data
# Payloads ≥ 500KB use 'transport': 'link' with URL to file server
```

### Examples

**Sending multiple payloads:**
```julia
data = [
    ("message", "Hello!", "text"),
    ("config", Dict("key" => "value"), "dictionary"),
    ("small_file", file_bytes, "binary"),
    ("large_image", image_bytes, "image")
]
message_envelope, message_envelope_json_str = msghandler.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

---

## Overview

msghandler provides a **transport-agnostic** communication layer with intelligent payload transport selection (same API for Julia and JavaScript):

| Transport | Payload Size | Method |
|-----------|--------------|--------|
| **Direct** | < 500KB | Sent directly via chosen transport (Base64 encoded) |
| **Link** | ≥ 500KB | Uploaded to HTTP file server, URL sent via chosen transport |

### Use Cases

- **Chat Applications**: Text, images, audio, video in a single message
- **File Transfer**: Efficient transfer of large files using claim-check pattern
- **Cross-Platform Communication**: Interoperability between Julia backend and JavaScript frontend
- **Any Transport**: Works with NATS, HTTP, WebSockets, WebRTC, or any custom transport mechanism

### Platform Differences

| Feature | Julia Backend | JavaScript Frontend |
|---------|---------------|---------------------|
| **Arrow IPC** | ✅ Supported | ❌ Not supported (use "jsontable" instead) |
| **Full Type Support** | ✅ All 8 types | ✅ 7 of 8 types |
| **Transport Support** | ✅ NATS, HTTP, WebSockets, etc. | ✅ Fetch, WebSockets, HTTP |
| **File Server** | ✅ Plik, AWS S3, custom | ✅ Plik, custom HTTP server |

### Supported Payload Types

| Type | Julia | JavaScript | Description |
|------|-------|------------|-------------|
| `"text"` | ✅ | ✅ | Plain text |
| `"dictionary"` | ✅ | ✅ | JSON object |
| `"arrowtable"` | ✅ | ❌ | Arrow IPC table (Julia only) |
| `"jsontable"` | ✅ | ✅ | JSON table |
| `"image"` | ✅ | ✅ | Image data |
| `"audio"` | ✅ | ✅ | Audio data |
| `"video"` | ✅ | ✅ | Video data |
| `"binary"` | ✅ | ✅ | Binary data |

### Key Design Principles

- **Transport Agnostic**: Core API (`smartpack`/`smartunpack`) is decoupled from transport
- **Claim-Check Pattern**: Efficient handling of large payloads via URL references
- **Rich Type System**: Support for Arrow IPC, JSON tables, and binary data
- **Handler Abstraction**: Pluggable file server implementations (Plik, AWS S3, custom)

---

## Features

- ✅ **Cross-platform** - Same API for Julia backend and JavaScript frontend
- ✅ **Transport-agnostic** - Core API works with any communication channel (NATS, HTTP, WebSockets, etc.)
- ✅ **Bi-directional** - Request-reply patterns with `reply_to` support
- ✅ **Multi-payload** - Send multiple payloads of different types in one message
- ✅ **Automatic transport selection** - Direct vs link based on payload size (threshold: 500KB)
- ✅ **Claim-Check pattern** - Efficient large payload handling via URL references
- ✅ **Rich payload types** - Text, dictionary, Arrow IPC (Julia only), JSON table, image, audio, video, binary
- ✅ **Exponential backoff** - Reliable file server downloads with retry logic
- ✅ **Correlation ID** - End-to-end message tracing
- ✅ **Handler abstraction** - Pluggable file server implementations (Plik, AWS S3, custom)

---

## Installation

### Julia Backend

```julia
using Pkg
Pkg.add("msghandler")
```

Add to your `Project.toml`:
```toml
[deps]
msghandler = "path-to-msghandler"
```

### JavaScript Frontend

**Using NPM:**
```bash
npm install msghandler-csr
```

**Using local source (development):**
```bash
cp ./src/msghandler-csr.js ./public/js/msghandler.js
```

Then import in your browser:
```javascript
import msghandlerCSR from './js/msghandler.js';
```

---

## Quick Start

### Prerequisites

1. **Transport Channel** - Choose your communication channel:
   - **NATS** (recommended): `docker run -p 4222:4222 nats:latest`
   - **HTTP/WebSocket**: Any HTTP server or WebSocket endpoint
   - **Custom**: Implement your own transport mechanism

2. **HTTP File Server** (optional, for payloads ≥ 500KB) - Install and run:
    ```bash
    # Using Plik
    docker run -p 8080:8080 -v /tmp/fileserver:/var/lib/plik -e PLIK_ADMIN_PASSWORD=admin plik/plik
    
    # OR using Python HTTP server
    mkdir -p /tmp/fileserver
    python3 -m http.server 8080 --directory /tmp/fileserver
    ```

### Julia Backend

#### Installation

```julia
using Pkg
Pkg.add("msghandler")
```

Add to your `Project.toml`:
```toml
[deps]
msghandler = "path-to-msghandler"
```

#### Send Messages (Publish - Subscribe Pattern)

```julia
using msghandler

# Data format: [(dataname, data, type), ...]
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

# Step 1: Create the message envelope (transport-agnostic)
message_envelope, message_envelope_json_str = msghandler.smartpack("test.topic", 
                                                  payloads; 
                                                  broker_url="nats.yiem.cc",
                                                  fileserver_url="http://192.168.88.104:8080")

# Step 2: Send via your chosen transport (NATS in this example)
using NATS
conn = NATS.connect("nats.yiem.cc")
NATS.publish(conn, "test.topic", message_envelope_json_str; reply_to="test.replytopic")
NATS.drain(conn)

# OR using request-reply pattern
reply = NATS.request(conn, "test.topic", message_envelope_json_str; timeout=10)
```

#### Receive Messages (Publish - Subscribe Pattern)

```julia
using msghandler, NATS

conn = NATS.connect("nats.yiem.cc")
NATS.subscribe(conn, "test.topic") do msg
    println("Received message on $(msg.subject)")
    message_envelope_json_str = String(msg.payload)
    
    # Step 2: Unpack the message_envelope (transport-agnostic)
    message_envelope = msghandler.smartunpack(
        message_envelope_json_str;
        max_retries = 5,
        base_delay = 100,
        max_delay = 5000
    )
    println(message_envelope.payloads[1])
end
```

#### --- OR ---
#### Send Messages (Request - Reply Pattern)

```julia
using msghandler

# Data format: [(dataname, data, type), ...]
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

# Step 1: Create the message envelope (transport-agnostic)
message_envelope, message_envelope_json_str = msghandler.smartpack("test.topic", 
                                                  payloads; 
                                                  broker_url="nats.yiem.cc",
                                                  fileserver_url="http://192.168.88.104:8080")

# Step 2: Send via your chosen transport (NATS in this example)
using NATS
conn = NATS.connect("nats.yiem.cc")
reply = NATS.request(conn, "test.topic", message_envelope_json_str, timeout=10)
```

#### Receive Messages (Request - Reply Pattern)

```julia
using msghandler, NATS

conn = NATS.connect("nats.yiem.cc")
sub1 = NATS.reply(conn, "nats.yiem.cc"; queue_group="group1", spawn=true) do msg
                  message_envelope_json_str = String(msg.payload)
                  message_envelope = msghandler.smartunpack(message_envelope_json_str)
                  response = [("respond_message", "msg received", "text")]
                  _, response_message_envelope_json_str = msghandler.smartpack("requester inbox", 
                                                  response; 
                                                  broker_url="nats.yiem.cc",
                                                  fileserver_url="http://192.168.88.104:8080")
                  return response_message_envelope_json_str
              end
```

### JavaScript CSR Webapp

#### Installation

**Using NPM:**
```bash
npm install msghandler-csr
```

**Using ES Modules (browser):**
```javascript
import msghandlerCSR from './src/msghandler-csr.js';
```

#### Features

- ✅ No build tools required (works directly in browser)
- ✅ Uses native browser APIs (Web Crypto, Fetch, TextEncoder/Decoder)
- ✅ Browser-compatible payload types (no Arrow IPC dependency)

#### Send Messages

```javascript
import msghandlerCSR from './src/msghandler-csr.js';

const { smartpack, smartunpack, plikOneshotUpload, fetchWithBackoff } = msghandlerCSR;

// Data format: [[dataname, data, type], ...]
const payload1 = ["test_message", "Hello World", "text"];
const payload2 = ["config_data", { key: "value" }, "dictionary"];
const payload3 = ["table_data", [{id: 1, name: "Alice"}, {id: 2, name: "Ton"}], "jsontable"];
const file_data = fs.readFileSync('./test/large_image.png');  // Read image from file
const payload4 = ["user_avatar", file_data, "binary"];

const payloads = [payload1, payload2, payload3, payload4];  // Array of arrays

// Step 1: Create the message envelope (transport-agnostic)
const [message_envelope, message_envelope_json_str] = await msghandlerCSR.smartpack("test.topic", payloads, {
    broker_url: "nats.yiem.cc",
    fileserver_url: "http://192.168.88.104:8080"
});

// Step 2: Send via your chosen transport (NATS, WebSocket, HTTP, etc.)
// await myTransport.publish("test.topic", message_envelope_json_str);

// OR using fetch API for HTTP transport
fetch("http://localhost:3000/api/messages", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: message_envelope_json_str
});
```

#### Receive Messages

```javascript
import msghandlerCSR from './src/msghandler-csr.js';

const { smartunpack } = msghandlerCSR;

// Option 1: Direct JSON string
const message_envelope = await msghandlerCSR.smartunpack(jsonString, {
    fileserver_download_handler: msghandlerCSR.fetchWithBackoff,
    max_retries: 5,
    base_delay: 100,
    max_delay: 5000
});

// Option 2: From transport message object (e.g., NATS, WebSocket)
const message_envelope = await msghandlerCSR.smartunpack(natsMessage, {
    fileserver_download_handler: msghandlerCSR.fetchWithBackoff
});

// Process payloads
for (const [dataname, data, type] of message_envelope.payloads) {
    console.log(`${dataname}:`, data, `(type: ${type})`);
}
```
---

## API Reference

### smartpack

Sends data via your chosen transport mechanism with intelligent transport selection.

```julia
using msghandler

message_envelope, message_envelope_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 = "",
    msg_id::String = string(uuid4()),
    sender_id::String = string(uuid4())
)
# Returns: ::Tuple{msg_envelope_v1, String}
```

### smartunpack

Receives and processes messages from your transport.

```julia
using msghandler

message_envelope = msghandler.smartunpack(
    msg_json_str::String;
    fileserver_download_handler::Function = _fetch_with_backoff,
    max_retries::Int = 5,
    base_delay::Int = 100,
    max_delay::Int = 5000
)
# Returns: ::JSON.Object{String, Any}
```

---

## Examples

### Example 1: Chat with Mixed Content (Julia)

Send text, image, and large file in one message.

```julia
using msghandler

data = [
    ("message_text", "Hello!", "text"),
    ("user_avatar", image_data, "image"),
    ("large_document", large_file_data, "binary")
]

message_envelope, message_envelope_json_str = smartpack("/chat/room1", data; fileserver_url="http://localhost:8080")
```

### Example 2: Dictionary Exchange (Julia)

Send configuration data.

```julia
using msghandler

config = Dict(
    "wifi_ssid" => "MyNetwork",
    "wifi_password" => "password123",
    "update_interval" => 60
)

data = [("config", config, "dictionary")]
message_envelope, message_envelope_json_str = smartpack("/device/config", data)
```

### Example 3: Table Data (Arrow IPC - Julia Only)

Send tabular data using Apache Arrow IPC format for efficient binary serialization:

```julia
using msghandler
using DataFrames

df = DataFrame(
    id = [1, 2, 3],
    name = ["Alice", "Bob", "Charlie"],
    score = [95, 88, 92]
)

data = [("students", df, "arrowtable")]
message_envelope, message_envelope_json_str = smartpack("/data/analysis", data)
```

### Example 3b: Table Data (JSON - Julia Compatible)

For cross-platform compatibility or when Arrow IPC is not needed, use `jsontable`:

```julia
using msghandler
using DataFrames

df = DataFrame(
    id = [1, 2, 3],
    name = ["Alice", "Bob", "Charlie"],
    score = [95, 88, 92]
)

data = [("students", df, "jsontable")]
message_envelope, message_envelope_json_str = smartpack("/data/analysis", data)
```

### Example 3c: Table Data (JSON - JavaScript Compatible)

For JavaScript frontend, use `jsontable` instead of `arrowtable`:

```javascript
const tableData = [
    { id: 1, name: "Alice", score: 95 },
    { id: 2, name: "Bob", score: 88 },
    { id: 3, name: "Charlie", score: 92 }
];

const [message_envelope, message_envelope_json_str] = await msghandlerCSR.smartpack("/data/analysis", [
    ["students", tableData, "jsontable"]
]);
```

### Example 4: Image Transmission (Julia)

Send image data directly in messages:

```julia
using msghandler

# Read image file
image_path = "./test/large_image.png"
image_data = read(image_path)

data = [("user_avatar", image_data, "binary")]
message_envelope, message_envelope_json_str = smartpack("/chat/room1", data; fileserver_url="http://localhost:8080")
```

### Example 5: Image Transmission (JavaScript)

Send image data directly in messages:

```javascript
import fs from 'fs';

const file_data = fs.readFileSync('./test/large_image.png');

const [message_envelope, message_envelope_json_str] = await msghandlerCSR.smartpack("/chat/room1", [
    ["user_avatar", file_data, "binary"]
]);
```

### Example 6: Table Data (JavaScript)

For JavaScript frontend, use `jsontable` for tabular data:

```javascript
const tableData = [
    { id: 1, name: "Alice", score: 95 },
    { id: 2, name: "Bob", score: 88 },
    { id: 3, name: "Charlie", score: 92 }
];

const [message_envelope, message_envelope_json_str] = await msghandlerCSR.smartpack("/data/analysis", [
    ["students", tableData, "jsontable"]
]);
```

### Example 7: Publish-Subscribe Pattern (Julia) 

Bi-directional communication.

```julia
using msghandler, NATS

# Requester
message_envelope, message_envelope_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", message_envelope_json_str)
NATS.drain(conn)

# Receiver (in separate application)
conn = NATS.connect("nats://localhost:4222")
NATS.subscribe(conn, "/device/command") do msg
    message_envelope = msghandler.smartunpack(msg)
    println("Received command: ", message_envelope["payloads"])
    
    result = Dict("value" => 42)
    response_message_envelope, response_message_envelope_json_str = msghandler.smartpack(
        "/device/response",
        [("result", result, "dictionary")],
        reply_to="/device/command",
        reply_to_msg_id=message_envelope["msg_id"]
    )
    
    NATS.publish(conn, "/device/response", response_message_envelope_json_str)
    NATS.drain(conn)
end
```

---

## Testing

### Julia Backend

```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 Frontend

To test the JavaScript version in a browser:

1. Start a file server for testing:
```bash
mkdir -p /tmp/fileserver
python3 -m http.server 8080 --directory /tmp/fileserver
```

2. Start a NATS server (optional):
```bash
docker run -p 4222:4222 nats:latest
```

3. Open the test HTML file in a browser:
```bash
# Create test file (example)
cat > test-browser.html << 'EOF'
<!DOCTYPE html>
<html>
<head><title>msghandler CSR Test</title></head>
<body>
<script type="module">
import msghandlerCSR from './src/msghandler-csr.js';

// Test smartpack
const [message_envelope, message_envelope_json_str] = await msghandlerCSR.smartpack("/test", [
    ["msg", "Hello Browser!", "text"]
]);
console.log("Sent:", message_envelope_json_str);
</script>
</body>
</html>
EOF

python3 -m http.server 8081
# Open http://localhost:8081/test-browser.html
```

---

## Documentation

For detailed architecture and implementation information, see:

- [`docs/architecture.md`](docs/architecture.md) - Architecture and design 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

### JavaScript API Reference

The JavaScript version provides the same core functionality as Julia, with browser-compatible APIs:

#### smartpack

```javascript
const [message_envelope, message_envelope_json_str] = await msghandlerCSR.smartpack(
    subject,
    data,  // [[dataname, data, type], ...]
    options
);
```

#### smartunpack

```javascript
const message_envelope = await msghandlerCSR.smartunpack(msg, options);
// message_envelope.payloads = [[dataname, data, type], ...]
```

#### plikOneshotUpload

```javascript
const result = await msghandlerCSR.plikOneshotUpload(
    fileServerUrl,
    dataname,
    data  // Uint8Array
);
// result = { status, uploadid, fileid, url }
```

#### fetchWithBackoff

```javascript
const data = await msghandlerCSR.fetchWithBackoff(
    url,
    maxRetries,
    baseDelay,
    maxDelay,
    correlationId
);
// data = Uint8Array
```

---

## 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.
