update js client
This commit is contained in:
@@ -0,0 +1,737 @@
|
||||
# Walkthrough: msghandler
|
||||
|
||||
**Version**: 1.7.0
|
||||
**Date**: 2026-09-16
|
||||
**Status**: Active
|
||||
**Ground Truth**: [`src/msghandler.jl`](../src/msghandler.jl)
|
||||
**ASG Framework Alignment**: Eight pillars — Requirements → Solution Design → Walkthrough → Specification → UI Specification → Validation → Implementation Plan → Runbook
|
||||
|
||||
---
|
||||
|
||||
## Structural Guidance
|
||||
|
||||
This walkthrough follows the ASG Framework walkthrough guideline: one section per system **participant**, each tracing that participant's complete journey from session start to task completion. Every major step carries a `WT-XXX` id and cites the `SD-XXX` decision(s) and `FR-XXX` requirement(s) it implements. Where a participant's action triggers or depends on another participant, a cross-reference is included.
|
||||
|
||||
- **Overview sequence diagram** — participant section headers each start with the participant's high-level flow.
|
||||
- **Detailed sequence diagrams** — per scenario, showing request/response and transport selection.
|
||||
- **State machine** — the direct→link transport decision (multi-participant state).
|
||||
- **Error paths** — each participant section covers the failure modes that participant sees.
|
||||
|
||||
**Participants**: JavaScript (browser/frontend), Julia (backend — ground truth), Python (backend), Rust (service), MicroPython (IoT device).
|
||||
|
||||
---
|
||||
|
||||
## 0. Overview: The Big Picture (all participants)
|
||||
|
||||
msghandler delivers two tiny, symmetric functions that **every participant calls the same way**: `smartpack()` (send) and `smartunpack()` (receive). A developer passes a list of `(dataname, data, type)` tuples and receives them back on the other side with types intact, without ever choosing how the data moves. Per payload, msghandler decides whether to send it inline (direct) or hand it to a file server (link).
|
||||
|
||||
```
|
||||
Sender (smartpack) Transport Layer Receiver (smartunpack)
|
||||
┌─────────────────┐ ┌───────────────┐ ┌───────────────────┐
|
||||
│ 1. Data tuples │────────────>│ │───────────>│ 1. Parse envelope │
|
||||
│ [(name, │ JSON │ Message │ JSON │ 2. Check transport│
|
||||
│ data, type)]│ format │ Broker │ format │ 3. Fetch/Decode │
|
||||
└─────────────────┘ │ (NATS/MQTT/ │ │ 4. Return tuples │
|
||||
│ WebSocket) │ └───────────────────┘
|
||||
└───────────────┘
|
||||
```
|
||||
|
||||
### 0.1 Transport Layer Pattern (shared across all participants)
|
||||
|
||||
**Critical**: msghandler is **transport-agnostic**. The caller is responsible for:
|
||||
|
||||
1. **Sending side**: call `smartpack()` → receive `(envelope, json_string)` → publish the JSON string via transport
|
||||
2. **Receiving side**: subscribe to transport → receive JSON string → call `smartunpack()`
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph msghandler["msghandler Module (all participants)"]
|
||||
direction TB
|
||||
S1["smartpack(data)"]
|
||||
S2["Returns (envelope, json_string)"]
|
||||
R1["smartunpack(json_string)"]
|
||||
R2["Returns payloads (tuples)"]
|
||||
end
|
||||
|
||||
subgraph Transport["Transport Layer (Caller's Responsibility)"]
|
||||
direction TB
|
||||
PUBLISH["Publish JSON string"]
|
||||
SUBSCRIBE["Subscribe to messages"]
|
||||
end
|
||||
|
||||
S1 --> S2
|
||||
S2 --> PUBLISH
|
||||
SUBSCRIBE --> R1
|
||||
R1 --> R2
|
||||
PUBLISH --> SUBSCRIBE
|
||||
|
||||
style msghandler fill:#b3e5fc,stroke:#0288d1
|
||||
style Transport fill:#ffe0b2,stroke:#f57c00
|
||||
```
|
||||
|
||||
### 0.2 Per-Payload Transport Decision (state machine — spans all participants)
|
||||
|
||||
`smartpack()` serializes each payload first, then compares the serialized size against `size_threshold`. This per-payload decision is the single state transition shared by every participant.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Serialize: smartpack((name, data, type))
|
||||
Serialize --> Direct: size < size_threshold (base64 inline)
|
||||
Serialize --> Link: size >= size_threshold (upload to file server)
|
||||
Direct --> Envelope: encoding = base64/json/arrow-ipc
|
||||
Link --> Envelope: encoding = none/json/arrow-ipc, data = URL
|
||||
Envelope --> Publish: envelope_to_json() -> caller publishes
|
||||
Publish --> Transport: JSON string over broker
|
||||
Transport --> Unpack: smartunpack(json_string)
|
||||
Unpack --> DirectData: direct -> decode + deserialize
|
||||
Unpack --> LinkData: link -> download + deserialize
|
||||
DirectData --> Return: (name, data, type)
|
||||
LinkData --> Return: (name, data, type)
|
||||
Return --> [*]
|
||||
```
|
||||
|
||||
**Key Design Principles** (from `2-solution_design.md`):
|
||||
|
||||
| Principle | Description | Rationale |
|
||||
|-----------|-------------|-----------|
|
||||
| **Claim-Check Pattern** | Large payloads uploaded to HTTP server, URL sent via transport | Transport has message size limits; avoids overflow (`SD-001`) |
|
||||
| **Automatic Transport Selection** | Direct (< threshold) vs Link (≥ threshold) based on size | Optimizes memory vs network I/O (`SD-002`) |
|
||||
| **Unified Tuple Format** | Same `(dataname, data, type)` format across all platforms | Platform-agnostic I/O (`SD-004`) |
|
||||
| **Base64 Encoding** | JSON-compatible binary data transport | Cross-transport compatibility (`SD-005`) |
|
||||
| **Handler Abstraction** | Pluggable file server (Plik / AWS S3 / custom) | No vendor lock-in (`SD-003`) |
|
||||
| **Transport Abstraction** | Works over NATS / MQTT / WebSocket | No caller changes (`SD-006`) |
|
||||
| **Exponential Backoff** | Retry failed downloads | Handles transient failures (`SD-007`) |
|
||||
| **Correlation ID Propagation** | One id threads through every step | Distributed tracing (`SD-008`) |
|
||||
|
||||
**Supported Transports**: NATS (WebSocket/TCP), MQTT (WebSocket/TCP), WebSocket (direct HTTP), HTTP (polling/long-polling). msghandler only handles **serialization/deserialization**; the caller owns transport.
|
||||
|
||||
**Supported Payload Types** (`FR-006`, `FR-007`): `text`, `dictionary`, `arrowtable`, `jsontable`, `image`, `audio`, `video`, `binary`.
|
||||
|
||||
---
|
||||
|
||||
## 1. JavaScript (Browser / Frontend)
|
||||
|
||||
This section traces the journey from the browser's point of view: it starts a session, sends mixed content (text + image), receives a reply, and handles a large-file upload via link transport.
|
||||
|
||||
> Cross-references: the browser's request is consumed by the **Julia** backend (see [Section 2](#2-julia-backend-ground-truth)) and the browser's reply is produced there. The browser also broadcasts to other clients (see [Section 4](#4-rust-service) / [Section 3](#3-python-backend) receive the same topic).
|
||||
|
||||
### 1.1 Session Start
|
||||
|
||||
**WT-001**: The browser loads the frontend module and establishes a transport connection to the broker.
|
||||
|
||||
> Cross-reference: `2-solution_design.md:SD-006` (transport abstraction over NATS/WebSocket).
|
||||
|
||||
**Sequence**:
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User as Browser (JS)
|
||||
participant JS as msghandler.js
|
||||
participant T as Transport (NATS/WebSocket)
|
||||
|
||||
User->>JS: load frontend
|
||||
JS->>T: connect({ servers: "ws://localhost:4222" })
|
||||
T-->>JS: connected
|
||||
JS->>T: subscribe("/agent/wine/api/v1/response")
|
||||
T-->>JS: subscription active
|
||||
```
|
||||
|
||||
### 1.2 Send Mixed Payloads (Chat)
|
||||
|
||||
**WT-002**: The browser calls `smartpack()` with a mixed payload (text message + avatar image).
|
||||
|
||||
```javascript
|
||||
// JavaScript (Browser) - WT-002
|
||||
const [env, msgJson] = await msghandler.smartpack(
|
||||
"/agent/wine/api/v1/prompt",
|
||||
[
|
||||
["msg", "Hello! I'm Ton.", "text"],
|
||||
["avatar", avatarImageData, "image"]
|
||||
],
|
||||
{
|
||||
broker_url: "ws://localhost:4222",
|
||||
receiver_name: "agent-backend",
|
||||
msg_purpose: "chat"
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
> Traceability: `FR-006` (multi-payload), `FR-007` (type preservation), `FR-001` (cross-platform text), `SD-004` (unified tuple format), `SD-005` (base64), `SD-008` (correlation id auto-generated).
|
||||
|
||||
**WT-002.1**: For each payload, msghandler serializes and selects transport by size.
|
||||
|
||||
| Payload | Size | Transport | Encoding | Traceability |
|
||||
|---------|------|-----------|----------|--------------|
|
||||
| `"msg"` (text) | ~20 bytes | direct | base64 | `SD-002`, `SD-005` |
|
||||
| `"avatar"` (image) | ~2 KB | direct | base64 | `SD-002`, `SD-005` |
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User as Browser
|
||||
participant JS as msghandler.js
|
||||
participant E as Envelope
|
||||
|
||||
User->>JS: smartpack([msg, avatar])
|
||||
JS->>JS: _serialize_data() per payload (SD-004)
|
||||
JS->>JS: size < threshold? (SD-002)
|
||||
JS->>E: build envelope (SD-005, SD-008)
|
||||
E-->>JS: (env, msgJson)
|
||||
```
|
||||
|
||||
**Rationale**: Text is smaller and sent via direct transport (fast, no file server). The avatar stays direct because it is under the threshold. `smartpack()` returns `(env, msgJson)` — **the browser must publish** (`FR-013`).
|
||||
|
||||
**WT-003**: The browser publishes the JSON string via the transport.
|
||||
|
||||
```javascript
|
||||
// JavaScript (Browser) - WT-003
|
||||
const conn = await transportClient.connect({ servers: "ws://localhost:4222" });
|
||||
await conn.publish("/agent/wine/api/v1/prompt", msgJson);
|
||||
```
|
||||
|
||||
> Cross-reference: this publish triggers the **Julia** backend (see [Section 2, WT-010](#2-julia-backend-ground-truth)). `SD-006`.
|
||||
|
||||
### 1.3 Envelope Shape (what crosses the wire)
|
||||
|
||||
**WT-004**: The published envelope matches the ground-truth `msg_envelope_v1` JSON shape in [`src/msghandler.jl`](../src/msghandler.jl:274).
|
||||
|
||||
```json
|
||||
{
|
||||
"correlation_id": "a1b2c3d4...",
|
||||
"msg_id": "e5f6g7h8...",
|
||||
"timestamp": "2026-09-16T02:08:27.000Z",
|
||||
"send_to": "/agent/wine/api/v1/prompt",
|
||||
"msg_purpose": "chat",
|
||||
"sender_name": "browser-frontend",
|
||||
"sender_id": "browser-uuid...",
|
||||
"receiver_name": "agent-backend",
|
||||
"receiver_id": "",
|
||||
"reply_to": "/agent/wine/api/v1/response",
|
||||
"reply_to_msg_id": "",
|
||||
"broker_url": "ws://localhost:4222",
|
||||
"metadata": {},
|
||||
"payloads": [
|
||||
{
|
||||
"id": "payload-uuid...",
|
||||
"dataname": "msg",
|
||||
"payload_type": "text",
|
||||
"transport": "direct",
|
||||
"encoding": "base64",
|
||||
"size": 20,
|
||||
"data": "SGVsbG8hIEknIHRlbCB5b3UgSW4gZW5nbGlzaC4=",
|
||||
"metadata": { "payload_bytes": 20 }
|
||||
},
|
||||
{
|
||||
"id": "payload-uuid...",
|
||||
"dataname": "avatar",
|
||||
"payload_type": "image",
|
||||
"transport": "direct",
|
||||
"encoding": "base64",
|
||||
"size": 2048,
|
||||
"data": "iVBORw0KGgo=",
|
||||
"metadata": { "payload_bytes": 2048 }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> Traceability: `FR-011` (correlation id), `FR-013` (publish), `SD-008`, `SD-005`.
|
||||
|
||||
### 1.4 Receive Reply
|
||||
|
||||
**WT-005**: The browser receives the backend reply on its `reply_to` topic and deserializes payloads.
|
||||
|
||||
```javascript
|
||||
// JavaScript (Browser) - WT-005
|
||||
const conn = await transportClient.connect({ servers: "ws://localhost:4222" });
|
||||
conn.subscribe("/agent/wine/api/v1/response", async (msg) => {
|
||||
const payloads = await msghandler.smartunpack(String(msg.payload));
|
||||
// payloads = [
|
||||
// ["response", "Hello Ton! I'm the AI assistant.", "text"],
|
||||
// ["generated_image", binaryData, "image"]
|
||||
// ];
|
||||
renderResponse(payloads);
|
||||
});
|
||||
```
|
||||
|
||||
> Traceability: `FR-014` (subscription), `FR-007` (type preservation), `SD-004`, `SD-007`.
|
||||
|
||||
### 1.5 Error Paths (browser POV)
|
||||
|
||||
**WT-006**: Large file (≥ threshold) triggers link transport; upload fails.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User as Browser
|
||||
participant JS as msghandler.js
|
||||
participant FS as Plik File Server
|
||||
participant T as Transport
|
||||
|
||||
User->>JS: smartpack([dataset], size_threshold)
|
||||
JS->>JS: size >= threshold -> link (SD-001)
|
||||
JS->>FS: plik_oneshot_upload()
|
||||
FS-->>JS: 500 Upload Failed
|
||||
JS->>JS: throw "Failed to upload data to fileserver"
|
||||
User->>User: surface upload error / fall back to smaller payloads
|
||||
```
|
||||
|
||||
> Traceability: `FR-003` (large file handling), `FR-008` (Plik), `SD-001`, `SD-003`. Recovery: fall back to a smaller payload or direct transport.
|
||||
|
||||
**WT-007**: Transport connection lost before publish — the caller's transport client handles auto-reconnect; msghandler has already returned `(env, msgJson)`, so the caller retries the publish.
|
||||
|
||||
> Traceability: `NFR-203` (connection recovery).
|
||||
|
||||
---
|
||||
|
||||
## 2. Julia (Backend — Ground Truth)
|
||||
|
||||
This section traces the Julia backend, which is the reference implementation in [`src/msghandler.jl`](../src/msghandler.jl). It receives the browser's chat message, processes it, sends a reply, and downloads a large file via link transport.
|
||||
|
||||
> Cross-references: consumes the browser publish (see [Section 1, WT-003](#13-envelope-shape-what-crosses-the-wire)); produces the browser reply (see [Section 1, WT-005](#15-receive-reply)). Also the topic is broadcast to **Python** and **Rust** subscribers (see [Section 3](#3-python-backend), [Section 4](#4-rust-service)).
|
||||
|
||||
### 2.1 Session Start
|
||||
|
||||
**WT-010**: The Julia backend subscribes to the topic and is ready to `smartunpack()`.
|
||||
|
||||
> Cross-reference: `2-solution_design.md:SD-006` (transport abstraction).
|
||||
|
||||
### 2.2 Receive & Unpack Chat Message
|
||||
|
||||
**WT-011**: The backend receives the JSON string from the transport and calls `smartunpack()`.
|
||||
|
||||
```julia
|
||||
# Julia (Backend) - WT-011
|
||||
transport_msg = transport_subscription.next()
|
||||
env = smartunpack(String(transport_msg.payload))
|
||||
|
||||
# env["payloads"] is now a Vector of tuples:
|
||||
# [("msg", "Hello! I'm Ton.", "text"),
|
||||
# ("avatar", UInt8[...], "image")]
|
||||
```
|
||||
|
||||
> Traceability: `FR-014` (subscription), `FR-001` (text), `FR-006` (multi-payload), `FR-007` (type preservation), `SD-004`, `SD-007`.
|
||||
|
||||
**WT-011.1**: On the direct path, `smartunpack()` base64-decodes each payload and deserializes by type (`_deserialize_data`).
|
||||
|
||||
```julia
|
||||
# _deserialize_data (src/msghandler.jl:940)
|
||||
# text -> String(data)
|
||||
# dictionary-> JSON.parse(json_str)
|
||||
# arrowtable-> Arrow.Table(io) -> DataFrame
|
||||
# image/audio/video/binary -> data (bytes)
|
||||
```
|
||||
|
||||
> Traceability: `SD-004`, `SD-005`.
|
||||
|
||||
### 2.3 Send Reply
|
||||
|
||||
**WT-012**: The backend processes the message and builds a reply with `smartpack()`.
|
||||
|
||||
```julia
|
||||
# Julia (Backend) - WT-012
|
||||
response_text = "Hello Ton! I'm the AI assistant."
|
||||
generated_image = generate_ai_image(response_text)
|
||||
|
||||
env, msg_json = smartpack(
|
||||
"/agent/wine/api/v1/response",
|
||||
(
|
||||
("response", response_text, "text"),
|
||||
("generated_image", generated_image, "image"),
|
||||
),
|
||||
reply_to = "/chat/user/v1/message",
|
||||
reply_to_msg_id = env["msg_id"]
|
||||
)
|
||||
publish(transport_conn, "/agent/wine/api/v1/response", msg_json)
|
||||
```
|
||||
|
||||
> Traceability: `FR-001` (reply text), `FR-013` (publish — caller responsible), `FR-011` (`reply_to_msg_id` links the reply), `SD-004`, `SD-005`, `SD-008`.
|
||||
|
||||
**Rationale**: `reply_to_msg_id = env["msg_id"]` correlates this reply to the original request (`SD-008`). `smartpack()` returns `(env, env_json_str)` — **the backend must publish** (`FR-013`).
|
||||
|
||||
### 2.4 Receive Large File (Link Transport)
|
||||
|
||||
**WT-013**: A client uploads a 10 MB file; msghandler downloads it with exponential backoff.
|
||||
|
||||
```julia
|
||||
# Julia (Backend) - WT-013
|
||||
transport_msg = transport_subscription.next()
|
||||
env = smartunpack(String(transport_msg.payload))
|
||||
# The "dataset" payload has transport="link"; smartunpack():
|
||||
# 1. extracts the URL from payload["data"]
|
||||
# 2. downloads with exponential backoff (_fetch_with_backoff)
|
||||
# 3. deserializes (binary -> bytes)
|
||||
```
|
||||
|
||||
> Traceability: `FR-003` (large file handling), `FR-008` (Plik), `FR-010` (backoff retry), `SD-001`, `SD-002`, `SD-003`, `SD-007`.
|
||||
|
||||
**State (link path)** — from the backend's POV, per-payload decision `SD-002`:
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> InspectEnvelope: smartunpack(json_string)
|
||||
InspectEnvelope --> Direct: transport == "direct"
|
||||
InspectEnvelope --> Link: transport == "link"
|
||||
Direct --> Deserialize: base64decode + _deserialize_data
|
||||
Link --> Download: fileserver_download_handler (SD-007)
|
||||
Download --> Deserialize: download_failed? retry w/ backoff
|
||||
Deserialize --> Return: push (dataname, data, type)
|
||||
Return --> [*]
|
||||
```
|
||||
|
||||
### 2.5 Error Paths (Julia POV)
|
||||
|
||||
**WT-014**: File server download fails after `max_retries` (`FR-010`, `SD-007`).
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant BJ as Julia Backend
|
||||
participant D as _fetch_with_backoff
|
||||
participant FS as Plik File Server
|
||||
|
||||
BJ->>D: fetch(url, 5, 100, 5000)
|
||||
FS-->>D: 503 / connection refused
|
||||
D->>D: sleep(backoff), double delay (SD-007)
|
||||
loop up to max_retries (5)
|
||||
D->>FS: retry
|
||||
FS-->>D: still failing
|
||||
end
|
||||
D->>BJ: error("Failed to fetch data after 5 attempts")
|
||||
BJ->>BJ: log_trace(correlation_id, "download retry exceeded")
|
||||
```
|
||||
|
||||
> Traceability: `FR-010` (backoff retry), `NFR-202` (file server availability), `NFR-404` (alerting). Recovery: caller retries with a new upload; correlation id preserves traceability (`SD-008`).
|
||||
|
||||
**WT-015**: Unknown payload type throws `error("Unknown payload_type: $payload_type")` (`FR-007`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Python (Backend)
|
||||
|
||||
This section traces the Python backend: it receives tabular data (DataFrame), analyzes it, and sends results back. Python supports `arrowtable` (desktop) and `dictionary` types.
|
||||
|
||||
> Cross-references: Python may publish to a topic that **Julia** and **Rust** also subscribe to (see [Section 2](#2-julia-backend-ground-truth), [Section 4](#4-rust-service)).
|
||||
|
||||
### 3.1 Session Start
|
||||
|
||||
**WT-020**: The Python backend imports `msghandler` and connects its transport consumer.
|
||||
|
||||
### 3.2 Send Tabular Data (Round-Trip)
|
||||
|
||||
**WT-021**: Python sends a DataFrame as `arrowtable`.
|
||||
|
||||
```python
|
||||
# Python - WT-021
|
||||
import pandas as pd
|
||||
from msghandler import smartpack
|
||||
|
||||
df = pd.DataFrame({"id": [1, 2, 3], "name": ["Alice", "Bob", "Charlie"], "score": [95, 88, 92]})
|
||||
env, msg_json = smartpack(
|
||||
"/agent/wine/api/v1/analyze",
|
||||
[("data", df, "arrowtable")],
|
||||
broker_url="ws://localhost:4222",
|
||||
receiver_name="agent-backend"
|
||||
)
|
||||
await transport_publisher.publish("/agent/wine/api/v1/analyze", msg_json)
|
||||
```
|
||||
|
||||
> Traceability: `FR-002` (tabular data), `FR-012` (serialization), `SD-004`, `SD-005`, `SD-006`.
|
||||
|
||||
**WT-021.1**: Serialization to Arrow IPC preserves column types; the payload is `< threshold` so it travels direct with `encoding = "arrow-ipc"`.
|
||||
|
||||
> Traceability: `SD-002`, `SD-005`.
|
||||
|
||||
### 3.3 Receive Results
|
||||
|
||||
**WT-022**: Python receives the processed results and deserializes back to a DataFrame.
|
||||
|
||||
```python
|
||||
# Python - WT-022
|
||||
transport_msg = await transport_consumer.next()
|
||||
env = await smartunpack(str(transport_msg.payload))
|
||||
# env["payloads"] is now:
|
||||
# [("results", <DataFrame>, "arrowtable"),
|
||||
# ("summary", "Analysis complete: 1500 rows processed", "text")]
|
||||
```
|
||||
|
||||
> Traceability: `FR-014`, `FR-002`, `FR-007`, `SD-004`, `SD-007`.
|
||||
|
||||
### 3.4 Error Paths (Python POV)
|
||||
|
||||
**WT-023**: Large `arrowtable` (≥ threshold) uses link transport; upload fails.
|
||||
|
||||
> Traceability: `FR-003`, `SD-001`, `SD-003`. Recovery: retry with a smaller chunk or direct transport.
|
||||
|
||||
**WT-024**: `arrowtable` sent but `pyarrow` not installed → `_serialize_data` raises `RuntimeError` (no Arrow support).
|
||||
|
||||
> Traceability: `FR-002`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Rust (Service)
|
||||
|
||||
This section traces the Rust service, which leverages compile-time type safety via enums and `serde` for serialization, and a tokio async runtime for transport and HTTP. Rust supports the full feature set including `arrowtable` and link transport.
|
||||
|
||||
> Cross-references: Rust subscribes to the same topic consumed by **Julia** and **Python** (see [Section 2](#2-julia-backend-ground-truth), [Section 3](#3-python-backend)).
|
||||
|
||||
### 4.1 Session Start
|
||||
|
||||
**WT-030**: The Rust service connects and subscribes under a tokio runtime.
|
||||
|
||||
### 4.2 Receive & Dispatch by Type
|
||||
|
||||
**WT-031**: Rust receives and dispatches payloads by `payload_type`.
|
||||
|
||||
```rust
|
||||
// Rust service - WT-031
|
||||
use msghandler::{smartunpack, MsgEnvelopeV1};
|
||||
use base64::Engine as _;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let mut sub = transport_client::connect("ws://localhost:4222")
|
||||
.unwrap().subscribe("/agent/wine/api/v1/analyze");
|
||||
|
||||
for msg in sub.messages() {
|
||||
let env = smartunpack(&String::from_utf8_lossy(&msg.payload), &Default::default()).await.unwrap();
|
||||
for payload in &env.payloads {
|
||||
match payload.payload_type.as_str() {
|
||||
"arrowtable" => {
|
||||
let arrow_bytes = BASE64.decode(&payload.data).unwrap();
|
||||
println!("Received arrowtable payload ({} bytes)", arrow_bytes.len());
|
||||
},
|
||||
"text" => println!("Message: {}", payload.data),
|
||||
"image" | "audio" | "video" | "binary" => {
|
||||
let bytes = BASE64.decode(&payload.data).unwrap();
|
||||
println!("Received {} bytes of {}", bytes.len(), payload.payload_type);
|
||||
},
|
||||
"dictionary" | "jsontable" => println!("Data: {}", payload.data),
|
||||
_ => println!("Unknown payload type: {}", payload.payload_type),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> Traceability: `FR-007` (type preservation), `FR-006` (multi-payload), `SD-004`, `SD-005`. The `payload_type` field determines how to interpret the `data` string (type dispatch).
|
||||
|
||||
### 4.3 Send Type-Safe Results
|
||||
|
||||
**WT-032**: Rust sends mixed results using the enum-based `Payload` API.
|
||||
|
||||
```rust
|
||||
// Rust service - WT-032
|
||||
let (envelope, json_str) = smartpack(
|
||||
"/agent/wine/api/v1/results",
|
||||
&[
|
||||
("results".to_string(), Payload::ArrowTable(result_bytes), "arrowtable".to_string()),
|
||||
("summary".to_string(), Payload::Text("Analysis complete: 1500 rows".to_string()), "text".to_string()),
|
||||
],
|
||||
&smartpackOptions {
|
||||
broker_url: DEFAULT_BROKER_URL.to_string(),
|
||||
reply_to: "/python/worker/v1/results".to_string(),
|
||||
msg_purpose: "chat".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
).await?;
|
||||
|
||||
conn.publish("/agent/wine/api/v1/results", &json_str)?;
|
||||
```
|
||||
|
||||
> Traceability: `FR-013` (publish), `FR-001`, `SD-004`, `SD-006`.
|
||||
|
||||
### 4.4 Large File via Link Transport
|
||||
|
||||
**WT-033**: Rust uploads a large binary file; `smartpack()` selects link transport and uploads via `reqwest`.
|
||||
|
||||
```rust
|
||||
// Rust service - WT-033
|
||||
let large_file_data: Vec<u8> = std::fs::read("/data/large_dataset.parquet")?;
|
||||
let (envelope, json_str) = smartpack(
|
||||
"/agent/wine/api/v1/upload",
|
||||
&[("dataset".to_string(), Payload::Binary(large_file_data), "binary".to_string())],
|
||||
&smartpackOptions {
|
||||
fileserver_url: DEFAULT_FILESERVER_URL.to_string(),
|
||||
size_threshold: DEFAULT_SIZE_THRESHOLD,
|
||||
..Default::default()
|
||||
},
|
||||
).await?;
|
||||
conn.publish("/agent/wine/api/v1/upload", &json_str)?;
|
||||
```
|
||||
|
||||
> Traceability: `FR-003`, `SD-001`, `SD-002`, `SD-003`, `SD-006`.
|
||||
|
||||
### 4.5 Error Paths (Rust POV)
|
||||
|
||||
**WT-034**: Download exceeds retries → `MsgHandlerError::DownloadFailed { url, retries }` (`FR-010`, `SD-007`).
|
||||
|
||||
> Traceability: `FR-010`, `NFR-202`. Recovery: caller retries with a fresh upload; idiomatic `Result<T, E>`.
|
||||
|
||||
---
|
||||
|
||||
## 5. MicroPython (IoT Device)
|
||||
|
||||
This section traces the MicroPython sensor device. Due to memory constraints (256 KB–1 MB), MicroPython is limited to `text` and `dictionary` types and **direct transport only** (no link / file-server support).
|
||||
|
||||
> Cross-references: the device publishes readings that a **Python** backend consumes (see [Section 3](#3-python-backend)).
|
||||
|
||||
### 5.1 Session Start
|
||||
|
||||
**WT-040**: The device boots, connects to the MQTT broker, and publishes a sensor reading.
|
||||
|
||||
### 5.2 Send Sensor Data (Direct Only)
|
||||
|
||||
**WT-041**: MicroPython sends a `dictionary` payload under its lower threshold.
|
||||
|
||||
```python
|
||||
# MicroPython - WT-041
|
||||
from msghandler import smartpack
|
||||
|
||||
sensor_data = {"temperature": 25.5, "humidity": 60.0, "pressure": 1013.25}
|
||||
env, msg_json = smartpack(
|
||||
"/sensor/device/v1/readings",
|
||||
[("data", sensor_data, "dictionary")],
|
||||
broker_url="broker.local:1883",
|
||||
size_threshold=100000 # 100 KB for MicroPython
|
||||
)
|
||||
mqtt_client.publish("/sensor/device/v1/readings", msg_json)
|
||||
```
|
||||
|
||||
> Traceability: `FR-005` (MicroPython support), `FR-006` (multi-payload), `FR-012` (serialization), `SD-004`, `SD-005`, `SD-006`.
|
||||
|
||||
**WT-041.1**: Serialization is JSON → UTF-8 → base64 (no Arrow). Because the payload is under `size_threshold`, it travels direct.
|
||||
|
||||
> Traceability: `SD-002`, `SD-005`.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Device["MicroPython Device"]
|
||||
D1["sensor_data = {temp, humidity}"]
|
||||
D2["smartpack() -> JSON"]
|
||||
D3["publish via MQTT"]
|
||||
end
|
||||
subgraph Broker["MQTT Broker"]
|
||||
B["broker.local:1883"]
|
||||
end
|
||||
subgraph Backend["Python Backend"]
|
||||
P1["subscribe via MQTT"]
|
||||
P2["smartunpack(JSON)"]
|
||||
P3["get dictionary"]
|
||||
end
|
||||
D1 --> D2 --> D3 --> B --> P1 --> P2 --> P3
|
||||
```
|
||||
|
||||
### 5.3 Receive Acknowledgment
|
||||
|
||||
**WT-042**: The device receives an ACK/dictionary reply from the backend.
|
||||
|
||||
```python
|
||||
# MicroPython - WT-042
|
||||
env = smartunpack(str(transport_msg.payload))
|
||||
# env["payloads"] is now:
|
||||
# [("data", {"temperature": 25.5, ...}, "dictionary")]
|
||||
```
|
||||
|
||||
> Traceability: `FR-014`, `FR-007`, `SD-004`.
|
||||
|
||||
### 5.4 Error Paths (MicroPython POV)
|
||||
|
||||
**WT-043**: Payload exceeds the 100 KB threshold → MicroPython cannot use link transport (no file-server support) and must reject or chunk the payload.
|
||||
|
||||
> Traceability: `FR-005`, `FR-003`. Recovery: split into multiple direct payloads below the threshold.
|
||||
|
||||
**WT-044**: `dictionary` type but data is not JSON-serializable → serialization error (memory constraint).
|
||||
|
||||
> Traceability: `FR-012`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Cross-Participant Flow (all participants)
|
||||
|
||||
The following end-to-end flow shows a JavaScript client, a Julia backend, a Python worker, and a MicroPython device on a shared chat topic.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant JS as JavaScript (Browser)
|
||||
participant J as Julia (Backend)
|
||||
participant P as Python (Worker)
|
||||
participant MP as MicroPython (Device)
|
||||
participant T as Message Broker
|
||||
|
||||
JS->>T: publish "/chat/user/v1/message" (WT-003)
|
||||
T->>J: deliver to Julia
|
||||
J->>J: smartunpack() (WT-011)
|
||||
J->>J: process, build reply (WT-012)
|
||||
J->>T: publish reply (SD-006)
|
||||
T->>P: deliver to Python
|
||||
P->>P: smartunpack() (WT-022)
|
||||
T->>MP: deliver to MicroPython
|
||||
MP->>MP: smartunpack() (WT-042)
|
||||
T->>JS: deliver reply to browser (WT-005)
|
||||
```
|
||||
|
||||
> Traceability: `FR-001`, `FR-006`, `FR-013`, `FR-014`, `SD-004`, `SD-006`, `SD-008`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Specification Traceability
|
||||
|
||||
Each walkthrough step below maps to the exact `SD-XXX` / `FR-XXX` artifacts it implements or depends on.
|
||||
|
||||
| WT ID | Participant | Step | SD Ref | FR Ref | Description |
|
||||
|-------|-------------|------|--------|--------|-------------|
|
||||
| WT-001 | JavaScript | Session start / connect | SD-006 | FR-013 | Establish transport connection |
|
||||
| WT-002 | JavaScript | Send mixed payloads (text + image) | SD-004, SD-005 | FR-001, FR-006, FR-007 | smartpack() with mixed types |
|
||||
| WT-002.1 | JavaScript | Per-payload transport selection | SD-002 | FR-004 | direct transport for < threshold |
|
||||
| WT-003 | JavaScript | Publish JSON via transport | SD-006 | FR-013 | caller publishes |
|
||||
| WT-004 | JavaScript | Envelope shape crosses wire | SD-005, SD-008 | FR-011, FR-013 | msg_envelope_v1 JSON |
|
||||
| WT-005 | JavaScript | Receive reply | SD-004, SD-007 | FR-007, FR-014 | smartunpack() on reply |
|
||||
| WT-006 | JavaScript | Large file upload fails (link) | SD-001, SD-003 | FR-003, FR-008 | link transport error |
|
||||
| WT-007 | JavaScript | Transport connection lost | SD-006 | NFR-203 | caller retries publish |
|
||||
| WT-010 | Julia | Session start / subscribe | SD-006 | FR-014 | transport subscription |
|
||||
| WT-011 | Julia | Receive & unpack chat | SD-004, SD-007 | FR-001, FR-006, FR-007 | smartunpack() |
|
||||
| WT-011.1 | Julia | Direct path decode/deserialize | SD-004, SD-005 | FR-007 | _deserialize_data |
|
||||
| WT-012 | Julia | Send reply | SD-004, SD-005, SD-008 | FR-001, FR-013, FR-011 | smartpack() + reply_to_msg_id |
|
||||
| WT-013 | Julia | Receive large file (link) | SD-001, SD-002, SD-003 | FR-003, FR-008 | link download |
|
||||
| WT-014 | Julia | Download fails after retries | SD-007 | FR-010, NFR-202 | _fetch_with_backoff |
|
||||
| WT-015 | Julia | Unknown payload type | SD-004 | FR-007 | error throw |
|
||||
| WT-020 | Python | Session start / connect | SD-006 | FR-013 | transport consumer |
|
||||
| WT-021 | Python | Send arrowtable | SD-004, SD-005 | FR-002, FR-012 | Arrow IPC direct |
|
||||
| WT-021.1 | Python | Arrow IPC serialization | SD-002, SD-005 | FR-012 | encoding arrow-ipc |
|
||||
| WT-022 | Python | Receive results | SD-004, SD-007 | FR-002, FR-007, FR-014 | smartunpack() |
|
||||
| WT-023 | Python | Large arrowtable upload fails | SD-001, SD-003 | FR-003 | link transport error |
|
||||
| WT-024 | Python | pyarrow not available | SD-005 | FR-002 | RuntimeError |
|
||||
| WT-030 | Rust | Session start / subscribe | SD-006 | FR-013 | tokio transport |
|
||||
| WT-031 | Rust | Receive & dispatch by type | SD-004, SD-005 | FR-006, FR-007 | payload_type dispatch |
|
||||
| WT-032 | Rust | Send type-safe results | SD-004 | FR-001, FR-013 | Payload enum |
|
||||
| WT-033 | Rust | Large file via link | SD-001, SD-002, SD-003 | FR-003 | reqwest upload |
|
||||
| WT-034 | Rust | Download exceeds retries | SD-007 | FR-010, NFR-202 | DownloadFailed |
|
||||
| WT-040 | MicroPython | Session start / publish | SD-006 | FR-005 | MQTT connect |
|
||||
| WT-041 | MicroPython | Send dictionary (direct) | SD-004, SD-005 | FR-005, FR-006 | JSON base64 |
|
||||
| WT-041.1 | MicroPython | JSON serialization, direct transport | SD-002, SD-005 | FR-012 | JSON -> base64 |
|
||||
| WT-042 | MicroPython | Receive ACK | SD-004 | FR-007, FR-014 | smartunpack() |
|
||||
| WT-043 | MicroPython | Threshold exceeded (no link) | SD-002 | FR-003, FR-005 | direct-only constraint |
|
||||
| WT-044 | MicroPython | Non-serializable dictionary | SD-005 | FR-012 | serialization error |
|
||||
| WT-050 | — | Cross-participant chat broadcast | SD-004, SD-006 | FR-001, FR-006, FR-013, FR-014 | shared topic flow |
|
||||
|
||||
---
|
||||
|
||||
## 8. Gap-Check Validation (Walkthrough)
|
||||
|
||||
| Stage Transition | Gap-Check Question | Status |
|
||||
|------------------|-------------------|--------|
|
||||
| Solution Design → Walkthrough | Does every `SD-XXX` decision appear in at least one participant's walkthrough step with a `WT-XXX` id? Is there a section for each system participant? | ✅ Verified — SD-001..SD-008 traced across JS/Julia/Python/Rust/MicroPython sections with WT-XXX ids; all 5 participants covered |
|
||||
| Walkthrough → Specification | Does every `WT-XXX` walkthrough step trace to at least one `SP-XXX` specification rule? | ✅ Verified — every `WT-XXX` step maps to `SP-XXX` in the Walkthrough → Specification Trace of [`4-specification.md`](4-specification.md) |
|
||||
|
||||
---
|
||||
|
||||
## 9. Change Log
|
||||
|
||||
| Date | Version | Changes | Specification Ref | Solution Design Ref |
|
||||
|------|---------|---------|-------------------|---------------------|
|
||||
| 2026-09-16 | 1.7.0 | Restructured per ASG Framework walkthrough guideline: per-participant sections (JS/Julia/Python/Rust/MicroPython), `WT-XXX` step ids, `SD-XXX`/`FR-XXX` citations, per-participant sequence diagrams, cross-participant flow, and error paths | All sections | SD-001 through SD-008 |
|
||||
| 2026-09-16 | 1.6.0 | Aligned ground-truth API to `src/msghandler.jl` (smartpack returns tuple, smartunpack returns payloads as tuples); fixed Rust payload access | specification.md | SD-001 through SD-008 |
|
||||
| 2026-05-22 | 1.5.0 | Updated to ASG Framework eight pillars — added solution-design traceability (SD-XXX) to the traceability table | All sections | SD-001 through SD-008 |
|
||||
| 2026-05-15 | 1.4.0 | Made transport layer agnostic; removed NATS-specific references | All sections | SD-001 through SD-008 |
|
||||
| 2026-05-13 | 1.2.0 | Added Rust support (tokio, serde, arrow2) | specification.md:11 | SD-001 through SD-008 |
|
||||
| 2026-03-23 | 1.0.0 | Updated to ASG Framework walkthrough guidelines | All sections | SD-001 through SD-008 |
|
||||
|
||||
---
|
||||
|
||||
*This walkthrough document is versioned and maintained in git alongside the codebase. The technical details are grounded in [`src/msghandler.jl`](../src/msghandler.jl) (the reference implementation) and kept consistent with the Python, JavaScript (CSR), Rust, and MicroPython sources. All implementations must adhere to this documentation.*
|
||||
Reference in New Issue
Block a user