43 Commits

Author SHA1 Message Date
5b70df7c9d update 2026-05-29 11:23:53 +07:00
da2085ad32 update 2026-05-25 10:32:12 +07:00
48fddb5cbc update doc 2026-05-25 07:36:17 +07:00
dff47a80f9 update readme 2026-05-25 06:46:19 +07:00
76c73aac03 update 2026-05-24 13:37:03 +07:00
0c981d943d update 2026-05-24 13:36:39 +07:00
481fef9f70 update readme 2026-05-24 13:35:08 +07:00
b11dd456c0 update 2026-05-24 13:11:25 +07:00
bc51f20e48 update 2026-05-24 13:05:39 +07:00
e7c78cf40d update 2026-05-24 13:03:28 +07:00
5d2b46eb38 update 2026-05-24 13:00:30 +07:00
a134bd44bd update 2026-05-24 12:57:45 +07:00
86224c1daf update docs 2026-05-24 12:52:00 +07:00
b8339897f3 update doc 2026-05-24 12:39:19 +07:00
42b68d5bee update 2026-05-24 11:40:27 +07:00
dcd88de1a5 update readme 2026-05-24 08:57:14 +07:00
6d327967b1 update readme 2026-05-23 20:37:01 +07:00
4e8b7faead update readme 2026-05-23 20:10:37 +07:00
efd77937a2 update readme 2026-05-23 14:29:10 +07:00
9c9493eaa0 update readme 2026-05-23 13:47:29 +07:00
f06fbce486 update 2026-05-23 13:07:37 +07:00
c919f9585d update readme 2026-05-23 12:29:18 +07:00
bfb7acd55b update readme 2026-05-23 12:20:17 +07:00
e74d0a3301 update readme 2026-05-23 12:15:27 +07:00
a4f450386c update 2026-05-23 12:02:18 +07:00
15a7f1c178 update readme 2026-05-23 10:57:01 +07:00
31727f3337 update 2026-05-23 06:17:13 +07:00
cc52ef1bda update 2026-05-23 06:16:16 +07:00
ton
a1fcd86a74 Merge pull request 'v1.0.0' (#1) from v1.0.0 into main
Reviewed-on: #1
2026-05-22 22:16:16 +00:00
ba21ccf763 update 2026-05-23 05:14:46 +07:00
e8112baf80 update 2026-05-22 20:22:52 +07:00
594eb32783 update 2026-05-22 20:21:49 +07:00
e93fe9c5c7 update docs 2026-05-22 19:42:34 +07:00
8de9a65c03 update docs 2026-05-22 18:59:23 +07:00
0c9aebdd37 update 2026-05-22 10:53:55 +07:00
6da7708e72 update docs 2026-05-22 10:49:18 +07:00
6171923c05 update 2026-05-22 08:51:47 +07:00
72e0c3be1e update requirement doc 2026-05-22 07:07:06 +07:00
312d14b28f update docs 2026-05-22 06:17:30 +07:00
396e0848da rename to smartpack n smartunpack 2026-05-18 19:30:58 +07:00
cc95bc97d3 change MsgHandlerError 2026-05-15 17:40:58 +07:00
d0910ccc3f update docs 2026-05-15 13:25:48 +07:00
df9012e0eb remove NATS integration 2026-05-15 11:55:41 +07:00
26 changed files with 3398 additions and 3578 deletions

View File

@@ -1,25 +1,16 @@
Consider the following scenarios: Consider the following scenarios:
Scenario 1: The "Command & Control" Loop (Low Latency)Focus: Small payloads, Core NATS, bi-directional JSON.The Action: A user on a JavaScript dashboard clicks a "Start Simulation" button. This sends a JSON configuration (parameters like step_size and iterations) to Julia.The Flow: * JS (Sender): Recognizes the message is small ($< 10KB$). Packages it as a direct transport JSON envelope.Julia (Receiver): Listens on the NATS subject, decodes the JSON, and immediately acknowledges receipt with a "Running" status.Project Requirement Met: Fast, low-overhead communication for control signals without involving the fileserver. Scenario 1: The "Command & Control" Loop (Low Latency)Focus: Small payloads, bi-directional JSON.The Action: A user on a JavaScript dashboard clicks a "Start Simulation" button. This sends a JSON configuration (parameters like step_size and iterations) to Julia.The Flow: * JS (Sender): Recognizes the message is small ($< 10KB$). Packages it as a direct transport JSON envelope.Julia (Receiver): Receives the JSON, decodes it, and immediately acknowledges receipt with a "Running" status.Project Requirement Met: Fast, low-overhead communication for control signals without involving the fileserver.
Scenario 2: The "Deep Dive" Analysis (High Bandwidth)Focus: Large Arrow tables, Claim-Check pattern, Julia to JS.The Action: Julia finishes a heavy computation and produces a 500MB DataFrame with 10 million rows. It needs to send this to the JS frontend for visualization (e.g., using Perspective.js or D3).The Flow:Julia (Sender): Converts the DataFrame to an Arrow IPC stream. It sees the size is $> 1MB$, so it uploads the bytes to the HTTP fileserver. It then publishes a NATS message with transport: "link" and the URL.JS (Receiver): Receives the URL, fetches the data via fetch(), and uses tableFromIPC() to load the data into memory with zero-copy.Project Requirement Met: Handling massive datasets that exceed NATS message limits while maintaining data integrity across languages. Scenario 2: The "Deep Dive" Analysis (High Bandwidth)Focus: Large Arrow tables, Claim-Check pattern, Julia to JS.The Action: Julia finishes a heavy computation and produces a 500MB DataFrame with 10 million rows. It needs to send this to the JS frontend for visualization (e.g., using Perspective.js or D3).The Flow:Julia (Sender): Converts the DataFrame to an Arrow IPC stream. It sees the size is $> 1MB$, so it uploads the bytes to the HTTP fileserver. It then sends a message with transport: "link" and the URL.JS (Receiver): Receives the URL, fetches the data via fetch(), and uses tableFromIPC() to load the data into memory with zero-copy.Project Requirement Met: Handling massive datasets that exceed message limits while maintaining data integrity across languages.
Scenario 3: Live Audio/Signal Processing (Multimedia & Metadata)Focus: Raw binary, bi-directional streaming, NATS Headers.The Action: The JS client captures a 2-second "chunk" of microphone audio. It needs Julia to perform a Fast Fourier Transform (FFT) or AI transcription.The Flow:JS (Sender): Sends the raw binary WAV/PCM data. It uses NATS Headers to store the metadata ($fs = 44.1kHz$, $channels = 1$) to keep the payload purely binary.Julia (Receiver): Processes the audio and sends back a JSON result (the transcription) and an Arrow Table (the frequency spectrum data).Project Requirement Met: Bi-directional flow involving mixed media (Audio) and technical results (Arrow). Scenario 3: Live Audio/Signal Processing (Multimedia & Metadata)Focus: Raw binary, bi-directional streaming, headers for metadata.The Action: The JS client captures a 2-second "chunk" of microphone audio. It needs Julia to perform a Fast Fourier Transform (FFT) or AI transcription.The Flow:JS (Sender): Sends the raw binary WAV/PCM data. It uses transport headers to store the metadata ($fs = 44.1kHz$, $channels = 1$) to keep the payload purely binary.Julia (Receiver): Processes the audio and sends back a JSON result (the transcription) and an Arrow Table (the frequency spectrum data).Project Requirement Met: Bi-directional flow involving mixed media (Audio) and technical results (Arrow).
Scenario 4: The "Catch-Up" (Persistence & JetStream)Focus: NATS JetStream, late-joining consumers, state sync.The Action: Julia is constantly publishing "System Health" updates. The JS dashboard is closed for 10 minutes. When the user re-opens the dashboard, they need to see the last 10 minutes of history.The Flow:NATS (Server): Uses a JetStream with a Limits retention policy.JS (Consumer): Connects and requests a "Replay" from the last 10 minutes. It receives a mix of direct (small updates) and link (historical snapshots) messages.Project Requirement Met: Temporal decoupling—consumers can receive data that was sent while they were offline. Scenario 4: The "Catch-Up" (Persistence & State Sync)Focus: Message persistence, late-joining consumers, state sync.The Action: Julia is constantly publishing "System Health" updates. The JS dashboard is closed for 10 minutes. When the user re-opens the dashboard, they need to see the last 10 minutes of history.The Flow:Transport (Server): Uses a persistence layer with a Limits retention policy.JS (Consumer): Connects and requests a "Replay" from the last 10 minutes. It receives a mix of direct (small updates) and link (historical snapshots) messages.Project Requirement Met: Temporal decoupling—consumers can receive data that was sent while they were offline.
Role: Principal Systems Architect & Lead Software Engineer.Objective: Implement a high-performance, bi-directional data bridge between a Julia service and a JavaScript (Node.js) service using NATS (Core & JetStream).⚠️ STRICT ARCHITECTURAL CONSTRAINTS (Non-Negotiable)Transport Strategy (Claim-Check Pattern):Direct Path: If payload is < 1MB, send data directly via NATS inside the message envelope (Base64 encoded).Link Path: If payload is > 1MB, upload to a shared HTTP fileserver/store. The NATS message must only contain the metadata and the download URL.Tabular Data Format: * MUST use Apache Arrow IPC Stream for all tables/DataFrames. No CSV or standard JSON-serialization of tables allowed.System Symmetry: * Both services must function as Producers AND Consumers.Modular Elegance: * Implementation must be abstracted into a SmartSend function and a SmartReceive handler. The developer calling these functions should not need to care if the data is going via NATS direct or HTTP link.Technical Stack & Use CasesJulia: NATS.jl, Arrow.jl, JSON3.jl, HTTP.jl.Node.js: nats.js, apache-arrow.Scenarios to Support: * Large Data: Sending a 500MB Arrow table from Julia $\rightarrow$ JS.Media: Sending a 5MB WAV file from JS $\rightarrow$ Julia.Signals: Sending small JSON control commands ($< 10KB$) directly via NATS.Implementation Requirements1. Unified JSON Envelope:Define a schema containing: correlation_id (UUID), type (table/binary/json), transport (direct/link), payload (if direct), and url (if link).2. The Julia Module:Implement SmartSend(subject, data, type): Handles Arrow serialization to an IOBuffer, checks size, and manages HTTP uploads for large blobs.Implement SmartReceive(msg): Parses envelope, handles the HTTP fetch with Exponential Backoff (to avoid race conditions), and restores the DataFrame.Include a basic HTTP.listen server to serve as the temporary storage.3. The JavaScript Module:Implement a symmetric SmartSend using nats.js and apache-arrow.Implement a JetStream Pull Consumer for SmartReceive to ensure backpressure and memory safety.4. Performance & Reliability:Demonstrate "Zero-Copy" reading of the Arrow IPC stream on the JS side.Log the correlation_id at every stage for distributed tracing.
Create a walkthrough for Julia service-A service sending a mix-content chat message to Julia service-B. the chat message must includes
Role: Principal Systems Architect & Lead Software Engineer.Objective: Implement a high-performance, bi-directional data bridge between a Julia service and a JavaScript (Node.js) service, using a unified message envelope with Claim-Check pattern for large payloads.⚠️ STRICT ARCHITECTURAL CONSTRAINTS (Non-Negotiable)Transport Strategy (Claim-Check Pattern):Direct Path: If payload is < 1MB, send data directly inside the message envelope (Base64 encoded).Link Path: If payload is > 1MB, upload to a shared HTTP fileserver/store. The message must only contain the metadata and the download URL.Tabular Data Format: * MUST use Apache Arrow IPC Stream for all tables/DataFrames. No CSV or standard JSON-serialization of tables allowed.System Symmetry: * Both services must function as Producers AND Consumers.Modular Elegance: * Implementation must be abstracted into a smartpack function and a smartunpack handler. The developer calling these functions should not need to care if the data is going via direct or HTTP link.Technical Stack & Use CasesJulia: Arrow.jl, JSON3.jl, HTTP.jl.Node.js: apache-arrow, native fetch.Scenarios to Support: * Large Data: Sending a 500MB Arrow table from Julia $\rightarrow$ JS.Media: Sending a 5MB WAV file from JS $\rightarrow$ Julia.Signals: Sending small JSON control commands ($< 10KB$) directly in the envelope.Implementation Requirements1. Unified JSON Envelope:Define a schema containing: correlation_id (UUID), type (table/binary/json), transport (direct/link), payload (if direct), and url (if link).2. The Julia Module:Implement smartpack(subject, data, type): Handles Arrow serialization to an IOBuffer, checks size, and manages HTTP uploads for large blobs.Implement smartunpack(msg): Parses envelope, handles the HTTP fetch with Exponential Backoff (to avoid race conditions), and restores the DataFrame.Include a basic HTTP.listen server to serve as the temporary storage.3. The JavaScript Module:Implement a symmetric smartpack using native fetch and apache-arrow.Implement a JetStream P... (line truncated to 2000 chars)
I updated the following: I updated the following:
- msghandler.jl. Essentially I add NATS_connection keyword and new publish_message function to support the keyword. - msghandler.jl. Essentially I add transport_connection keyword and new publish_message function to support the keyword.
Use them and ONLY them as ground truth. Use them and ONLY them as ground truth.
Then update the following files accordingly: Then update the following files accordingly:
- architecture.md - architecture.md
@@ -30,19 +21,12 @@ All API should be semantically consistent and naming should be consistent across
Task: Update msghandler.js to reflect recent changes in msghandler.jl and docs Task: Update msghandler.js to reflect recent changes in msghandler.jl and docs
Context: msghandler.jl and docs has been updated. Context: msghandler.jl and docs has been updated.
Requirements: Requirements:
Source of Truth: Treat the updated msghandler.jl and docs as the definitive source. Source of Truth: Treat the updated msghandler.jl and docs as the definitive source.
API Consistency: Ensure the Main Package API (e.g., smartsend(), publish_message()) uses consistent naming across all three supported languages. API Consistency: Ensure the Main Package API (e.g., smartpack(), publish_message()) uses consistent naming across all three supported languages.
Ecosystem Variance: Low-level native functions (e.g., NATS.connect(), JSON.read()) should follow the conventions of the specific language ecosystem and do not require cross-language consistency. Ecosystem Variance: Low-level native functions (e.g., connect(), JSON.parse()) should follow the conventions of the specific language ecosystem and do not require cross-language consistency.
@@ -67,8 +51,6 @@ Now do the following:
I'm expanding this Julia package (msghandler) into a cross-platform project by adding I'm expanding this Julia package (msghandler) into a cross-platform project by adding
a JavaScript, Python and MicroPython implementation. a JavaScript, Python and MicroPython implementation.
The following will serve as the ground truth: The following will serve as the ground truth:
@@ -91,9 +73,12 @@ Now, help me do the following:
# ---------------------------------------------- 100 --------------------------------------------- # # ---------------------------------------------- 100 --------------------------------------------- #
Got it — lets rebuild your table in my own teaching style, keeping it crisp, intuitive, and easy for students to grasp. Ill emphasize **purpose, audience, format, example, and KPI** in a way that flows like a story of how projects move from idea → contract → design → code → review → operations. Got it — let's rebuild your table in my own teaching style, keeping it crisp, intuitive, and easy for students to grasp. I'll emphasize **purpose, audience, format, example, and KPI** in a way that flows like a story of how projects move from idea → contract → design → code → review → operations.
--- ---
@@ -101,42 +86,35 @@ Got it — lets rebuild your table in my own teaching style, keeping it crisp
| Document | Purpose (Rationale) | Primary Audience | Format / Content | Example (SaaS Context) | Measurement (KPI) | | Document | Purpose (Rationale) | Primary Audience | Format / Content | Example (SaaS Context) | Measurement (KPI) |
|-----------------|---------------------|-----------------|------------------|------------------------|-------------------| |-----------------|---------------------|-----------------|------------------|------------------------|-------------------|
| **Requirements** | Capture the **business intent** — why were building this and what success looks like. Defines boundaries and uservisible outcomes. | Stakeholders, Product Owners, Lead Developers | User stories, PRDs, acceptance criteria, nonfunctional constraints. | System must process tabular data from Julia to SvelteKit UI with <200ms latency for 5member teams. | 95% of requests complete <200ms (synthetic monitoring). | | **Requirements** | Capture the **business intent** — why we're building this and what success looks like. Defines boundaries and uservisible outcomes. | Stakeholders, Product Owners, Lead Developers | User stories, PRDs, acceptance criteria, nonfunctional constraints. | "System must process tabular data from Julia to SvelteKit UI with <200ms latency for 5member teams." | 95% of requests complete <200ms (synthetic monitoring). |
| **Specification** | The **technical contract** — precise rules for inputs, outputs, and data shape. Ensures consistency across dev and test. | Developers, QA Engineers, CI/CD pipelines | OpenAPI, Protobuf, AsyncAPI. Endpoint definitions, schemas, error codes. | `contract.yaml` defining a NATS subject that accepts Arrow streams with snake_case headers. | 100% of messages validated against spec (CI block rate). | | **Specification** | The **technical contract** — precise rules for inputs, outputs, and data shape. Ensures consistency across dev and test. | Developers, QA Engineers, CI/CD pipelines | OpenAPI, Protobuf, AsyncAPI. Endpoint definitions, schemas, error codes. | `contract.yaml` defining a subject that accepts Arrow streams with snake_case headers. | 100% of messages validated against spec (CI block rate). |
| **Architecture** | The **blueprint** — how components fit together, interact, and scale. Guides system structure and tradeoffs. | Architects, Senior Developers, DevOps | C4 diagrams, Mermaid.js, component/network/storage models. | Diagram showing 6node cluster routing traffic via Caddy → Node.js API → Julia pods. | 100% of major decisions logged with tradeoff analysis. | | **Architecture** | The **blueprint** — how components fit together, interact, and scale. Guides system structure and tradeoffs. | Architects, Senior Developers, DevOps | C4 diagrams, Mermaid.js, component/network/storage models. | Diagram showing 6node cluster routing traffic via Caddy → Node.js API → Julia pods. | 100% of major decisions logged with tradeoff analysis. |
| **Walkthrough** | The **story of flow** — shows how pieces connect endtoend and why steps are sequenced. Builds intuition for new devs. | New Developers, Team Members | TOUR.md, Loom videos, sequence diagrams. Stepbystep traces with rationale. | UI sends JSON → Node.js wraps ClaimCheck → Julia pulls Arrow data (prevents NATS overflow). | New developers ship feature in <2 days (PR timeline). | | **Walkthrough** | The **story of flow** — shows how pieces connect endtoend and why steps are sequenced. Builds intuition for new devs. | New Developers, Team Members | TOUR.md, Loom videos, sequence diagrams. Stepbystep traces with rationale. | "UI sends JSON → Node.js wraps ClaimCheck → Julia pulls Arrow data (prevents overflow)." | New developers ship feature in <2 days (PR timeline). |
| **Implementation** | The **real code** — business logic, helpers, tests, configs. Where design becomes executable. | Developers, Code Reviewers | Source code, README.md, unit tests, setup scripts. | Julia function for matrix calculation + SvelteKit component rendering table. | >80% unit test coverage, <5% drift from spec. | | **Implementation** | The **real code** — business logic, helpers, tests, configs. Where design becomes executable. | Developers, Code Reviewers | Source code, README.md, unit tests, setup scripts. | Julia function for matrix calculation + SvelteKit component rendering table. | >80% unit test coverage, <5% drift from spec. |
| **Validation** | The **enforcer** — ensures implementation matches the spec. Blocks drift and human error. | Automation servers, QA, Lead Developers | CI jobs, contract tests, linting, integration checks. | CI job rejects PR with camelCase field not allowed by YAML spec. | <1% of PRs bypass validation gates. | | **Validation** | The **enforcer** — ensures implementation matches the spec. Blocks drift and human error. | Automation servers, QA, Lead Developers | CI jobs, contract tests, linting, integration checks. | CI job rejects PR with camelCase field not allowed by YAML spec. | <1% of PRs bypass validation gates. |
| **Runbook** | The **operational manual** — how the system lives in production, scales, and recovers. Guides oncall engineers. | DevOps, SREs, Oncall Developers | K8s manifests, Helm charts, Markdown guides. Deployment, scaling, backup/restore, troubleshooting. | GitOps manifest ensuring 6 Julia replicas restart if memory >80%. | MTTR <15 minutes for P1 incidents. | | **Runbook** | The **operational manual** — how the system lives in production, scales, and recovers. Guides oncall engineers. | DevOps, SREs, Oncall Developers | K8s manifests, Helm charts, Markdown guides. Deployment, scaling, backup/restore, troubleshooting. | GitOps manifest ensuring 6 Julia replicas restart if memory >80%. | MTTR <15 minutes for P1 incidents. |
# ---------------------------------------------- 100 --------------------------------------------- # # ---------------------------------------------- 100 --------------------------------------------- #
SDD + GitOps Documentation Stack SDD + GitOps Documentation Stack
Document,"Purpose (The ""Rationale"")",Primary Audience,Format / Content,Example (SaaS Context),"Measurement (KPI)" Document,"Purpose (The ""Rationale"")",Primary Audience,Format / Content,Example (SaaS Context),"Measurement (KPI)"
Requirements,"Defines the ""Why"" and the Business Boundary. It sets the constraints and success criteria so the team knows when a feature is ""done"" from a user's perspective.","Stakeholders, Product Owners, Lead Developers","Format: User Stories, PRDs. Content: Functional goals, non-functional requirements (latency, scale), and explicit ""out-of-scope"" items.","""The system must process high-volume tabular data from Julia to the SvelteKit UI with <200ms latency for 5-member teams."",""Pass/Fail: 95% of requests complete <200ms (measured via synthetic monitoring)"" Requirements,"Defines the ""Why"" and the Business Boundary. It sets the constraints and success criteria so the team knows when a feature is ""done"" from a user's perspective.","Stakeholders, Product Owners, Lead Developers","Format: User Stories, PRDs. Content: Functional goals, non-functional requirements (latency, scale), and explicit ""out-of-scope"" items.","""The system must process high-volume tabular data from Julia to the SvelteKit UI with <200ms latency for 5-member teams."",""Pass/Fail: 95% of requests complete <200ms (measured via synthetic monitoring)""
The Spec,"The Technical Contract. It serves as the single source of truth that defines the shape of data. In SDD, this file drives code generation and automated testing.","Developers, QA Engineers, CI/CD Pipelines","Format: OpenAPI (YAML), Protobuf, AsyncAPI. Content: Endpoint definitions, strict data types, error codes, and request/response schemas.",A contract.yaml defining a NATS subject that accepts an Apache Arrow stream with specific snake_case headers.",""Schema Validation Rate: 100% of messages validated against spec (CI block rate)"" The Spec,"The Technical Contract. It serves as the single source of truth that defines the shape of data. In SDD, this file drives code generation and automated testing.","Developers, QA Engineers, CI/CD Pipelines","Format: OpenAPI (YAML), Protobuf, AsyncAPI. Content: Endpoint definitions, strict data types, error codes, and request/response schemas.",A contract.yaml defining a subject that accepts an Apache Arrow stream with specific snake_case headers.",""Schema Validation Rate: 100% of messages validated against spec (CI block rate)""
Architecture,"The Structural Blueprint. It explains how the ""pieces"" are arranged in the cluster. It defines the relationships between services, databases, and external providers.","System Architects, Senior Developers, DevOps","Format: C4 Model Diagrams, Mermaid.js. Content: Component diagrams, network flow, storage strategy, and technology stack definitions.",A diagram showing how the 6-node cluster routes traffic through Caddy to the Node.js API and offloads heavy math to Julia pods.",""Architecture Decision Log: 100% of major decisions documented with trade-off analysis"" Architecture,"The Structural Blueprint. It explains how the ""pieces"" are arranged in the cluster. It defines the relationships between services, databases, and external providers.","System Architects, Senior Developers, DevOps","Format: C4 Model Diagrams, Mermaid.js. Content: Component diagrams, network flow, storage strategy, and technology stack definitions.",A diagram showing how the 6-node cluster routes traffic through Caddy to the Node.js API and offloads heavy math to Julia pods.",""Architecture Decision Log: 100% of major decisions documented with trade-off analysis""
Walkthrough,"The Intuition & Flow. It connects multiple APIs/services into a cohesive end-to-end story. It explains the ""steps"" and the ""rationale"" behind the sequence of operations.","New Developers, Current Team Members","Format: TOUR.md, Loom videos, Sequence Diagrams. Content: Step-by-step trace of a feature, explanation of state changes, and the ""why"" behind complex logic.","""End-to-End Trace:"" 1. UI sends JSON to Node.js. 2. Node.js wraps it in a Claim-Check. 3. Julia pulls the Arrow data. Rationale: This prevents NATS memory overflow.",""Onboarding Velocity: New developers deploy feature in <2 days (tracked via PR timeline)"" Walkthrough,"The Intuition & Flow. It connects multiple APIs/services into a cohesive end-to-end story. It explains the ""steps"" and the ""rationale"" behind the sequence of operations.","New Developers, Current Team Members","Format: TOUR.md, Loom videos, Sequence Diagrams. Content: Step-by-step trace of a feature, explanation of state changes, and the ""why"" behind complex logic.","""End-to-End Trace:"" 1. UI sends JSON to Node.js. 2. Node.js wraps it in a Claim-Check. 3. Julia pulls the Arrow data. Rationale: This prevents overflow.",""Onboarding Velocity: New developers deploy feature in <2 days (tracked via PR timeline)""
Implementation,"The Functional Reality. This is the actual execution of the logic. In SDD, parts of this are auto-generated to ensure it never drifts from the Spec.","Developers, Code Reviewers","Format: Source Code (Git), README.md. Content: Business logic, internal helper functions, unit tests, and local setup instructions.",The Julia function that performs the matrix calculation and the SvelteKit component that renders the resulting table.",""Code Coverage: >80% unit test coverage, <5% test drift from spec"" Implementation,"The Functional Reality. This is the actual execution of the logic. In SDD, parts of this are auto-generated to ensure it never drifts from the Spec.","Developers, Code Reviewers","Format: Source Code (Git), README.md. Content: Business logic, internal helper functions, unit tests, and local setup instructions.",The Julia function that performs the matrix calculation and the SvelteKit component that renders the resulting table.",""Code Coverage: >80% unit test coverage, <5% test drift from spec""
Validation,"The Enforcement Layer. It ensures that the ""Reality"" (Code) actually matches the ""Contract"" (Spec). It prevents human error from breaking the system.","Automation Servers, QA, Lead Developers","Format: GitHub Actions, Dredd, Prism. Content: Contract tests, linting rules, and integration tests that check API compliance.",A CI job that blocks a Pull Request because a developer added a camelCase field that isn't allowed in the shared YAML spec.",""Block Rate: <1% of PRs reach production without validation (CI gate pass rate)"" Validation,"The Enforcement Layer. It ensures that the ""Reality"" (Code) actually matches the ""Contract"" (Spec). It prevents human error from breaking the system.","Automation Servers, QA, Lead Developers","Format: GitHub Actions, Dredd, Prism. Content: Contract tests, linting rules, and integration tests that check API compliance.",A CI job that blocks a Pull Request because a developer added a camelCase field that isn't allowed in the shared YAML spec.",""Block Rate: <1% of PRs reach production without validation (CI gate pass rate)""
Runbook,"The Operational Life-Support. It defines how the system lives in production and how to fix it. In GitOps, the ""State"" is declared here.","DevOps, SREs, On-call Developers","Format: K8s Manifests, Helm Charts, Markdown. Content: Deployment steps, scaling triggers, backup/restore commands, and troubleshooting guides.",A GitOps manifest in Flux that ensures 6 replicas of the Julia service are always running and restarts them if memory hits 80%.",""MTTR: <15 minutes for P1 incidents (tracked via incident management system)"" Runbook,"The Operational Life-Support. It defines how the system lives in production and how to fix it. In GitOps, the ""State"" is declared here.","DevOps, SREs, On-call Developers","Format: K8s Manifests, Helm Charts, Markdown. Content: Deployment steps, scaling triggers, backup/restore commands, and troubleshooting guides.",A GitOps manifest in Flux that ensures 6 replicas of the Julia service are always running and restarts them if memory hits 80%.",""MTTR: <15 minutes for P1 incidents (tracked via incident management system)""
Do you understand the provided text? Don't fucking change the table content. I want you to add "Measurement (KPI)" column. it is only example of course. This table will be used for consult and teaching. Do you understand the provided text? Don't fucking change the table content. I want you to add "Measurement (KPI)" column. it is only example of course. This table will be used for consult and teaching.
# ---------------------------------------------- 100 --------------------------------------------- # # ---------------------------------------------- 100 --------------------------------------------- #
Can you write the table and explain this approach and each doc in details then save to docs/SDD_FRAMEWORK.md so I can consult it later. Can you write the table and explain this approach and each doc in details then save to docs/SDD_FRAMEWORK.md so I can consult it later.
Don't forget to add How to use this approach effectively. Don't forget to add How to use this approach effectively.
# ---------------------------------------------- 100 --------------------------------------------- # # ---------------------------------------------- 100 --------------------------------------------- #
Since I develop src folder before I adopt SDD_FRAMEWORK.md approach, can you check src folder and my current doc files then write docs/requirements.md according to SDD framework? Treat src as ground truth. Since I develop src folder before I adopt SDD_FRAMEWORK.md approach, can you check src folder and my current doc files then write docs/requirements.md according to SDD framework? Treat src as ground truth.
@@ -170,8 +148,6 @@ Can you update the content of the following files according to /home/ton/docker-
<!-- ------------------------------------------- 100 ------------------------------------------- -->
I updated ./src/msghandler.jl. Use it as groundtruth. Check ./docs folder I want to update the content of the following files according to /home/ton/docker-apps/sommpanion/ASG_Framework/ASG_Framework.md: I updated ./src/msghandler.jl. Use it as groundtruth. Check ./docs folder I want to update the content of the following files according to /home/ton/docker-apps/sommpanion/ASG_Framework/ASG_Framework.md:
- ./docs/requirements.md - ./docs/requirements.md
- ./docs/specification.md - ./docs/specification.md
@@ -180,7 +156,6 @@ I updated ./src/msghandler.jl. Use it as groundtruth. Check ./docs folder I want
Check the following files: Check the following files:
- ./docs/requirements.md - ./docs/requirements.md
- ./docs/specification.md - ./docs/specification.md
@@ -189,6 +164,14 @@ Check the following files:
I would like to expand this package (msghandler) to include Rust support. I would like to expand this package (msghandler) to include Rust support.
Now help me update Rust implementation of this package at ./src/msghandler.rs. Now help me update Rust implementation of this package at ./src/msghandler.rs.
<!-- ------------------------------------------- 100 ------------------------------------------- -->
I updated ./src/msghandler-csr.js. Can you check whether files in ./docs needs to be update?
You should check the files sequencially in the following order:
1) ./docs/requirements.md
2) ./docs/specification.md
3) ./docs/architecture.md
4) ./docs/walkthrough.md
<!-- ------------------------------------------- 100 ------------------------------------------- -->
@@ -197,6 +180,339 @@ I want to build a client-side-rendering Dioxus-based chat webapp.
Dioxus version 0.7+ should be great. Dioxus version 0.7+ should be great.
I already populate the current folder for the project. I already populate the current folder for the project.
my server REST API endpoint is sommpanion.yiem.cc/agent-fronent/api/v1/chat but I didn't run the server yet. A message format is JSON string. my server REST API endpoint is sommpanion.yiem.cc/agent-fronent/api/v1/chat but I didn't run the server yet. A message format is JSON string.
I just placed my custom package for encode and decode message at ./src/msghandler.rs. smartsend() is for encoding and smartreceive() is for decoding. I just placed my custom package for encode and decode message at ./src/msghandler.rs. smartpack() is for encoding and smartunpack() is for decoding.
you may also check the file /home/ton/docker-apps/sommpanion/msghandler/docs/walkthrough.md for more info about my package. you may also check the file /home/ton/docker-apps/sommpanion/msghandler/docs/walkthrough.md for more info about my package.
You can test whether Dioxus webapp can be build using this command "dx bundle --web --release --debug-symbols=false" You can test whether Dioxus webapp can be build using this command "dx bundle --web --release --debug-symbols=false"
Do you know about ChatGPT chat interface? I want to build similar webapp.
My app should be built as client-side-rendering Dioxus-based (version 0.7+).
I already build backend server and I intend to communicate with the webapp using json string that encode the following message envelop:
{
"correlation_id": "a1b2c3d4...",
"msg_id": "e5f6g7h8...",
"timestamp": "2026-03-13T16:30:00.000Z",
"send_to": "",
"msg_purpose": "chat",
"sender_name": "chat-webapp",
"sender_id": "sender-uuid...",
"receiver_name": "agent-backend",
"receiver_id": "",
"reply_to": "",
"reply_to_msg_id": "",
"broker_url": "myservice.mydomain.com/subservice/api/v1/chat",
"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": 150000,
"data": "iVBORw0KGgoAAAANSUhEUgAA...",
"metadata": {"payload_bytes": 150000}
},
{
...,
"payload_type": "text",
...,
},
...
]
}
---
I already have this Rust module ./src/msghandler.rs containing the following functions for the webapp to use:
- smartpack() to encode the above message envelop into json string.
- smartunpack() to decode json string back to message envelop.
- the msghandler.rs walkthrough is at /home/ton/docker-apps/sommpanion/msghandler/docs/walkthrough.md
MQTT will be used as communication channel between the webapp and the backend. MQTT broker is "mqtt.mydomain.com". I didn't run the broker yet.
I already setup the project structure. Can you implement the app?
To test whether this Dioxus project can be build, you may use this command "dx bundle --web --release --debug-symbols=false"
P.S. In a Dioxus single-page application (SPA), switching screens can be handled perfectly using standard Rust state matching (often called conditional rendering or state-based routing).
read the following files:
./docs/requirements.md
./docs/solution-design.md
./docs/specification.md
./docs/walkthrough.md
What is the main interface of this package?
read the following files:
- ./src/msghandler.jl
- ./test/test_julia_mix_payloads_sender.jl
- ./src/msghandler-csr.js
- ./README.md
I want to add:
1) sending jsontable and arrowtable Julia example to README.md
2) sending image and jsontable Javascript(browser) example to README.md
read the following files:
- ./README.md
- ./src/msghandler.jl
- ./test/test_julia_mix_payloads_sender.jl
- ./src/msghandler-csr.js
I want to:
1) add sending jsontable and arrowtable julia example in README.md
2) add sending jsontable and image Javascript example in README.md
I execute test/test_julia_mix_payloads_sender.jl and I get this error:
ERROR: LoadError: HTTP.ConnectError for url = `https://fileserver.yiem.cc/upload`: IOError: SSL_ERROR_SSL
Stacktrace:
[1] macro expansion
@ ~/.julia/packages/OpenSSL/2SUGA/src/ssl.jl:476 [inlined]
[2] macro expansion
@ ./lock.jl:376 [inlined]
[3] connect(ssl::OpenSSL.SSLStream; require_ssl_verification::Bool)
@ OpenSSL ~/.julia/packages/OpenSSL/2SUGA/src/ssl.jl:443
[4] connect
@ ~/.julia/packages/OpenSSL/2SUGA/src/ssl.jl:509 [inlined]
[5] #sslconnection#21
@ ~/.julia/packages/HTTP/Y97L1/src/Connections.jl:595 [inlined]
[6] sslconnection
@ ~/.julia/packages/HTTP/Y97L1/src/Connections.jl:585 [inlined]
[7] getconnection(::Type{OpenSSL.SSLStream}, host::SubString{String}, port::SubString{String}; kw::@Kwargs{require_ssl_verification::Bool, keepalive::Bool, readtimeout::Int64, iofunction::Nothing, decompress::Nothing, verbose::Int64, body_is_form::Bool})
@ HTTP.Connections ~/.julia/packages/HTTP/Y97L1/src/Connections.jl:582
[8] getconnection
@ ~/.julia/packages/HTTP/Y97L1/src/Connections.jl:574 [inlined]
[9] #13
@ ~/.julia/packages/HTTP/Y97L1/src/Connections.jl:463 [inlined]
[10] macro expansion
@ ~/.julia/packages/ConcurrentUtilities/hQBsU/src/try_with_timeout.jl:92 [inlined]
[11] (::ConcurrentUtilities.var"#try_with_timeout##2#try_with_timeout##3"{Any, Channel{Any}, HTTP.Connections.var"#13#14"{OpenSSL.SSLStream, Bool, Bool, @Kwargs{readtimeout::Int64, iofunction::Nothing, decompress::Nothing, verbose::Int64, body_is_form::Bool}, SubString{String}, SubString{String}}, Timer})()
@ ConcurrentUtilities ~/.julia/packages/ConcurrentUtilities/hQBsU/src/ConcurrentUtilities.jl:10
Stacktrace:
[1] (::HTTP.ConnectionRequest.var"#connections#connectionlayer##0"{HTTP.ConnectionRequest.var"#connections#1#connectionlayer##1"{HTTP.TimeoutRequest.var"#timeouts#timeoutlayer##0"{HTTP.TimeoutRequest.var"#timeouts#1#timeoutlayer##1"{HTTP.ExceptionRequest.var"#exceptions#exceptionlayer##0"{HTTP.ExceptionRequest.var"#exceptions#1#exceptionlayer##1"{typeof(HTTP.StreamRequest.streamlayer)}}}}}})(req::HTTP.Messages.Request; proxy::Nothing, socket_type::Type, socket_type_tls::Nothing, readtimeout::Int64, connect_timeout::Int64, logerrors::Bool, logtag::Nothing, closeimmediately::Bool, kw::@Kwargs{iofunction::Nothing, decompress::Nothing, verbose::Int64, body_is_form::Bool})
@ HTTP.ConnectionRequest ~/.julia/packages/HTTP/Y97L1/src/clientlayers/ConnectionRequest.jl:88
[2] connections
@ ~/.julia/packages/HTTP/Y97L1/src/clientlayers/ConnectionRequest.jl:60 [inlined]
[3] (::Base.var"#46#47"{Base.var"#48#49"{ExponentialBackOff, HTTP.RetryRequest.var"#retrylayer##2#retrylayer##3"{Int64, typeof(HTTP.RetryRequest.FALSE), HTTP.Messages.Request, Base.RefValue{Int64}}, HTTP.ConnectionRequest.var"#connections#connectionlayer##0"{HTTP.ConnectionRequest.var"#connections#1#connectionlayer##1"{HTTP.TimeoutRequest.var"#timeouts#timeoutlayer##0"{HTTP.TimeoutRequest.var"#timeouts#1#timeoutlayer##1"{HTTP.ExceptionRequest.var"#exceptions#exceptionlayer##0"{HTTP.ExceptionRequest.var"#exceptions#1#exceptionlayer##1"{typeof(HTTP.StreamRequest.streamlayer)}}}}}}}})(args::HTTP.Messages.Request; kwargs::@Kwargs{iofunction::Nothing, decompress::Nothing, verbose::Int64, body_is_form::Bool})
@ Base ./error.jl:309
[4] (::HTTP.RetryRequest.var"#manageretries#retrylayer##0"{HTTP.RetryRequest.var"#manageretries#1#retrylayer##1"{HTTP.ConnectionRequest.var"#connections#connectionlayer##0"{HTTP.ConnectionRequest.var"#connections#1#connectionlayer##1"{HTTP.TimeoutRequest.var"#timeouts#timeoutlayer##0"{HTTP.TimeoutRequest.var"#timeouts#1#timeoutlayer##1"{HTTP.ExceptionRequest.var"#exceptions#exceptionlayer##0"{HTTP.ExceptionRequest.var"#exceptions#1#exceptionlayer##1"{typeof(HTTP.StreamRequest.streamlayer)}}}}}}}})(req::HTTP.Messages.Request; retry::Bool, retries::Int64, retry_delays::ExponentialBackOff, retry_check::Function, retry_non_idempotent::Bool, kw::@Kwargs{iofunction::Nothing, decompress::Nothing, verbose::Int64, body_is_form::Bool})
@ HTTP.RetryRequest ~/.julia/packages/HTTP/Y97L1/src/clientlayers/RetryRequest.jl:75
[5] manageretries
@ ~/.julia/packages/HTTP/Y97L1/src/clientlayers/RetryRequest.jl:30 [inlined]
[6] (::HTTP.CookieRequest.var"#managecookies#cookielayer##0"{HTTP.CookieRequest.var"#managecookies#1#cookielayer##1"{HTTP.RetryRequest.var"#manageretries#retrylayer##0"{HTTP.RetryRequest.var"#manageretries#1#retrylayer##1"{HTTP.ConnectionRequest.var"#connections#connectionlayer##0"{HTTP.ConnectionRequest.var"#connections#1#connectionlayer##1"{HTTP.TimeoutRequest.var"#timeouts#timeoutlayer##0"{HTTP.TimeoutRequest.var"#timeouts#1#timeoutlayer##1"{HTTP.ExceptionRequest.var"#exceptions#exceptionlayer##0"{HTTP.ExceptionRequest.var"#exceptions#1#exceptionlayer##1"{typeof(HTTP.StreamRequest.streamlayer)}}}}}}}}}})(req::HTTP.Messages.Request; cookies::Bool, cookiejar::HTTP.Cookies.CookieJar, kw::@Kwargs{iofunction::Nothing, decompress::Nothing, verbose::Int64, body_is_form::Bool})
@ HTTP.CookieRequest ~/.julia/packages/HTTP/Y97L1/src/clientlayers/CookieRequest.jl:42
[7] managecookies
@ ~/.julia/packages/HTTP/Y97L1/src/clientlayers/CookieRequest.jl:19 [inlined]
[8] (::HTTP.HeadersRequest.var"#defaultheaders#headerslayer##0"{HTTP.HeadersRequest.var"#defaultheaders#1#headerslayer##1"{HTTP.CookieRequest.var"#managecookies#cookielayer##0"{HTTP.CookieRequest.var"#managecookies#1#cookielayer##1"{HTTP.RetryRequest.var"#manageretries#retrylayer##0"{HTTP.RetryRequest.var"#manageretries#1#retrylayer##1"{HTTP.ConnectionRequest.var"#connections#connectionlayer##0"{HTTP.ConnectionRequest.var"#connections#1#connectionlayer##1"{HTTP.TimeoutRequest.var"#timeouts#timeoutlayer##0"{HTTP.TimeoutRequest.var"#timeouts#1#timeoutlayer##1"{HTTP.ExceptionRequest.var"#exceptions#exceptionlayer##0"{HTTP.ExceptionRequest.var"#exceptions#1#exceptionlayer##1"{typeof(HTTP.StreamRequest.streamlayer)}}}}}}}}}}}})(req::HTTP.Messages.Request; iofunction::Nothing, decompress::Nothing, basicauth::Bool, detect_content_type::Bool, canonicalize_headers::Bool, kw::@Kwargs{verbose::Int64, body_is_form::Bool})
@ HTTP.HeadersRequest ~/.julia/packages/HTTP/Y97L1/src/clientlayers/HeadersRequest.jl:71
[9] defaultheaders
@ ~/.julia/packages/HTTP/Y97L1/src/clientlayers/HeadersRequest.jl:14 [inlined]
[10] (::HTTP.RedirectRequest.var"#redirects#redirectlayer##0"{HTTP.RedirectRequest.var"#redirects#1#redirectlayer##1"{HTTP.HeadersRequest.var"#defaultheaders#headerslayer##0"{HTTP.HeadersRequest.var"#defaultheaders#1#headerslayer##1"{HTTP.CookieRequest.var"#managecookies#cookielayer##0"{HTTP.CookieRequest.var"#managecookies#1#cookielayer##1"{HTTP.RetryRequest.var"#manageretries#retrylayer##0"{HTTP.RetryRequest.var"#manageretries#1#retrylayer##1"{HTTP.ConnectionRequest.var"#connections#connectionlayer##0"{HTTP.ConnectionRequest.var"#connections#1#connectionlayer##1"{HTTP.TimeoutRequest.var"#timeouts#timeoutlayer##0"{HTTP.TimeoutRequest.var"#timeouts#1#timeoutlayer##1"{HTTP.ExceptionRequest.var"#exceptions#exceptionlayer##0"{HTTP.ExceptionRequest.var"#exceptions#1#exceptionlayer##1"{typeof(HTTP.StreamRequest.streamlayer)}}}}}}}}}}}}}})(req::HTTP.Messages.Request; redirect::Bool, redirect_limit::Int64, redirect_method::Nothing, forwardheaders::Bool, response_stream::Nothing, kw::@Kwargs{verbose::Int64, body_is_form::Bool})
@ HTTP.RedirectRequest ~/.julia/packages/HTTP/Y97L1/src/clientlayers/RedirectRequest.jl:25
[11] redirects
@ ~/.julia/packages/HTTP/Y97L1/src/clientlayers/RedirectRequest.jl:14 [inlined]
[12] (::HTTP.MessageRequest.var"#makerequest#messagelayer##0"{HTTP.MessageRequest.var"#makerequest#1#messagelayer##1"{HTTP.RedirectRequest.var"#redirects#redirectlayer##0"{HTTP.RedirectRequest.var"#redirects#1#redirectlayer##1"{HTTP.HeadersRequest.var"#defaultheaders#headerslayer##0"{HTTP.HeadersRequest.var"#defaultheaders#1#headerslayer##1"{HTTP.CookieRequest.var"#managecookies#cookielayer##0"{HTTP.CookieRequest.var"#managecookies#1#cookielayer##1"{HTTP.RetryRequest.var"#manageretries#retrylayer##0"{HTTP.RetryRequest.var"#manageretries#1#retrylayer##1"{HTTP.ConnectionRequest.var"#connections#connectionlayer##0"{HTTP.ConnectionRequest.var"#connections#1#connectionlayer##1"{HTTP.TimeoutRequest.var"#timeouts#timeoutlayer##0"{HTTP.TimeoutRequest.var"#timeouts#1#timeoutlayer##1"{HTTP.ExceptionRequest.var"#exceptions#exceptionlayer##0"{HTTP.ExceptionRequest.var"#exceptions#1#exceptionlayer##1"{typeof(HTTP.StreamRequest.streamlayer)}}}}}}}}}}}}}}}})(method::String, url::URIs.URI, headers::Vector{Pair{String, String}}, body::String; copyheaders::Bool, response_stream::Nothing, http_version::HTTP.Strings.HTTPVersion, verbose::Int64, kw::@Kwargs{body_is_form::Bool})
@ HTTP.MessageRequest ~/.julia/packages/HTTP/Y97L1/src/clientlayers/MessageRequest.jl:35
[13] makerequest
@ ~/.julia/packages/HTTP/Y97L1/src/clientlayers/MessageRequest.jl:24 [inlined]
[14] request(stack::HTTP.MessageRequest.var"#makerequest#messagelayer##0"{HTTP.MessageRequest.var"#makerequest#1#messagelayer##1"{HTTP.RedirectRequest.var"#redirects#redirectlayer##0"{HTTP.RedirectRequest.var"#redirects#1#redirectlayer##1"{HTTP.HeadersRequest.var"#defaultheaders#headerslayer##0"{HTTP.HeadersRequest.var"#defaultheaders#1#headerslayer##1"{HTTP.CookieRequest.var"#managecookies#cookielayer##0"{HTTP.CookieRequest.var"#managecookies#1#cookielayer##1"{HTTP.RetryRequest.var"#manageretries#retrylayer##0"{HTTP.RetryRequest.var"#manageretries#1#retrylayer##1"{HTTP.ConnectionRequest.var"#connections#connectionlayer##0"{HTTP.ConnectionRequest.var"#connections#1#connectionlayer##1"{HTTP.TimeoutRequest.var"#timeouts#timeoutlayer##0"{HTTP.TimeoutRequest.var"#timeouts#1#timeoutlayer##1"{HTTP.ExceptionRequest.var"#exceptions#exceptionlayer##0"{HTTP.ExceptionRequest.var"#exceptions#1#exceptionlayer##1"{typeof(HTTP.StreamRequest.streamlayer)}}}}}}}}}}}}}}}}, method::String, url::String, h::Vector{Pair{String, String}}, b::String, q::Nothing; headers::Vector{Pair{String, String}}, body::String, query::Nothing, kw::@Kwargs{body_is_form::Bool})
@ HTTP ~/.julia/packages/HTTP/Y97L1/src/HTTP.jl:465
[15] #request#21
@ ~/.julia/packages/HTTP/Y97L1/src/HTTP.jl:323 [inlined]
[16] request
@ ~/.julia/packages/HTTP/Y97L1/src/HTTP.jl:321 [inlined]
[17] plik_upload_handler(fileserver_url::String, dataname::String, data::Vector{UInt8})
@ Main ~/docker-apps/sommpanion/msghandler/test/test_julia_mix_payloads_sender.jl:48
[18] smartpack(subject::String, data::Vector{Tuple{String, Any, String}}; broker_url::String, fileserver_url::String, fileserver_upload_handler::typeof(plik_upload_handler), size_threshold::Int64, correlation_id::String, msg_purpose::String, sender_name::String, receiver_name::String, receiver_id::String, reply_to::String, reply_to_msg_id::String, msg_id::String, sender_id::String)
@ Main.msghandler ~/docker-apps/sommpanion/msghandler/src/msghandler.jl:493
[19] test_mix_send()
@ Main ~/docker-apps/sommpanion/msghandler/test/test_julia_mix_payloads_sender.jl:208
[20] top-level scope
@ ~/docker-apps/sommpanion/msghandler/test/test_julia_mix_payloads_sender.jl:256
[21] include(mod::Module, _path::String)
@ Base ./Base.jl:306
[22] exec_options(opts::Base.JLOptions)
@ Base ./client.jl:317
[23] _start()
@ Base ./client.jl:550
in expression starting at /home/ton/docker-apps/sommpanion/msghandler/test/test_julia_mix_payloads_sender.jl:256
caused by: IOError: SSL_ERROR_SSL
Stacktrace:
[1] macro expansion
@ ~/.julia/packages/OpenSSL/2SUGA/src/ssl.jl:476 [inlined]
[2] macro expansion
@ ./lock.jl:376 [inlined]
[3] connect(ssl::OpenSSL.SSLStream; require_ssl_verification::Bool)
@ OpenSSL ~/.julia/packages/OpenSSL/2SUGA/src/ssl.jl:443
[4] connect
@ ~/.julia/packages/OpenSSL/2SUGA/src/ssl.jl:509 [inlined]
[5] #sslconnection#21
@ ~/.julia/packages/HTTP/Y97L1/src/Connections.jl:595 [inlined]
[6] sslconnection
@ ~/.julia/packages/HTTP/Y97L1/src/Connections.jl:585 [inlined]
[7] getconnection(::Type{OpenSSL.SSLStream}, host::SubString{String}, port::SubString{String}; kw::@Kwargs{require_ssl_verification::Bool, keepalive::Bool, readtimeout::Int64, iofunction::Nothing, decompress::Nothing, verbose::Int64, body_is_form::Bool})
@ HTTP.Connections ~/.julia/packages/HTTP/Y97L1/src/Connections.jl:582
[8] getconnection
@ ~/.julia/packages/HTTP/Y97L1/src/Connections.jl:574 [inlined]
[9] #13
@ ~/.julia/packages/HTTP/Y97L1/src/Connections.jl:463 [inlined]
[10] macro expansion
@ ~/.julia/packages/ConcurrentUtilities/hQBsU/src/try_with_timeout.jl:92 [inlined]
[11] (::ConcurrentUtilities.var"#try_with_timeout##2#try_with_timeout##3"{Any, Channel{Any}, HTTP.Connections.var"#13#14"{OpenSSL.SSLStream, Bool, Bool, @Kwargs{readtimeout::Int64, iofunction::Nothing, decompress::Nothing, verbose::Int64, body_is_form::Bool}, SubString{String}, SubString{String}}, Timer})()
@ ConcurrentUtilities ~/.julia/packages/ConcurrentUtilities/hQBsU/src/ConcurrentUtilities.jl:10
Stacktrace:
[1] try_yieldto(undo::typeof(Base.ensure_rescheduled))
@ Base ./task.jl:1157
[2] wait()
@ Base ./task.jl:1229
[3] wait(c::Base.GenericCondition{ReentrantLock}; first::Bool)
@ Base ./condition.jl:141
[4] wait
@ ./condition.jl:136 [inlined]
[5] take_unbuffered(c::Channel{Any})
@ Base ./channels.jl:549
[6] take!
@ ./channels.jl:526 [inlined]
[7] try_with_timeout(f::Function, timeout::Int64, ::Type{Any})
@ ConcurrentUtilities ~/.julia/packages/ConcurrentUtilities/hQBsU/src/try_with_timeout.jl:99
[8] try_with_timeout
@ ~/.julia/packages/ConcurrentUtilities/hQBsU/src/try_with_timeout.jl:77 [inlined]
[9] (::HTTP.Connections.var"#11#12"{OpenSSL.SSLStream, Int64, Int64, Bool, Bool, @Kwargs{readtimeout::Int64, iofunction::Nothing, decompress::Nothing, verbose::Int64, body_is_form::Bool}, SubString{String}, SubString{String}})()
@ HTTP.Connections ~/.julia/packages/HTTP/Y97L1/src/Connections.jl:460
[10] acquire(f::HTTP.Connections.var"#11#12"{OpenSSL.SSLStream, Int64, Int64, Bool, Bool, @Kwargs{readtimeout::Int64, iofunction::Nothing, decompress::Nothing, verbose::Int64, body_is_form::Bool}, SubString{String}, SubString{String}}, pool::ConcurrentUtilities.Pools.Pool{Tuple{AbstractString, AbstractString, Bool, Bool, Bool}, HTTP.Connections.Connection{OpenSSL.SSLStream}}, key::Tuple{SubString{String}, SubString{String}, Bool, Bool, Bool}; forcenew::Bool, isvalid::HTTP.Connections.var"#15#16"{Int64})
@ ConcurrentUtilities.Pools ~/.julia/packages/ConcurrentUtilities/hQBsU/src/pools.jl:159
[11] acquire
@ ~/.julia/packages/ConcurrentUtilities/hQBsU/src/pools.jl:140 [inlined]
[12] #newconnection#7
@ ~/.julia/packages/HTTP/Y97L1/src/Connections.jl:455 [inlined]
[13] (::HTTP.ConnectionRequest.var"#connections#connectionlayer##0"{HTTP.ConnectionRequest.var"#connections#1#connectionlayer##1"{HTTP.TimeoutRequest.var"#timeouts#timeoutlayer##0"{HTTP.TimeoutRequest.var"#timeouts#1#timeoutlayer##1"{HTTP.ExceptionRequest.var"#exceptions#exceptionlayer##0"{HTTP.ExceptionRequest.var"#exceptions#1#exceptionlayer##1"{typeof(HTTP.StreamRequest.streamlayer)}}}}}})(req::HTTP.Messages.Request; proxy::Nothing, socket_type::Type, socket_type_tls::Nothing, readtimeout::Int64, connect_timeout::Int64, logerrors::Bool, logtag::Nothing, closeimmediately::Bool, kw::@Kwargs{iofunction::Nothing, decompress::Nothing, verbose::Int64, body_is_form::Bool})
@ HTTP.ConnectionRequest ~/.julia/packages/HTTP/Y97L1/src/clientlayers/ConnectionRequest.jl:82
[14] connections
@ ~/.julia/packages/HTTP/Y97L1/src/clientlayers/ConnectionRequest.jl:60 [inlined]
[15] (::Base.var"#46#47"{Base.var"#48#49"{ExponentialBackOff, HTTP.RetryRequest.var"#retrylayer##2#retrylayer##3"{Int64, typeof(HTTP.RetryRequest.FALSE), HTTP.Messages.Request, Base.RefValue{Int64}}, HTTP.ConnectionRequest.var"#connections#connectionlayer##0"{HTTP.ConnectionRequest.var"#connections#1#connectionlayer##1"{HTTP.TimeoutRequest.var"#timeouts#timeoutlayer##0"{HTTP.TimeoutRequest.var"#timeouts#1#timeoutlayer##1"{HTTP.ExceptionRequest.var"#exceptions#exceptionlayer##0"{HTTP.ExceptionRequest.var"#exceptions#1#exceptionlayer##1"{typeof(HTTP.StreamRequest.streamlayer)}}}}}}}})(args::HTTP.Messages.Request; kwargs::@Kwargs{iofunction::Nothing, decompress::Nothing, verbose::Int64, body_is_form::Bool})
@ Base ./error.jl:309
[16] (::HTTP.RetryRequest.var"#manageretries#retrylayer##0"{HTTP.RetryRequest.var"#manageretries#1#retrylayer##1"{HTTP.ConnectionRequest.var"#connections#connectionlayer##0"{HTTP.ConnectionRequest.var"#connections#1#connectionlayer##1"{HTTP.TimeoutRequest.var"#timeouts#timeoutlayer##0"{HTTP.TimeoutRequest.var"#timeouts#1#timeoutlayer##1"{HTTP.ExceptionRequest.var"#exceptions#exceptionlayer##0"{HTTP.ExceptionRequest.var"#exceptions#1#exceptionlayer##1"{typeof(HTTP.StreamRequest.streamlayer)}}}}}}}})(req::HTTP.Messages.Request; retry::Bool, retries::Int64, retry_delays::ExponentialBackOff, retry_check::Function, retry_non_idempotent::Bool, kw::@Kwargs{iofunction::Nothing, decompress::Nothing, verbose::Int64, body_is_form::Bool})
@ HTTP.RetryRequest ~/.julia/packages/HTTP/Y97L1/src/clientlayers/RetryRequest.jl:75
[17] manageretries
@ ~/.julia/packages/HTTP/Y97L1/src/clientlayers/RetryRequest.jl:30 [inlined]
[18] (::HTTP.CookieRequest.var"#managecookies#cookielayer##0"{HTTP.CookieRequest.var"#managecookies#1#cookielayer##1"{HTTP.RetryRequest.var"#manageretries#retrylayer##0"{HTTP.RetryRequest.var"#manageretries#1#retrylayer##1"{HTTP.ConnectionRequest.var"#connections#connectionlayer##0"{HTTP.ConnectionRequest.var"#connections#1#connectionlayer##1"{HTTP.TimeoutRequest.var"#timeouts#timeoutlayer##0"{HTTP.TimeoutRequest.var"#timeouts#1#timeoutlayer##1"{HTTP.ExceptionRequest.var"#exceptions#exceptionlayer##0"{HTTP.ExceptionRequest.var"#exceptions#1#exceptionlayer##1"{typeof(HTTP.StreamRequest.streamlayer)}}}}}}}}}})(req::HTTP.Messages.Request; cookies::Bool, cookiejar::HTTP.Cookies.CookieJar, kw::@Kwargs{iofunction::Nothing, decompress::Nothing, verbose::Int64, body_is_form::Bool})
@ HTTP.CookieRequest ~/.julia/packages/HTTP/Y97L1/src/clientlayers/CookieRequest.jl:42
[19] managecookies
@ ~/.julia/packages/HTTP/Y97L1/src/clientlayers/CookieRequest.jl:19 [inlined]
[20] (::HTTP.HeadersRequest.var"#defaultheaders#headerslayer##0"{HTTP.HeadersRequest.var"#defaultheaders#1#headerslayer##1"{HTTP.CookieRequest.var"#managecookies#cookielayer##0"{HTTP.CookieRequest.var"#managecookies#1#cookielayer##1"{HTTP.RetryRequest.var"#manageretries#retrylayer##0"{HTTP.RetryRequest.var"#manageretries#1#retrylayer##1"{HTTP.ConnectionRequest.var"#connections#connectionlayer##0"{HTTP.ConnectionRequest.var"#connections#1#connectionlayer##1"{HTTP.TimeoutRequest.var"#timeouts#timeoutlayer##0"{HTTP.TimeoutRequest.var"#timeouts#1#timeoutlayer##1"{HTTP.ExceptionRequest.var"#exceptions#exceptionlayer##0"{HTTP.ExceptionRequest.var"#exceptions#1#exceptionlayer##1"{typeof(HTTP.StreamRequest.streamlayer)}}}}}}}}}}}})(req::HTTP.Messages.Request; iofunction::Nothing, decompress::Nothing, basicauth::Bool, detect_content_type::Bool, canonicalize_headers::Bool, kw::@Kwargs{verbose::Int64, body_is_form::Bool})
@ HTTP.HeadersRequest ~/.julia/packages/HTTP/Y97L1/src/clientlayers/HeadersRequest.jl:71
[21] defaultheaders
@ ~/.julia/packages/HTTP/Y97L1/src/clientlayers/HeadersRequest.jl:14 [inlined]
[22] (::HTTP.RedirectRequest.var"#redirects#redirectlayer##0"{HTTP.RedirectRequest.var"#redirects#1#redirectlayer##1"{HTTP.HeadersRequest.var"#defaultheaders#headerslayer##0"{HTTP.HeadersRequest.var"#defaultheaders#1#headerslayer##1"{HTTP.CookieRequest.var"#managecookies#cookielayer##0"{HTTP.CookieRequest.var"#managecookies#1#cookielayer##1"{HTTP.RetryRequest.var"#manageretries#retrylayer##0"{HTTP.RetryRequest.var"#manageretries#1#retrylayer##1"{HTTP.ConnectionRequest.var"#connections#connectionlayer##0"{HTTP.ConnectionRequest.var"#connections#1#connectionlayer##1"{HTTP.TimeoutRequest.var"#timeouts#timeoutlayer##0"{HTTP.TimeoutRequest.var"#timeouts#1#timeoutlayer##1"{HTTP.ExceptionRequest.var"#exceptions#exceptionlayer##0"{HTTP.ExceptionRequest.var"#exceptions#1#exceptionlayer##1"{typeof(HTTP.StreamRequest.streamlayer)}}}}}}}}}}}}}})(req::HTTP.Messages.Request; redirect::Bool, redirect_limit::Int64, redirect_method::Nothing, forwardheaders::Bool, response_stream::Nothing, kw::@Kwargs{verbose::Int64, body_is_form::Bool})
@ HTTP.RedirectRequest ~/.julia/packages/HTTP/Y97L1/src/clientlayers/RedirectRequest.jl:25
[23] redirects
@ ~/.julia/packages/HTTP/Y97L1/src/clientlayers/RedirectRequest.jl:14 [inlined]
[24] (::HTTP.MessageRequest.var"#makerequest#messagelayer##0"{HTTP.MessageRequest.var"#makerequest#1#messagelayer##1"{HTTP.RedirectRequest.var"#redirects#redirectlayer##0"{HTTP.RedirectRequest.var"#redirects#1#redirectlayer##1"{HTTP.HeadersRequest.var"#defaultheaders#headerslayer##0"{HTTP.HeadersRequest.var"#defaultheaders#1#headerslayer##1"{HTTP.CookieRequest.var"#managecookies#cookielayer##0"{HTTP.CookieRequest.var"#managecookies#1#cookielayer##1"{HTTP.RetryRequest.var"#manageretries#retrylayer##0"{HTTP.RetryRequest.var"#manageretries#1#retrylayer##1"{HTTP.ConnectionRequest.var"#connections#connectionlayer##0"{HTTP.ConnectionRequest.var"#connections#1#connectionlayer##1"{HTTP.TimeoutRequest.var"#timeouts#timeoutlayer##0"{HTTP.TimeoutRequest.var"#timeouts#1#timeoutlayer##1"{HTTP.ExceptionRequest.var"#exceptions#exceptionlayer##0"{HTTP.ExceptionRequest.var"#exceptions#1#exceptionlayer##1"{typeof(HTTP.StreamRequest.streamlayer)}}}}}}}}}}}}}}}})(method::String, url::URIs.URI, headers::Vector{Pair{String, String}}, body::String; copyheaders::Bool, response_stream::Nothing, http_version::HTTP.Strings.HTTPVersion, verbose::Int64, kw::@Kwargs{body_is_form::Bool})
@ HTTP.MessageRequest ~/.julia/packages/HTTP/Y97L1/src/clientlayers/MessageRequest.jl:35
[25] makerequest
@ ~/.julia/packages/HTTP/Y97L1/src/clientlayers/MessageRequest.jl:24 [inlined]
[26] request(stack::HTTP.MessageRequest.var"#makerequest#messagelayer##0"{HTTP.MessageRequest.var"#makerequest#1#messagelayer##1"{HTTP.RedirectRequest.var"#redirects#redirectlayer##0"{HTTP.RedirectRequest.var"#redirects#1#redirectlayer##1"{HTTP.HeadersRequest.var"#defaultheaders#headerslayer##0"{HTTP.HeadersRequest.var"#defaultheaders#1#headerslayer##1"{HTTP.CookieRequest.var"#managecookies#cookielayer##0"{HTTP.CookieRequest.var"#managecookies#1#cookielayer##1"{HTTP.RetryRequest.var"#manageretries#retrylayer##0"{HTTP.RetryRequest.var"#manageretries#1#retrylayer##1"{HTTP.ConnectionRequest.var"#connections#connectionlayer##0"{HTTP.ConnectionRequest.var"#connections#1#connectionlayer##1"{HTTP.TimeoutRequest.var"#timeouts#timeoutlayer##0"{HTTP.TimeoutRequest.var"#timeouts#1#timeoutlayer##1"{HTTP.ExceptionRequest.var"#exceptions#exceptionlayer##0"{HTTP.ExceptionRequest.var"#exceptions#1#exceptionlayer##1"{typeof(HTTP.StreamRequest.streamlayer)}}}}}}}}}}}}}}}}, method::String, url::String, h::Vector{Pair{String, String}}, b::String, q::Nothing; headers::Vector{Pair{String, String}}, body::String, query::Nothing, kw::@Kwargs{body_is_form::Bool})
@ HTTP ~/.julia/packages/HTTP/Y97L1/src/HTTP.jl:465
[27] #request#21
@ ~/.julia/packages/HTTP/Y97L1/src/HTTP.jl:323 [inlined]
[28] request
@ ~/.julia/packages/HTTP/Y97L1/src/HTTP.jl:321 [inlined]
[29] plik_upload_handler(fileserver_url::String, dataname::String, data::Vector{UInt8})
@ Main ~/docker-apps/sommpanion/msghandler/test/test_julia_mix_payloads_sender.jl:48
[30] smartpack(subject::String, data::Vector{Tuple{String, Any, String}}; broker_url::String, fileserver_url::String, fileserver_upload_handler::typeof(plik_upload_handler), size_threshold::Int64, correlation_id::String, msg_purpose::String, sender_name::String, receiver_name::String, receiver_id::String, reply_to::String, reply_to_msg_id::String, msg_id::String, sender_id::String)
@ Main.msghandler ~/docker-apps/sommpanion/msghandler/src/msghandler.jl:493
[31] test_mix_send()
@ Main ~/docker-apps/sommpanion/msghandler/test/test_julia_mix_payloads_sender.jl:208
[32] top-level scope
@ ~/docker-apps/sommpanion/msghandler/test/test_julia_mix_payloads_sender.jl:256
[33] include(mod::Module, _path::String)
@ Base ./Base.jl:306
[34] exec_options(opts::Base.JLOptions)
@ Base ./client.jl:317
[35] _start()
@ Base ./client.jl:550
---
This is my Caddyfile:
fileserver.mydomain.cc {
# 1. Handle OPTIONS preflight requests specifically
@options {
method OPTIONS
}
respond @options 204
# 2. Add CORS headers to all responses (including those from the proxy)
header {
Access-Control-Allow-Origin "*"
Access-Control-Allow-Methods "GET, HEAD, OPTIONS, POST, PUT, DELETE"
Access-Control-Allow-Headers "*"
Access-Control-Max-Age "86400"
}
# 3. Proxy to your Plik server
reverse_proxy 192.168.88.104:8080 {
header_up Host {http.request.host}
header_up X-Forwarded-For {http.request.remote}
header_up X-Forwarded-Proto {http.request.scheme}
}
}
---
What do you think?

1
Cargo.lock generated
View File

@@ -906,6 +906,7 @@ dependencies = [
"base64", "base64",
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-channel",
"futures-core", "futures-core",
"futures-util", "futures-util",
"h2", "h2",

View File

@@ -2,7 +2,7 @@
name = "msghandler" name = "msghandler"
version = "1.2.0" version = "1.2.0"
edition = "2021" edition = "2021"
description = "Cross-platform bi-directional data bridge for NATS communication" description = "Cross-platform bi-directional data bridge"
[lib] [lib]
name = "msghandler" name = "msghandler"
@@ -12,7 +12,7 @@ path = "src/msghandler.rs"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json", "stream", "multipart"] } reqwest = { version = "0.12", features = ["json", "stream", "multipart", "blocking"] }
uuid = { version = "1", features = ["v4", "serde"] } uuid = { version = "1", features = ["v4", "serde"] }
base64 = "0.22" base64 = "0.22"
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
@@ -23,9 +23,9 @@ futures = "0.3"
tempfile = "3" tempfile = "3"
[[example]] [[example]]
name = "smartsend_example" name = "smartpack_example"
path = "examples/smartsend_example.rs" path = "examples/smartpack_example.rs"
[[example]] [[example]]
name = "smartreceive_example" name = "smartunpack_example"
path = "examples/smartreceive_example.rs" path = "examples/smartunpack_example.rs"

View File

@@ -1,8 +1,8 @@
# This file is machine-generated - editing it directly is not advised # This file is machine-generated - editing it directly is not advised
julia_version = "1.12.5" julia_version = "1.12.6"
manifest_format = "2.0" manifest_format = "2.0"
project_hash = "b632f853bcf5355f5c53ad3efa7a19f70444dc6c" project_hash = "ec31595f278190cb6cfb8b50156867ebf16234d0"
[[deps.AliasTables]] [[deps.AliasTables]]
deps = ["PtrArrays", "Random"] deps = ["PtrArrays", "Random"]
@@ -52,15 +52,15 @@ version = "1.2.2"
[[deps.CSV]] [[deps.CSV]]
deps = ["CodecZlib", "Dates", "FilePathsBase", "InlineStrings", "Mmap", "Parsers", "PooledArrays", "PrecompileTools", "SentinelArrays", "Tables", "Unicode", "WeakRefStrings", "WorkerUtilities"] deps = ["CodecZlib", "Dates", "FilePathsBase", "InlineStrings", "Mmap", "Parsers", "PooledArrays", "PrecompileTools", "SentinelArrays", "Tables", "Unicode", "WeakRefStrings", "WorkerUtilities"]
git-tree-sha1 = "deddd8725e5e1cc49ee205a1964256043720a6c3" git-tree-sha1 = "8d8e0b0f350b8e1c91420b5e64e5de774c2f0f4d"
uuid = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" uuid = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
version = "0.10.15" version = "0.10.16"
[[deps.CodeTracking]] [[deps.CodeTracking]]
deps = ["InteractiveUtils", "UUIDs"] deps = ["InteractiveUtils", "REPL", "UUIDs"]
git-tree-sha1 = "b7231a755812695b8046e8471ddc34c8268cbad5" git-tree-sha1 = "cfb7a2e89e245a9d5016b70323db412b3a7438d5"
uuid = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" uuid = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2"
version = "3.0.0" version = "3.0.2"
[[deps.CodecBase]] [[deps.CodecBase]]
deps = ["TranscodingStreams"] deps = ["TranscodingStreams"]
@@ -108,9 +108,9 @@ version = "1.3.0+1"
[[deps.ConcurrentUtilities]] [[deps.ConcurrentUtilities]]
deps = ["Serialization", "Sockets"] deps = ["Serialization", "Sockets"]
git-tree-sha1 = "d9d26935a0bcffc87d2613ce14c527c99fc543fd" git-tree-sha1 = "21d088c496ea22914fe80906eb5bce65755e5ec8"
uuid = "f0e56b4a-5159-44fe-b623-3e5288b988bb" uuid = "f0e56b4a-5159-44fe-b623-3e5288b988bb"
version = "2.5.0" version = "2.5.1"
[[deps.Crayons]] [[deps.Crayons]]
git-tree-sha1 = "249fe38abf76d48563e2f4556bebd215aa317e15" git-tree-sha1 = "249fe38abf76d48563e2f4556bebd215aa317e15"
@@ -124,15 +124,15 @@ version = "1.16.0"
[[deps.DataFrames]] [[deps.DataFrames]]
deps = ["Compat", "DataAPI", "DataStructures", "Future", "InlineStrings", "InvertedIndices", "IteratorInterfaceExtensions", "LinearAlgebra", "Markdown", "Missings", "PooledArrays", "PrecompileTools", "PrettyTables", "Printf", "Random", "Reexport", "SentinelArrays", "SortingAlgorithms", "Statistics", "TableTraits", "Tables", "Unicode"] deps = ["Compat", "DataAPI", "DataStructures", "Future", "InlineStrings", "InvertedIndices", "IteratorInterfaceExtensions", "LinearAlgebra", "Markdown", "Missings", "PooledArrays", "PrecompileTools", "PrettyTables", "Printf", "Random", "Reexport", "SentinelArrays", "SortingAlgorithms", "Statistics", "TableTraits", "Tables", "Unicode"]
git-tree-sha1 = "d8928e9169ff76c6281f39a659f9bca3a573f24c" git-tree-sha1 = "5fab31e2e01e70ad66e3e24c968c264d1cf166d6"
uuid = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0" uuid = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
version = "1.8.1" version = "1.8.2"
[[deps.DataStructures]] [[deps.DataStructures]]
deps = ["OrderedCollections"] deps = ["OrderedCollections"]
git-tree-sha1 = "e357641bb3e0638d353c4b29ea0e40ea644066a6" git-tree-sha1 = "e86f4a2805f7f19bec5129bc9150c38208e5dc23"
uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
version = "0.19.3" version = "0.19.4"
[[deps.DataValueInterfaces]] [[deps.DataValueInterfaces]]
git-tree-sha1 = "bfc1187b79289637fa0ef6d4436ebdfe6905cbd6" git-tree-sha1 = "bfc1187b79289637fa0ef6d4436ebdfe6905cbd6"
@@ -146,9 +146,9 @@ version = "1.11.0"
[[deps.Distributions]] [[deps.Distributions]]
deps = ["AliasTables", "FillArrays", "LinearAlgebra", "PDMats", "Printf", "QuadGK", "Random", "SpecialFunctions", "Statistics", "StatsAPI", "StatsBase", "StatsFuns"] deps = ["AliasTables", "FillArrays", "LinearAlgebra", "PDMats", "Printf", "QuadGK", "Random", "SpecialFunctions", "Statistics", "StatsAPI", "StatsBase", "StatsFuns"]
git-tree-sha1 = "fbcc7610f6d8348428f722ecbe0e6cfe22e672c6" git-tree-sha1 = "e421c1938fafab0165b04dc1a9dbe2a26272952c"
uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f"
version = "0.25.123" version = "0.25.125"
[deps.Distributions.extensions] [deps.Distributions.extensions]
DistributionsChainRulesCoreExt = "ChainRulesCore" DistributionsChainRulesCoreExt = "ChainRulesCore"
@@ -171,9 +171,9 @@ uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
version = "1.7.0" version = "1.7.0"
[[deps.EnumX]] [[deps.EnumX]]
git-tree-sha1 = "7bebc8aad6ee6217c78c5ddcf7ed289d65d0263e" git-tree-sha1 = "c49898e8438c828577f04b92fc9368c388ac783c"
uuid = "4e289a0a-7415-4d19-859d-a7e5c4648b56" uuid = "4e289a0a-7415-4d19-859d-a7e5c4648b56"
version = "1.0.6" version = "1.0.7"
[[deps.ExceptionUnwrapping]] [[deps.ExceptionUnwrapping]]
deps = ["Test"] deps = ["Test"]
@@ -234,9 +234,9 @@ version = "0.3.1"
[[deps.HTTP]] [[deps.HTTP]]
deps = ["Base64", "CodecZlib", "ConcurrentUtilities", "Dates", "ExceptionUnwrapping", "Logging", "LoggingExtras", "MbedTLS", "NetworkOptions", "OpenSSL", "PrecompileTools", "Random", "SimpleBufferStream", "Sockets", "URIs", "UUIDs"] deps = ["Base64", "CodecZlib", "ConcurrentUtilities", "Dates", "ExceptionUnwrapping", "Logging", "LoggingExtras", "MbedTLS", "NetworkOptions", "OpenSSL", "PrecompileTools", "Random", "SimpleBufferStream", "Sockets", "URIs", "UUIDs"]
git-tree-sha1 = "5e6fe50ae7f23d171f44e311c2960294aaa0beb5" git-tree-sha1 = "51059d23c8bb67911a2e6fd5130229113735fc7e"
uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3" uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3"
version = "1.10.19" version = "1.11.0"
[[deps.HashArrayMappedTries]] [[deps.HashArrayMappedTries]]
git-tree-sha1 = "2eaa69a7cab70a52b9687c8bf950a5a93ec895ae" git-tree-sha1 = "2eaa69a7cab70a52b9687c8bf950a5a93ec895ae"
@@ -281,15 +281,15 @@ version = "1.0.0"
[[deps.JLLWrappers]] [[deps.JLLWrappers]]
deps = ["Artifacts", "Preferences"] deps = ["Artifacts", "Preferences"]
git-tree-sha1 = "0533e564aae234aff59ab625543145446d8b6ec2" git-tree-sha1 = "7204148362dafe5fe6a273f855b8ccbe4df8173e"
uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210"
version = "1.7.1" version = "1.8.0"
[[deps.JSON]] [[deps.JSON]]
deps = ["Dates", "Logging", "Parsers", "PrecompileTools", "StructUtils", "UUIDs", "Unicode"] deps = ["Dates", "Logging", "Parsers", "PrecompileTools", "StructUtils", "UUIDs", "Unicode"]
git-tree-sha1 = "b3ad4a0255688dcb895a52fafbaae3023b588a90" git-tree-sha1 = "f76f7560267b840e492180f9899b472f30b88450"
uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
version = "1.4.0" version = "1.6.0"
weakdeps = ["ArrowTypes"] weakdeps = ["ArrowTypes"]
[deps.JSON.extensions] [deps.JSON.extensions]
@@ -307,9 +307,9 @@ weakdeps = ["ArrowTypes"]
[[deps.JuliaInterpreter]] [[deps.JuliaInterpreter]]
deps = ["CodeTracking", "InteractiveUtils", "Random", "UUIDs"] deps = ["CodeTracking", "InteractiveUtils", "Random", "UUIDs"]
git-tree-sha1 = "80580012d4ed5a3e8b18c7cd86cebe4b816d17a6" git-tree-sha1 = "58927c485919bf17ea308d9d82156de1adf4b006"
uuid = "aa1ae85d-cabe-5617-a682-6adf51b2e16a" uuid = "aa1ae85d-cabe-5617-a682-6adf51b2e16a"
version = "0.10.9" version = "0.10.12"
[[deps.JuliaSyntaxHighlighting]] [[deps.JuliaSyntaxHighlighting]]
deps = ["StyledStrings"] deps = ["StyledStrings"]
@@ -383,9 +383,9 @@ version = "1.2.0"
[[deps.LoweredCodeUtils]] [[deps.LoweredCodeUtils]]
deps = ["CodeTracking", "Compiler", "JuliaInterpreter"] deps = ["CodeTracking", "Compiler", "JuliaInterpreter"]
git-tree-sha1 = "65ae3db6ab0e5b1b5f217043c558d9d1d33cc88d" git-tree-sha1 = "5d4278f755440f70648d80cc6225f51e78e94094"
uuid = "6f1432cf-f94c-5a45-995e-cdbf5db27b0b" uuid = "6f1432cf-f94c-5a45-995e-cdbf5db27b0b"
version = "3.5.0" version = "3.5.1"
[[deps.Lz4_jll]] [[deps.Lz4_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl"] deps = ["Artifacts", "JLLWrappers", "Libdl"]
@@ -400,9 +400,9 @@ version = "1.11.0"
[[deps.MbedTLS]] [[deps.MbedTLS]]
deps = ["Dates", "MbedTLS_jll", "MozillaCACerts_jll", "NetworkOptions", "Random", "Sockets"] deps = ["Dates", "MbedTLS_jll", "MozillaCACerts_jll", "NetworkOptions", "Random", "Sockets"]
git-tree-sha1 = "c067a280ddc25f196b5e7df3877c6b226d390aaf" git-tree-sha1 = "8785729fa736197687541f7053f6d8ab7fc44f92"
uuid = "739be429-bea8-5141-9913-cc70e7f3736d" uuid = "739be429-bea8-5141-9913-cc70e7f3736d"
version = "1.1.9" version = "1.1.10"
[[deps.MbedTLS_jll]] [[deps.MbedTLS_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl"] deps = ["Artifacts", "JLLWrappers", "Libdl"]
@@ -432,15 +432,9 @@ version = "2025.11.4"
[[deps.NATS]] [[deps.NATS]]
deps = ["Base64", "BufferedStreams", "CodecBase", "Dates", "DocStringExtensions", "JSON3", "MbedTLS", "NanoDates", "Random", "ScopedValues", "Sockets", "Sodium", "StructTypes", "URIs"] deps = ["Base64", "BufferedStreams", "CodecBase", "Dates", "DocStringExtensions", "JSON3", "MbedTLS", "NanoDates", "Random", "ScopedValues", "Sockets", "Sodium", "StructTypes", "URIs"]
git-tree-sha1 = "d9d9a189fb9155a460e6b5e8966bf6a66737abf8" git-tree-sha1 = "a1cdf34ba90ee5cd2658e487d3277ffafee712ce"
uuid = "55e73f9c-eeeb-467f-b4cc-a633fde63d2a" uuid = "55e73f9c-eeeb-467f-b4cc-a633fde63d2a"
version = "0.1.0" version = "0.1.1"
[[deps.msghandler]]
deps = ["Arrow", "DataFrames", "Dates", "GeneralUtils", "HTTP", "JSON", "NATS", "PrettyPrinting", "Revise", "UUIDs"]
path = "."
uuid = "f2724d33-f338-4a57-b9f8-1be882570d10"
version = "0.4.1"
[[deps.NanoDates]] [[deps.NanoDates]]
deps = ["Dates", "Parsers"] deps = ["Dates", "Parsers"]
@@ -496,9 +490,9 @@ weakdeps = ["StatsBase"]
[[deps.Parsers]] [[deps.Parsers]]
deps = ["Dates", "PrecompileTools", "UUIDs"] deps = ["Dates", "PrecompileTools", "UUIDs"]
git-tree-sha1 = "7d2f8f21da5db6a806faf7b9b292296da42b2810" git-tree-sha1 = "5d5e0a78e971354b1c7bff0655d11fdc1b0e12c8"
uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0"
version = "2.8.3" version = "2.8.4"
[[deps.PooledArrays]] [[deps.PooledArrays]]
deps = ["DataAPI", "Future"] deps = ["DataAPI", "Future"]
@@ -508,15 +502,15 @@ version = "1.4.3"
[[deps.PrecompileTools]] [[deps.PrecompileTools]]
deps = ["Preferences"] deps = ["Preferences"]
git-tree-sha1 = "07a921781cab75691315adc645096ed5e370cb77" git-tree-sha1 = "edbeefc7a4889f528644251bdb5fc9ab5348bc2c"
uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
version = "1.3.3" version = "1.3.4"
[[deps.Preferences]] [[deps.Preferences]]
deps = ["TOML"] deps = ["TOML"]
git-tree-sha1 = "522f093a29b31a93e34eaea17ba055d850edea28" git-tree-sha1 = "8b770b60760d4451834fe79dd483e318eee709c4"
uuid = "21216c6a-2e73-6563-6e65-726566657250" uuid = "21216c6a-2e73-6563-6e65-726566657250"
version = "1.5.1" version = "1.5.2"
[[deps.PrettyPrinting]] [[deps.PrettyPrinting]]
git-tree-sha1 = "142ee93724a9c5d04d78df7006670a93ed1b244e" git-tree-sha1 = "142ee93724a9c5d04d78df7006670a93ed1b244e"
@@ -525,9 +519,15 @@ version = "0.4.2"
[[deps.PrettyTables]] [[deps.PrettyTables]]
deps = ["Crayons", "LaTeXStrings", "Markdown", "PrecompileTools", "Printf", "REPL", "Reexport", "StringManipulation", "Tables"] deps = ["Crayons", "LaTeXStrings", "Markdown", "PrecompileTools", "Printf", "REPL", "Reexport", "StringManipulation", "Tables"]
git-tree-sha1 = "c5a07210bd060d6a8491b0ccdee2fa0235fc00bf" git-tree-sha1 = "624de6279ab7d94fc9f672f0068107eb6619732c"
uuid = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d" uuid = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"
version = "3.1.2" version = "3.3.2"
[deps.PrettyTables.extensions]
PrettyTablesTypstryExt = "Typstry"
[deps.PrettyTables.weakdeps]
Typstry = "f0ed7684-a786-439e-b1e3-3b82803b501e"
[[deps.Printf]] [[deps.Printf]]
deps = ["Unicode"] deps = ["Unicode"]
@@ -535,15 +535,15 @@ uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7"
version = "1.11.0" version = "1.11.0"
[[deps.PtrArrays]] [[deps.PtrArrays]]
git-tree-sha1 = "1d36ef11a9aaf1e8b74dacc6a731dd1de8fd493d" git-tree-sha1 = "4fbbafbc6251b883f4d2705356f3641f3652a7fe"
uuid = "43287f4e-b6f4-7ad1-bb20-aadabca52c3d" uuid = "43287f4e-b6f4-7ad1-bb20-aadabca52c3d"
version = "1.3.0" version = "1.4.0"
[[deps.QuadGK]] [[deps.QuadGK]]
deps = ["DataStructures", "LinearAlgebra"] deps = ["DataStructures", "LinearAlgebra"]
git-tree-sha1 = "9da16da70037ba9d701192e27befedefb91ec284" git-tree-sha1 = "5e8e8b0ab68215d7a2b14b9921a946fee794749e"
uuid = "1fd47b50-473d-5c70-9696-f719f8f3bcdc" uuid = "1fd47b50-473d-5c70-9696-f719f8f3bcdc"
version = "2.11.2" version = "2.11.3"
[deps.QuadGK.extensions] [deps.QuadGK.extensions]
QuadGKEnzymeExt = "Enzyme" QuadGKEnzymeExt = "Enzyme"
@@ -568,9 +568,9 @@ version = "1.2.2"
[[deps.Revise]] [[deps.Revise]]
deps = ["CodeTracking", "FileWatching", "InteractiveUtils", "JuliaInterpreter", "LibGit2", "LoweredCodeUtils", "OrderedCollections", "Preferences", "REPL", "UUIDs"] deps = ["CodeTracking", "FileWatching", "InteractiveUtils", "JuliaInterpreter", "LibGit2", "LoweredCodeUtils", "OrderedCollections", "Preferences", "REPL", "UUIDs"]
git-tree-sha1 = "14d1bfb0a30317edc77e11094607ace3c800f193" git-tree-sha1 = "d9383b639663d8220ac9c523927e38bc21cad16a"
uuid = "295af30f-e4ad-537b-8983-00126c2a3abe" uuid = "295af30f-e4ad-537b-8983-00126c2a3abe"
version = "3.13.2" version = "3.14.3"
[deps.Revise.extensions] [deps.Revise.extensions]
DistributedExt = "Distributed" DistributedExt = "Distributed"
@@ -596,9 +596,9 @@ version = "0.7.0"
[[deps.ScopedValues]] [[deps.ScopedValues]]
deps = ["HashArrayMappedTries", "Logging"] deps = ["HashArrayMappedTries", "Logging"]
git-tree-sha1 = "c3b2323466378a2ba15bea4b2f73b081e022f473" git-tree-sha1 = "67a144433c4ce877ee6d1ada69a124d6b1ecf7be"
uuid = "7e506255-f358-4e82-b7e4-beb19740aa63" uuid = "7e506255-f358-4e82-b7e4-beb19740aa63"
version = "1.5.0" version = "1.6.2"
[[deps.Scratch]] [[deps.Scratch]]
deps = ["Dates"] deps = ["Dates"]
@@ -608,9 +608,9 @@ version = "1.3.0"
[[deps.SentinelArrays]] [[deps.SentinelArrays]]
deps = ["Dates", "Random"] deps = ["Dates", "Random"]
git-tree-sha1 = "ebe7e59b37c400f694f52b58c93d26201387da70" git-tree-sha1 = "084c47c7c5ce5cfecefa0a98dff69eb3646b5a80"
uuid = "91c51154-3ec4-41a3-a24f-3f23e20d615c" uuid = "91c51154-3ec4-41a3-a24f-3f23e20d615c"
version = "1.4.9" version = "1.4.10"
[[deps.Serialization]] [[deps.Serialization]]
uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
@@ -644,9 +644,9 @@ version = "1.12.0"
[[deps.SpecialFunctions]] [[deps.SpecialFunctions]]
deps = ["IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"] deps = ["IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"]
git-tree-sha1 = "f2685b435df2613e25fc10ad8c26dddb8640f547" git-tree-sha1 = "2700b235561b0335d5bef7097a111dc513b8655e"
uuid = "276daf66-3868-5448-9aa4-cd146d93841b" uuid = "276daf66-3868-5448-9aa4-cd146d93841b"
version = "2.6.1" version = "2.7.2"
[deps.SpecialFunctions.extensions] [deps.SpecialFunctions.extensions]
SpecialFunctionsChainRulesCoreExt = "ChainRulesCore" SpecialFunctionsChainRulesCoreExt = "ChainRulesCore"
@@ -692,9 +692,9 @@ version = "1.5.2"
[[deps.StringManipulation]] [[deps.StringManipulation]]
deps = ["PrecompileTools"] deps = ["PrecompileTools"]
git-tree-sha1 = "a3c1536470bf8c5e02096ad4853606d7c8f62721" git-tree-sha1 = "d05693d339e37d6ab134c5ab53c29fce5ee5d7d5"
uuid = "892a3eda-7b42-436c-8928-eab12a02cf0e" uuid = "892a3eda-7b42-436c-8928-eab12a02cf0e"
version = "0.4.2" version = "0.4.4"
[[deps.StringViews]] [[deps.StringViews]]
git-tree-sha1 = "f2dcb92855b31ad92fe8f079d4f75ac57c93e4b8" git-tree-sha1 = "f2dcb92855b31ad92fe8f079d4f75ac57c93e4b8"
@@ -709,16 +709,18 @@ version = "1.11.0"
[[deps.StructUtils]] [[deps.StructUtils]]
deps = ["Dates", "UUIDs"] deps = ["Dates", "UUIDs"]
git-tree-sha1 = "9297459be9e338e546f5c4bedb59b3b5674da7f1" git-tree-sha1 = "82bee338d650aa515f31866c460cb7e3bcef90b8"
uuid = "ec057cc2-7a8d-4b58-b3b3-92acb9f63b42" uuid = "ec057cc2-7a8d-4b58-b3b3-92acb9f63b42"
version = "2.6.2" version = "2.8.2"
[deps.StructUtils.extensions] [deps.StructUtils.extensions]
StructUtilsMeasurementsExt = ["Measurements"] StructUtilsMeasurementsExt = ["Measurements"]
StructUtilsStaticArraysCoreExt = ["StaticArraysCore"]
StructUtilsTablesExt = ["Tables"] StructUtilsTablesExt = ["Tables"]
[deps.StructUtils.weakdeps] [deps.StructUtils.weakdeps]
Measurements = "eff96d63-e80a-5855-80a2-b1b0885c5ab7" Measurements = "eff96d63-e80a-5855-80a2-b1b0885c5ab7"
StaticArraysCore = "1e83bf80-4336-4d27-bf5d-d5a4f845583c"
Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
[[deps.StyledStrings]] [[deps.StyledStrings]]
@@ -795,9 +797,9 @@ version = "1.11.0"
[[deps.WeakRefStrings]] [[deps.WeakRefStrings]]
deps = ["DataAPI", "InlineStrings", "Parsers"] deps = ["DataAPI", "InlineStrings", "Parsers"]
git-tree-sha1 = "b1be2855ed9ed8eac54e5caff2afcdb442d52c23" git-tree-sha1 = "0716e01c3b40413de5dedbc9c5c69f27cddfddfc"
uuid = "ea10d353-3f73-51f8-a26c-33c1cb351aa5" uuid = "ea10d353-3f73-51f8-a26c-33c1cb351aa5"
version = "1.4.2" version = "1.4.3"
[[deps.WorkerUtilities]] [[deps.WorkerUtilities]]
git-tree-sha1 = "cd1659ba0d57b71a464a29e64dbc67cfe83d54e7" git-tree-sha1 = "cd1659ba0d57b71a464a29e64dbc67cfe83d54e7"
@@ -826,6 +828,12 @@ git-tree-sha1 = "011b0a7331b41c25524b64dc42afc9683ee89026"
uuid = "a9144af2-ca23-56d9-984f-0d03f7b5ccf8" uuid = "a9144af2-ca23-56d9-984f-0d03f7b5ccf8"
version = "1.0.21+0" version = "1.0.21+0"
[[deps.msghandler]]
deps = ["Arrow", "Base64", "DataFrames", "Dates", "GeneralUtils", "HTTP", "JSON", "NATS", "PrettyPrinting", "Revise", "UUIDs"]
path = "."
uuid = "f2724d33-f338-4a57-b9f8-1be882570d10"
version = "0.5.6"
[[deps.nghttp2_jll]] [[deps.nghttp2_jll]]
deps = ["Artifacts", "Libdl"] deps = ["Artifacts", "Libdl"]
uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d"

View File

@@ -18,4 +18,6 @@ UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
[compat] [compat]
Base64 = "1.11.0" Base64 = "1.11.0"
Dates = "1.11.0"
GeneralUtils = "0.3 - 0.3.1"
JSON = "1.4.0" JSON = "1.4.0"

1303
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,941 +0,0 @@
# Architecture Documentation: msghandler
**Version**: 1.4.0
**Date**: 2026-05-14
**Status**: Active
**Ground Truth**: [`src/msghandler.jl`](../src/msghandler.jl)
**Architecture Level**: C4 Container Level
---
## 1. Executive Summary
This document defines the **blueprint** for msghandler - the cross-platform bi-directional data bridge that enables seamless communication between **Julia**, **JavaScript**, **Python**, **Dart**, **Rust**, and **MicroPython** applications using NATS as the message bus.
This architecture document serves as the single source of truth for:
- **System Structure**: How components fit together and interact
- **Scaling Considerations**: How the system scales horizontally and vertically
- **Failure Modes**: How the system handles failures and recovers
- **Trade-off Decisions**: The rationale behind architectural decisions
### 1.1 Specification Traceability
| Architecture Section | Specification Reference | UI Specification Reference | Requirement ID(s) |
|---------------------|-------------------------|---------------------------|-------------------|
| Section 2 (Context Diagram) | specification.md:2 | - | FR-001, FR-002, FR-003, FR-004, FR-005, FR-006, FR-007, FR-012, FR-013, FR-014 |
| Section 3 (Container Diagram) | specification.md:2, specification.md:3, specification.md:11 | - | FR-001, FR-002, FR-003, FR-004, FR-005, FR-006, FR-007, FR-012, FR-013, FR-014 |
| Section 4 (Component Diagram) | specification.md:2, specification.md:3, specification.md:5, specification.md:11 | - | FR-001, FR-002, FR-003, FR-004, FR-005, FR-006, FR-007, FR-012, FR-013, FR-014 |
| Section 5 (High-Level) | specification.md:2, specification.md:3, specification.md:5, specification.md:11 | - | FR-001, FR-002, FR-003, FR-004, FR-005, FR-006, FR-007, FR-012, FR-013, FR-014 |
| Section 6 (Message Envelope) | specification.md:2, specification.md:3, specification.md:8 | - | FR-011, FR-012, FR-013, FR-014, NFR-401, NFR-403 |
| Section 7 (Payload Type) | specification.md:3, specification.md:5, specification.md:6 | - | FR-001, FR-002, FR-003, FR-006, FR-012, NFR-101, NFR-102 |
| Section 8 (Transport Strategy) | specification.md:6, specification.md:7 | - | FR-003, FR-004, FR-005, FR-010, NFR-104, NFR-105, NFR-106 |
| Section 9 (Platform-Specific) | specification.md:13, specification.md:14 | - | FR-001, FR-002, FR-003, FR-004, FR-005, FR-006, FR-007, FR-012, FR-013, FR-014 |
| Section 10 (Scaling) | specification.md:7, specification.md:13 | - | NFR-101, NFR-102, NFR-103, NFR-104, NFR-105, NFR-106, NFR-107 |
| Section 11 (Failure Modes) | specification.md:9, specification.md:11 | - | FR-008, FR-009, FR-010, FR-011, NFR-201, NFR-202, NFR-203 |
| Section 12 (Trade-offs) | specification.md:2, specification.md:3, specification.md:6, specification.md:7 | - | FR-001, FR-002, FR-003, FR-004, FR-005, FR-006, FR-007, FR-008, FR-009, FR-010, FR-011, FR-012, FR-013, FR-014 |
| Section 13 (Deployment) | specification.md:12, specification.md:18 | - | FR-013, FR-014, NFR-201, NFR-203 |
| Section 14 (Security) | specification.md:4, specification.md:9, specification.md:12 | - | NFR-301, NFR-302, NFR-303, NFR-401, NFR-402, NFR-403, NFR-404, NFR-405 |
| Section 15 (Testing) | specification.md:17 | - | FR-001, FR-002, FR-003, FR-004, FR-005, FR-006, FR-007, FR-012, FR-013, FR-014 |
---
## 2. Architecture Overview
## Architecture Overview
### C4 Context Diagram
```mermaid
flowchart TD
subgraph "External Systems"
NATS_Server[NATS Server]
File_Server[HTTP File Server<br/>Plik/AWS S3/Custom]
end
subgraph "Client Applications"
Julia_App[Julia Application]
JS_App[JavaScript Application<br/>Node.js/Browser]
Python_App[Python Application<br/>Desktop]
Dart_App[Dart Application<br/>Desktop/Flutter/Web]
Rust_App[Rust Application<br/>Server/Desktop]
MicroPython_App[MicroPython Device]
end
Julia_App -->|NATS| NATS_Server
JS_App -->|NATS| NATS_Server
Python_App -->|NATS| NATS_Server
Dart_App -->|NATS| NATS_Server
Rust_App -->|NATS| NATS_Server
MicroPython_App -->|NATS| NATS_Server
Julia_App -->|HTTP| File_Server
JS_App -->|HTTP| File_Server
Python_App -->|HTTP| File_Server
Dart_App -->|HTTP| File_Server
Rust_App -->|HTTP| File_Server
MicroPython_App -->|HTTP| File_Server
style NATS_Server fill:#fff3e0,stroke:#f57c00
style File_Server fill:#f3e5f5,stroke:#9c27b4
style Julia_App fill:#e8f5e9,stroke:#4caf50
style JS_App fill:#e3f2fd,stroke:#2196f3
style Python_App fill:#e3f2fd,stroke:#2196f3
style Dart_App fill:#fff0f6,stroke:#e91e63
style Rust_App fill:#dea584,stroke:#e65100
style MicroPython_App fill:#fce4ec,stroke:#e91e63
```
### C4 Container Diagram
```mermaid
flowchart TD
subgraph "Client Container"
Julia_Module[Julia msghandler Module]
JS_Module[JavaScript msghandler Module]
Python_Module[Python msghandler Module]
Dart_Module[Dart msghandler Module]
Rust_Module[Rust msghandler Module]
MicroPython_Module[MicroPython msghandler Module]
end
Julia_Module --> NATS_Client
JS_Module --> NATS_Client
Python_Module --> NATS_Client
Dart_Module --> NATS_Client
Rust_Module --> NATS_Client
MicroPython_Module --> NATS_Client
NATS_Client --> NATS_Broker
Julia_Module --> File_Client
JS_Module --> File_Client
Python_Module --> File_Client
Dart_Module --> File_Client
Rust_Module --> File_Client
MicroPython_Module --> File_Client
File_Client --> File_Server
style Julia_Module fill:#e8f5e9,stroke:#4caf50
style JS_Module fill:#e3f2fd,stroke:#2196f3
style Python_Module fill:#e3f2fd,stroke:#2196f3
style Dart_Module fill:#fff0f6,stroke:#e91e63
style Rust_Module fill:#dea584,stroke:#e65100
style MicroPython_Module fill:#fce4ec,stroke:#e91e63
style NATS_Broker fill:#fff3e0,stroke:#f57c00
style File_Server fill:#f3e5f5,stroke:#9c27b4
```
### C4 Component Diagram (Julia Implementation)
```mermaid
flowchart TD
subgraph "msghandler Module"
SmartSend[smartsend Function]
SmartReceive[smartreceive Function]
Serialize[_serialize_data]
Deserialize[_deserialize_data]
EnvelopeToJson[envelope_to_json]
FileServerUpload[fileserver_upload_handler]
FileServerDownload[fileserver_download_handler]
LogTrace[log_trace]
end
subgraph "Data Models"
Payload[msg_payload_v1 Struct]
Envelope[msg_envelope_v1 Struct]
end
SmartSend --> Serialize
SmartSend --> EnvelopeToJson
SmartSend --> FileServerUpload
SmartReceive --> Deserialize
SmartReceive --> FileServerDownload
EnvelopeToJson --> Envelope
Serialize --> Payload
style SmartSend fill:#d1fae5,stroke:#10b981
style SmartReceive fill:#d1fae5,stroke:#10b981
style FileServerUpload fill:#fef3c7,stroke:#f59e0b
style FileServerDownload fill:#fef3c7,stroke:#f59e0b
```
---
## High-Level Architecture
### System Components
| Component | Purpose | Platform Support |
|-----------|---------|------------------|
| **smartsend** | Send data via NATS with automatic transport selection, returns (envelope, json_string) for caller to publish | All |
| **smartreceive** | Receive and process NATS messages from JSON string | All |
| **_serialize_data** | Serialize data according to payload type | All |
| **_deserialize_data** | Deserialize bytes to native data types | All |
| **envelope_to_json** | Convert msg_envelope_v1 struct to JSON string | All |
| **log_trace** | Log trace messages with correlation ID | All |
| **fileserver_upload_handler** | Upload large payloads to HTTP server | Desktop (Julia/JS/Python/Dart/Rust) |
| **fileserver_download_handler** | Download payloads from HTTP server with exponential backoff | Desktop (Julia/JS/Python/Dart/Rust) |
| **plik_upload_file** | Upload a local file to Plik server from disk | Rust |
### Data Flow
```mermaid
flowchart TD
A[User calls smartsend subject data] --> B[Process each payload]
B --> C{Calculate serialized size}
C -->|Size < Threshold| D[Direct Transport]
C -->|Size >= Threshold| E[Link Transport]
D --> F[Serialize data]
F --> G[Base64 encode]
G --> H[Build payload object]
E --> I[Serialize data]
I --> J[Upload to file server]
J --> K[Get download URL]
K --> H
H --> L[Build envelope]
L --> M[Convert to JSON]
M --> N[Return envelope + JSON to caller]
style A fill:#f9f9f9,stroke:#333
style N fill:#e0e7ff,stroke:#3b82f6
style D fill:#d1fae5,stroke:#10b981
style E fill:#fef3c7,stroke:#f59e0b
```
---
## Message Envelope Architecture
### msg_envelope_v1 Structure (Julia)
```julia
struct msg_envelope_v1
correlation_id::String # UUID v4 for distributed tracing
msg_id::String # UUID v4 for this message
timestamp::String # ISO 8601 UTC timestamp
send_to::String # NATS subject to publish to
msg_purpose::String # ACK, NACK, updateStatus, shutdown, chat
sender_name::String # Sender application name
sender_id::String # UUID v4 of sender
receiver_name::String # Receiver application name (empty = broadcast)
receiver_id::String # UUID v4 of receiver (empty = broadcast)
reply_to::String # Topic for reply messages
reply_to_msg_id::String # Message ID being replied to
broker_url::String # NATS broker URL
metadata::Dict{String, Any} # Message-level metadata
payloads::Vector{msg_payload_v1} # List of payloads
end
```
### msg_payload_v1 Structure (Julia)
```julia
struct msg_payload_v1
id::String # UUID v4 for this payload
dataname::String # Name of the payload
payload_type::String # text, dictionary, arrowtable, etc.
transport::String # direct or link
encoding::String # none, json, base64, arrow-ipc
size::Integer # Size in bytes
data::Any # Base64 string or URL
metadata::Dict{String, Any} # Payload-level metadata
end
```
### JSON Schema (Cross-Platform)
```json
{
"correlation_id": "string (UUID v4)",
"msg_id": "string (UUID v4)",
"timestamp": "string (ISO 8601 UTC)",
"send_to": "string",
"msg_purpose": "string",
"sender_name": "string",
"sender_id": "string (UUID v4)",
"receiver_name": "string",
"receiver_id": "string (UUID v4)",
"reply_to": "string",
"reply_to_msg_id": "string",
"broker_url": "string",
"metadata": "object",
"payloads": [
{
"id": "string (UUID v4)",
"dataname": "string",
"payload_type": "string",
"transport": "string",
"encoding": "string",
"size": "integer",
"data": "string or URL",
"metadata": "object"
}
]
}
```
---
## Payload Type Architecture
### Supported Payload Types
| Type | Description | Serialization | Encoding | Platforms |
|------|-------------|---------------|----------|-----------|
| `text` | Plain text string | UTF-8 bytes | Base64 | All |
| `dictionary` | JSON object | JSON string | Base64/JSON | All |
| `arrowtable` | Apache Arrow IPC | Arrow IPC stream | Base64/arrow-ipc | Desktop (Julia/Python/Node.js/Dart/Rust) |
| `jsontable` | JSON array of objects | JSON string | Base64/json | All (including Browser/Dart Web) |
| `image` | Binary image data | Raw bytes | Base64 | All |
| `audio` | Binary audio data | Raw bytes | Base64 | All |
| `video` | Binary video data | Raw bytes | Base64 | All |
| `binary` | Generic binary data | Raw bytes | Base64 | All |
### Serialization Logic
```mermaid
flowchart TD
A[Input data + payload_type] --> B{Payload Type}
B -->|"text"| C[UTF-8 encode]
B -->|"dictionary"| D[JSON serialize]
B -->|"arrowtable"| E[Arrow IPC serialize]
B -->|"jsontable"| F[JSON serialize]
B -->|"image"| G[Raw bytes]
B -->|"audio"| H[Raw bytes]
B -->|"video"| I[Raw bytes]
B -->|"binary"| J[Raw bytes]
C --> K[Return bytes]
D --> K
E --> K
F --> K
G --> K
H --> K
I --> K
J --> K
style A fill:#f9f9f9,stroke:#333
style K fill:#e0e7ff,stroke:#3b82f6
```
---
## Transport Strategy Architecture
### Size Threshold Decision Logic
| Platform | Size Threshold | Notes |
|----------|----------------|-------|
| Desktop (Julia/JS/Python/Dart) | 500,000 bytes (0.5MB) | Default threshold |
| Dart Desktop | 500,000 bytes (0.5MB) | Default threshold |
| Dart Flutter | 500,000 bytes (0.5MB) | Default threshold |
| Dart Web | 500,000 bytes (0.5MB) | Default threshold |
| MicroPython | 100,000 bytes (100KB) | Lower threshold for memory constraints |
### Transport Selection Flow
```mermaid
flowchart TD
A[smartsend called] --> B[Serialize payload]
B --> C[Calculate size]
C --> D{Size < Threshold?}
D -->|Yes| E[Direct Transport]
D -->|No| F[Link Transport]
E --> G[Base64 encode]
G --> H[Build payload with direct transport]
F --> I[Upload to file server]
I --> J[Get download URL]
J --> K[Build payload with link transport]
H --> L[Build envelope]
K --> L
style A fill:#f9f9f9,stroke:#333
style L fill:#e0e7ff,stroke:#3b82f6
style E fill:#d1fae5,stroke:#10b981
style F fill:#fef3c7,stroke:#f59e0b
```
### Direct Transport Protocol
When `transport = "direct"`, the `data` field contains a Base64-encoded string of the serialized payload.
**Encoding Rules**:
- `text`: UTF-8 → Base64
- `dictionary`: JSON → Base64 (or direct JSON)
- `arrowtable`: Arrow IPC → Base64 (or arrow-ipc)
- `jsontable`: JSON → Base64 (or direct JSON)
- `image`/`audio`/`video`/`binary`: Raw bytes → Base64
### Link Transport Protocol
When `transport = "link"`, the `data` field contains a URL pointing to the uploaded payload.
**Upload Flow**:
1. Serialize payload according to `payload_type`
2. Upload to HTTP file server (e.g., Plik)
3. Include returned URL in `data` field
**Download Flow**:
1. Extract URL from payload
2. Fetch with exponential backoff (max 5 retries)
3. Deserialize based on `payload_type`
---
## Platform-Specific Architecture
### Julia Architecture
Julia leverages multiple dispatch for type-specific implementations:
- **Multiple Dispatch**: Function overloading based on argument types
- **Struct-based Data Models**: Explicit type definitions with `struct`
- **Native Arrow IPC**: Support via `Arrow.jl`
- **Async/Await**: Tasks for non-blocking I/O
```julia
# Multiple dispatch for serialization
function _serialize_data(data::String, payload_type::String)
# Text serialization
end
function _serialize_data(data::Dict, payload_type::String)
# Dictionary serialization
end
function _serialize_data(data::DataFrame, payload_type::String)
# Arrow table serialization
end
```
### JavaScript Architecture
JavaScript uses async/await for non-blocking I/O:
- **Module-level Utilities**: Serialization functions
- **Native ArrayBuffer**: Binary data handling (Browser) / Buffer (Node.js)
- **Fetch API**: HTTP file server communication
#### Node.js Implementation (msghandler_ssr.js)
- **TCP NATS connections**: Uses `nats://` or `tls://` URLs
- **Apache Arrow IPC**: Full support via `apache-arrow`
- **Buffer for binary data**: Native Node.js Buffer handling
#### Browser Implementation (msghandler_csr.js)
- **WebSocket NATS connections**: Uses `ws://` or `wss://` URLs via `nats.ws`
- **No Apache Arrow**: Uses `jsontable` for tabular data only
- **Uint8Array for binary data**: Browser-compatible binary handling
- **Web Crypto API**: UUID generation via `crypto.getRandomValues()`
### Python Architecture
Python uses classes for stateful operations:
- **Class-based msghandler**: Encapsulated API
- **Dataclasses**: Structured data (MsgPayloadV1, MsgEnvelopeV1)
- **Async/await**: I/O operations
- **pyarrow**: Arrow IPC support
```python
class msghandler:
DEFAULT_SIZE_THRESHOLD = 500_000
def __init__(self, broker_url=None, fileserver_url=None):
self.broker_url = broker_url or self.DEFAULT_BROKER_URL
self.fileserver_url = fileserver_url or self.DEFAULT_FILESERVER_URL
```
### Dart Architecture
Dart uses classes for stateful operations with async/await:
- **Class-based msghandler**: Encapsulated API
- **Data classes**: Structured data (MsgPayloadV1, MsgEnvelopeV1)
- **Async/await**: I/O operations
- **dart-arrow**: Arrow IPC support (Desktop/Flutter only)
- **HTTP package**: HTTP file server communication
- **nats package**: NATS client with WebSocket support (Dart Web)
```dart
class msghandler {
static const DEFAULT_SIZE_THRESHOLD = 500000;
final String brokerUrl;
final String fileserverUrl;
msghandler({
this.brokerUrl = 'nats://localhost:4222',
this.fileserverUrl = 'http://localhost:8080',
});
}
```
#### Dart Desktop (Dart SDK)
- **TCP NATS connections**: Uses `nats://` or `tls://` URLs
- **Apache Arrow IPC**: Full support via `dart-arrow`
- **Uint8List for binary data**: Native Dart binary handling
#### Dart Flutter (Dart SDK)
- **TCP NATS connections**: Uses `nats://` or `tls://` URLs
- **Apache Arrow IPC**: Full support via `dart-arrow`
- **Uint8List for binary data**: Native Dart binary handling
#### Dart Web (Dart SDK)
- **WebSocket NATS connections**: Uses `ws://` or `wss://` URLs via `nats` package
- **No Apache Arrow**: Uses `jsontable` for tabular data only
- **Uint8List for binary data**: Browser-compatible binary handling
- **Fetch API**: HTTP file server communication via `http` package
### Browser Architecture
Browser JavaScript has specific constraints due to security and compatibility:
- **Async/await**: Native async/await support
- **No Apache Arrow**: Arrow IPC not available in browsers
- **JSON table only**: Use "jsontable" for tabular data
- **WebSocket NATS**: Uses nats.ws for browser-compatible NATS connections
- **Fetch API**: HTTP file server communication via fetch
### MicroPython Architecture
MicroPython has significant constraints:
- **Synchronous API**: No async/await
- **Memory-constrained**: 256KB - 1MB
- **Limited payload support**: No tables, max 50KB
- **Simplified UUID generation**: Custom implementation
```python
# MicroPython constraints
DEFAULT_SIZE_THRESHOLD = 100_000 # 100KB
MAX_PAYLOAD_SIZE = 50_000 # 50KB hard limit
```
### Rust Architecture
Rust leverages compile-time type safety and async runtimes:
- **Type-safe payloads**: Rust enum discriminates between `Text`, `Dictionary`, `ArrowTable`, `Binary`, etc.
- **serde serialization**: Automatic JSON deserialization via `#[derive(Serialize, Deserialize)]`
- **tokio runtime**: Efficient async I/O for NATS connections and HTTP file server operations
- **arrow2 integration**: Native Arrow IPC deserialization without intermediate format conversion
- **reqwest**: High-performance HTTP client with built-in TLS and connection pooling
- **Zero-copy patterns**: `Vec<u8>` passed directly to avoid unnecessary memory copies
- **Result<T, E>**: Idiomatic error handling with typed error types
```rust
// Type-safe payload enum (compile-time discrimination)
#[derive(Serialize, Deserialize, Clone)]
pub enum Payload {
Text(String),
Dictionary(serde_json::Value),
ArrowTable(Vec<u8>),
JsonTable(serde_json::Value),
Image(Vec<u8>),
Audio(Vec<u8>),
Video(Vec<u8>),
Binary(Vec<u8>),
}
// Configuration via builder pattern
pub struct SmartsendOptions {
pub broker_url: String,
pub fileserver_url: String,
pub fileserver_upload_handler: Option<Arc<dyn FileUploadHandler>>,
pub size_threshold: usize,
pub correlation_id: String,
pub msg_purpose: String,
pub sender_name: String,
// ... other fields
}
// NATS client with tokio integration
let conn = nats::connect("nats://localhost:4222").await?;
// Subscribe and process messages
let mut sub = conn.subscribe("/agent/wine/api/v1/analyze")?;
for msg in sub.messages() {
let envelope = smartreceive(&String::from_utf8_lossy(&msg.payload), &Default::default()).await?;
// Access deserialized payloads by type
for payload in &envelope.payloads {
match payload.payload_type.as_str() {
"arrowtable" => { /* payload.data is base64-encoded Arrow IPC */ },
"text" => { /* payload.data is decoded text string */ },
"binary" | "image" | "audio" | "video" => { /* payload.data is base64-encoded binary */ },
_ => { /* other types */ }
}
}
}
```
---
## Scaling Architecture
### Horizontal Scaling
| Component | Scaling Strategy |
|-----------|------------------|
| **NATS Server** | Cluster deployment with multiple nodes |
| **File Server** | Load balancer + multiple instances |
| **Client Applications** | Deploy multiple instances behind load balancer |
### Vertical Scaling
| Component | Scaling Strategy |
|-----------|------------------|
| **NATS Server** | Increase memory, CPU, disk I/O |
| **File Server** | Increase memory, CPU, disk capacity |
| **Client Applications** | Increase heap size (Python/JS) |
### Performance Considerations
| Metric | Target | Notes |
|--------|--------|-------|
| Message serialization overhead | <50ms | For 10KB payload |
| Message deserialization overhead | <50ms | For 10KB payload |
| NATS connection establishment | <100ms | Connection pool recommended |
| File upload latency | <1s | For 0.5MB file |
| File download latency | <1s | For 0.5MB file |
---
## Failure Modes and Recovery
### NATS Connection Failure
**Scenario**: NATS server unavailable
**Handler**:
- Connection auto-reconnect via TCP-level reconnection
- Retry with exponential backoff for publish operations
**Recovery**:
- NATS client automatically attempts reconnection
- Application can check connection status before publishing
### File Server Unavailable
**Scenario**: HTTP file server unavailable during upload/download
**Handler**:
- Retry up to 5 times with exponential backoff (100ms → 5000ms)
- Fallback to direct transport for upload (MicroPython)
**Recovery**:
- Exponential backoff: `delay = min(delay * 2, max_delay)`
- After max retries, throw error with correlation ID
### Deserialization Error
**Scenario**: Payload type mismatch or corrupted data
**Handler**:
- Log correlation ID and throw error
- No retry (data corruption)
**Recovery**:
- Application must validate payload_type matches data type
- Use proper serialization before sending
### Memory Overflow (MicroPython)
**Scenario**: Payload exceeds maximum size (50KB)
**Handler**:
- Reject payloads >50KB with MemoryError
- No retry (client-side check)
**Recovery**:
- Application must split large payloads
- Use direct transport only for small payloads
---
## Trade-off Decisions
### Decision 1: Direct vs Link Transport Threshold
**Trade-off**: Memory vs Network I/O
**Decision**: Use 0.5MB threshold for desktop, 100KB for MicroPython
**Rationale**:
- Direct transport uses more memory (Base64 encoding adds ~33% overhead)
- Link transport requires network I/O for upload/download
- 0.5MB is reasonable for desktop memory constraints
- 100KB is necessary for MicroPython memory constraints
### Decision 2: Base64 Encoding for Direct Transport
**Trade-off**: Bandwidth vs Simplicity
**Decision**: Use Base64 encoding for all direct transport payloads
**Rationale**:
- Simplifies JSON serialization (all data is string-compatible)
- Increases payload size by ~33%, but NATS can handle this
- Alternative would be binary payload support (more complex)
### Decision 3: Multiple Platform Implementations
**Trade-off**: Development effort vs Cross-platform support
**Decision**: Maintain separate implementations for each platform
**Rationale**:
- Each platform has idiomatic patterns (multiple dispatch, async/await, etc.)
- Maintains developer productivity and code quality
- API parity ensures cross-platform compatibility
### Decision 4: Handler Function Abstraction
**Trade-off**: Flexibility vs Simplicity
**Decision**: Abstract file server operations through handler functions
**Rationale**:
- Allows support for different file server implementations (Plik, AWS S3, custom)
- Maintains simplicity for common use cases
- Enables plug-in architecture for custom backends
---
## Deployment Architecture
### Minimum Infrastructure
| Component | Minimum | Notes |
|-----------|---------|-------|
| NATS Server | 1 instance | Single node for development |
| File Server | 1 instance | HTTP server for large payloads |
| Client Memory | 50MB | Desktop platforms (Julia/JS/Python/Dart) |
| Client Memory | 256KB | MicroPython devices |
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `NATS_URL` | `nats://localhost:4222` | NATS server URL |
| `FILESERVER_URL` | `http://localhost:8080` | HTTP file server URL |
| `SIZE_THRESHOLD` | `500000` | Size threshold in bytes (0.5MB) |
### Container Deployment
```mermaid
flowchart TD
subgraph "Docker Network"
NATS_Container[NATS Server]
FileServer_Container[Plik File Server]
App_Container[Application Container]
end
App_Container -->|NATS| NATS_Container
App_Container -->|HTTP| FileServer_Container
style NATS_Container fill:#fff3e0,stroke:#f57c00
style FileServer_Container fill:#f3e5f5,stroke:#9c27b4
style App_Container fill:#e3f2fd,stroke:#2196f3
```
---
## Security Considerations
### Payload Integrity
**Mechanism**: SHA-256 checksum via metadata
**Implementation**:
- Sender calculates checksum and stores in payload metadata
- Receiver validates checksum on receipt
### Transport Security
**Mechanism**: TLS support for NATS connections
**Implementation**:
- Use `nats://` URL for plain text
- Use `tls://` URL for TLS-encrypted connections
### File Server Security
**Mechanism**: Authentication token for file uploads
**Implementation**:
- Plik uses upload token in `X-UploadToken` header
- Application can implement custom authentication
---
## Testing Architecture
### Unit Test Coverage
| Test Category | Coverage | Files |
|---------------|----------|-------|
| Serialization | All payload types | `test/test_*_sender.*` |
| Deserialization | All payload types | `test/test_*_receiver.*` |
| Transport selection | Direct vs link | `test/test_*_mix_payloads.*` |
| File server upload | Plik integration | Platform-specific |
| File server download | Exponential backoff | Platform-specific |
### Integration Test Scenarios
| Scenario | Platforms | Payloads | Transport | Expected Result |
|----------|-----------|----------|-----------|-----------------|
| Cross-platform text | Julia ↔ JS ↔ Python | text | direct | Round-trip successful |
| Arrow IPC round-trip | Julia ↔ JS ↔ Python | arrowtable | direct | Arrow IPC preserved |
| Large file transfer | All | image/audio/video | link | File server upload/download |
| Multi-payload mixed | All | text + image + file | direct/link | All payloads preserved |
---
## Versioning
### Architecture Versioning
| Component | Version | Notes |
|-----------|---------|-------|
| Architecture | 1.0.0 | Initial release |
| Protocol | v1 | Message envelope protocol version |
### Backward Compatibility
| Version | Supported Platforms |
|---------|---------------------|
| v1.0.x | Julia 1.7+, Node.js 16+, Python 3.8+, Dart 2.17+, Rust 1.70+, MicroPython 1.19+ |
---
## Change Log
| Date | Version | Changes |
|------|---------|---------|
| 2026-05-14 | 1.4.0 | Updated Rust API to reflect `smartreceive` deserialization changes | All sections |
| - | - | `smartreceive` now stores deserialized data in `MsgPayloadV1.data` | specification.md:8 |
| - | - | Added `plik_upload_file` convenience function to component table | specification.md:13 |
| - | - | Fixed Rust payload access pattern (data is String, not Payload enum) | All sections |
| - | - | Fixed `SmartsendOptions.fileserver_upload_handler` type to `Arc<dyn FileUploadHandler>` | specification.md:13 |
| - | - | Removed `metadata` from link transport examples (now `None`/omitted) | specification.md:3 |
| - | - | Removed duplicate footer text | All sections |
| 2026-05-13 | 1.3.0 | Added Rust support with tokio, serde, and arrow2 | All sections |
| - | - | Added Rust to C4 diagrams (context, container) | All sections |
| - | - | Added Rust platform-specific architecture section | specification.md:13 |
| - | - | Updated component table with Rust support | All sections |
| 2026-05-13 | 1.2.0 | Aligned with ground truth implementation (src/msghandler.jl) |
| - | - | Removed publish_message component (commented out in source) |
| - | - | Removed NATSClient and NATSConnectionPool classes (not in ground truth) |
| - | - | Updated component diagram to match actual module structure |
| - | - | Updated data flow to show smartsend returns JSON for caller to publish |
| - | - | Fixed SIZE_THRESHOLD default to 500,000 bytes |
| 2026-03-15 | 1.1.0 | JavaScript connection management |
| - | - | Added NATSClient with keepAlive support |
| - | - | Added NATSConnectionPool for connection reuse |
| - | - | Added publishMessage function with closeConnection option |
| 2026-03-13 | 1.0.0 | Initial architecture documentation |
---
## 16. References
### 16.1 Documentation Artifacts
| Document | Purpose | Specification Traceability | UI Specification Traceability | Requirement ID(s) |
|----------|---------|---------------------------|------------------------------|-------------------|
| [`docs/requirements.md`](./requirements.md) | Business requirements and user stories | FR-001 through FR-014, NFR-101 through NFR-405 | - | FR-001 through FR-014, NFR-101 through NFR-405 |
| [`docs/specification.md`](./specification.md) | Technical contract for msghandler | specification.md:2-19 (all sections) | - | FR-001 through FR-014, NFR-101 through NFR-405 |
| [`docs/ui-specification.md`](./ui-specification.md) | UI specification for client applications | - | All UI components and interactions | FR-001 through FR-014, NFR-101 through NFR-405 |
| [`docs/walkthrough.md`](./walkthrough.md) | End-to-end system flow | specification.md:2-19 (all sections) | - | FR-001 through FR-014, NFR-101 through NFR-405 |
| [`docs/architecture.md`](./architecture.md) | System architecture diagrams | specification.md:2-19 (all sections) | - | FR-001 through FR-014, NFR-101 through NFR-405 |
| [`docs/validation.md`](./validation.md) | CI/CD validation rules | specification.md:2-19 (all sections) | - | FR-001 through FR-014, NFR-101 through NFR-405 |
| [`docs/runbook.md`](./runbook.md) | Operational runbook | specification.md:2-19 (all sections) | - | FR-001 through FR-014, NFR-101 through NFR-405 |
### 16.2 Implementation Files
| File | Platform | Features | Specification Traceability | Requirement ID(s) |
|------|----------|----------|---------------------------|-------------------|
| [`src/msghandler.jl`](../src/msghandler.jl) | Julia | Full feature set, Arrow IPC, multiple dispatch | specification.md:2-19 (all sections) | FR-001 through FR-014, NFR-101 through NFR-405 |
| [`src/msghandler_ssr.js`](../src/msghandler_ssr.js) | Node.js | Arrow IPC, async/await | specification.md:2-19 (all sections) | FR-001 through FR-014, NFR-101 through NFR-405 |
| [`src/msghandler_csr.js`](../src/msghandler_csr.js) | Browser | JSON table only, WebSocket NATS | specification.md:2-19 (all sections) | FR-001 through FR-014, NFR-101 through NFR-405 |
| [`src/msghandler.py`](../src/msghandler.py) | Python | Arrow IPC, async/await | specification.md:2-19 (all sections) | FR-001 through FR-014, NFR-101 through NFR-405 |
| [`src/msghandler.dart`](../src/msghandler.dart) | Dart | Full feature set, Arrow IPC, async/await | specification.md:2-19 (all sections) | FR-001 through FR-014, NFR-101 through NFR-405 |
| [`src/msghandler.rs`](../src/msghandler.rs) | Rust | Full feature set, Arrow IPC, async/await, type-safe, file upload helpers | specification.md:2-19 (all sections) | FR-001 through FR-014, NFR-101 through NFR-405 |
| [`src/msghandler_mpy.py`](../src/msghandler_mpy.py) | MicroPython | Limited to direct transport | specification.md:2-19 (all sections) | FR-005, FR-006, FR-012 |
### 16.3 External Dependencies
| Platform | Package | Version | Purpose | Specification Traceability | Requirement ID(s) |
|----------|---------|---------|---------|---------------------------|-------------------|
| Julia | NATS.jl | Latest | NATS client | specification.md:11 | FR-013, FR-014, NFR-201 |
| Julia | JSON.jl | Latest | JSON serialization | specification.md:11 | FR-012, NFR-101, NFR-102 |
| Julia | Arrow.jl | Latest | Arrow IPC support | specification.md:11 | FR-002, FR-012 |
| Julia | HTTP.jl | Latest | HTTP file server | specification.md:11 | FR-008, FR-009 |
| Julia | UUIDs.jl | Latest | UUID generation | specification.md:11 | FR-011, NFR-401 |
| Node.js | nats | Latest | NATS client (TCP) | specification.md:11 | FR-013, FR-014 |
| Node.js | node-fetch | Latest | HTTP file server | specification.md:11 | FR-008, FR-009 |
| Browser | nats.ws | Latest | NATS client (WebSocket) | specification.md:11 | FR-013, FR-014 |
| Browser | nats | Latest | NATS client (for bundling) | specification.md:11 | FR-013, FR-014 |
| Python | nats-py | Latest | NATS client | specification.md:11 | FR-013, FR-014 |
| Python | aiohttp | Latest | HTTP file server | specification.md:11 | FR-008, FR-009 |
| Python | pyarrow | Latest | Arrow IPC support | specification.md:11 | FR-002, FR-012 |
| Dart | nats | Latest | NATS client | specification.md:11 | FR-013, FR-014 |
| Dart | http | Latest | HTTP file server | specification.md:11 | FR-008, FR-009 |
| Dart | uuid | Latest | UUID generation | specification.md:11 | FR-011, NFR-401 |
| Dart | dart-arrow | Latest | Arrow IPC support | specification.md:11 | FR-002, FR-012 |
| Rust | nats | Latest | NATS client | specification.md:11 | FR-013, FR-014 |
| Rust | serde | Latest | JSON serialization | specification.md:11 | FR-012, NFR-101, NFR-102 |
| Rust | serde_json | Latest | JSON handling | specification.md:11 | FR-012, NFR-101, NFR-102 |
| Rust | tokio | Latest | Async runtime | specification.md:11 | FR-013, FR-014 |
| Rust | reqwest | Latest | HTTP file server | specification.md:11 | FR-008, FR-009 |
| Rust | uuid | Latest | UUID generation | specification.md:11 | FR-011, NFR-401 |
| Rust | arrow2 | Latest | Arrow IPC support | specification.md:11 | FR-002, FR-012 |
| MicroPython | builtin | N/A | Limited implementation | specification.md:11 | FR-005, FR-006, FR-012 |
---
## 17. Change Log
| Date | Version | Changes | Specification Reference |
|------|---------|---------|------------------------|
| 2026-03-23 | 1.1.0 | Updated to ASG Framework architecture guidelines | specification.md:2-19 (all sections) |
| 2026-03-15 | 1.1.0 | JavaScript connection management | specification.md:2-19 (all sections) |
| 2026-03-13 | 1.0.0 | Initial architecture documentation | specification.md:2-19 (all sections) |
---
## 18. Gap-Check Validation
| Stage Transition | Gap-Check Question | Status |
|------------------|-------------------|--------|
| Requirements → Specification | Does the Specification define all edge cases and conflict scenarios from the Requirements? | ✅ Verified - All FR-XXX requirements have corresponding spec rules |
| Specification → UI Specification | Does the UI Specification expose all the data and states defined in the Specification? | ⏳ Pending - UI spec not yet created |
| UI Specification → Walkthrough | Does the Walkthrough reflect the complete flow including error states and timing? | ⏳ Pending - UI spec not yet created |
| Walkthrough → Architecture | Does the Architecture support the performance and integration requirements defined in the Walkthrough? | ✅ Verified - Architecture supports all walkthrough flows |
---
*This architecture document is versioned and maintained in git alongside the codebase. All implementations must adhere to this architecture.*

416
docs/implementation-plan.md Normal file
View File

@@ -0,0 +1,416 @@
# Implementation Plan: msghandler
**Version**: 1.3.0
**Date**: 2026-05-19
**Status**: Active
**Ground Truth**: [`src/msghandler.jl`](../src/msghandler.jl)
---
## 1. Implementation Phases and Timeline
### Phase 1: Core API Implementation (Week 1-2)
| Task | Priority | Estimated Effort | Status |
|------|----------|-----------------|--------|
| Core `smartpack()` implementation | P0 | 3 days | ✅ Complete |
| Core `smartunpack()` implementation | P0 | 3 days | ✅ Complete |
| Message envelope structure | P0 | 2 days | ✅ Complete |
| Payload type handling | P0 | 2 days | ✅ Complete |
| Transport adapter layer | P0 | 3 days | ✅ Complete |
**Deliverables**:
- Julia module: `src/msghandler.jl`
- Node.js module: `src/msghandler_ssr.js`
- Browser module: `src/msghandler_csr.js`
- Python module: `src/msghandler.py`
- MicroPython module: `src/msghandler_mpy.py`
### Phase 2: File Server Integration (Week 3)
| Task | Priority | Estimated Effort | Status |
|------|----------|-----------------|--------|
| File server upload handler | P1 | 2 days | ✅ Complete |
| File server download handler | P1 | 2 days | ✅ Complete |
| Exponential backoff logic | P1 | 1 day | ✅ Complete |
| Plik integration | P1 | 2 days | ✅ Complete |
**Deliverables**:
- Upload handler with plik_oneshot_upload
- Download handler with retry logic
- Configurable file server URL
### Phase 3: Platform-Specific Features (Week 4)
| Task | Priority | Estimated Effort | Status |
|------|----------|-----------------|--------|
| Arrow IPC support (Desktop) | P1 | 3 days | ✅ Complete |
| JSON table support (Browser) | P1 | 2 days | ✅ Complete |
| Browser WebSocket transport | P1 | 2 days | ✅ Complete |
| MicroPython optimizations | P2 | 2 days | ✅ Complete |
**Deliverables**:
- Arrow IPC serialization for tabular data
- JSON table format for browser compatibility
- Browser-specific transport layer
- Memory-optimized MicroPython implementation
### Phase 4: Cross-Platform Testing (Week 5)
| Task | Priority | Estimated Effort | Status |
|------|----------|-----------------|--------|
| Text message tests | P1 | 1 day | ✅ Complete |
| Dictionary tests | P1 | 1 day | ✅ Complete |
| Tabular data tests | P1 | 2 days | ✅ Complete |
| Mixed payload tests | P1 | 2 days | ✅ Complete |
| Large file tests | P1 | 2 days | ✅ Complete |
**Deliverables**:
- Platform-specific test suites
- Integration test scenarios
- Performance benchmarks
### Phase 5: Documentation & Examples (Week 6)
| Task | Priority | Estimated Effort | Status |
|------|----------|-----------------|--------|
| API documentation | P2 | 2 days | ✅ Complete |
| Walkthrough examples | P2 | 2 days | ✅ Complete |
| Architecture diagrams | P2 | 1 day | ✅ Complete |
| Deployment guides | P2 | 1 day | ✅ Complete |
**Deliverables**:
- Comprehensive documentation
- Code examples for all platforms
- Deployment runbooks
---
## 2. Module/Component Breakdown
### Core Modules
#### msghandler.jl (Julia)
```
src/
└── msghandler.jl
├── Constants (DEFAULT_SIZE_THRESHOLD, etc.)
├── msg_payload_v1 struct
├── msg_envelope_v1 struct
├── Serialization functions
│ ├── serialize_text()
│ ├── serialize_dictionary()
│ ├── serialize_arrowtable()
│ ├── serialize_jsontable()
│ └── serialize_binary()
├── Deserialization functions
│ ├── deserialize_text()
│ ├── deserialize_dictionary()
│ ├── deserialize_arrowtable()
│ ├── deserialize_jsontable()
│ └── deserialize_binary()
├── File server handlers
│ ├── plik_oneshot_upload()
│ └── _fetch_with_backoff()
├── smartpack() - Main sender function
└── smartunpack() - Main receiver function
```
**Dependencies**:
- JSON.jl (JSON serialization)
- Arrow.jl (Arrow IPC)
- HTTP.jl (File server)
- UUIDs.jl (IDs)
- DataFrames.jl (DataFrame support)
#### msghandler_ssr.js (Node.js)
```
src/
├── msghandler_ssr.js
│ ├── Constants
│ ├── msg_payload_v1 class
│ ├── msg_envelope_v1 class
│ ├── Serialization methods
│ ├── Deserialization methods
│ ├── File server handlers
│ ├── smartpack() function
│ └── smartunpack() function
└── nats/
├── NATSClient.js
└── NATSConnectionPool.js
```
**Dependencies**:
- nats (NATS client)
- node-fetch (HTTP file server)
#### msghandler_csr.js (Browser)
```
src/
└── msghandler_csr.js
├── Constants
├── msg_payload_v1 class
├── msg_envelope_v1 class
├── Serialization methods (JSON table only)
├── Deserialization methods
├── File server handlers (browser-compatible)
├── smartpack() function
└── smartunpack() function
```
**Dependencies**:
- nats.ws (Browser NATS client)
#### msghandler.py (Python)
```
src/
└── msghandler.py
├── Constants
├── msg_payload_v1 class
├── msg_envelope_v1 class
├── Serialization methods
├── Deserialization methods
├── File server handlers
├── smartpack() async function
└── smartunpack() async function
```
**Dependencies**:
- aiohttp (HTTP file server)
- pyarrow (Arrow IPC)
- uuid (IDs)
#### msghandler.rs (Rust)
```
src/
├── msghandler.rs
│ ├── Constants
│ ├── msg_payload_v1 struct
│ ├── msg_envelope_v1 struct
│ ├── Serialization traits
│ ├── Deserialization traits
│ ├── File server handlers
│ ├── smartpack() async function
│ └── smartunpack() async function
├── Payload enum
├── smartpackOptions struct
└── smartunpackOptions struct
```
**Dependencies**:
- tokio (Async runtime)
- serde (JSON serialization)
- reqwest (HTTP file server)
- arrow2 (Arrow IPC)
#### msghandler_mpy.py (MicroPython)
```
src/
└── msghandler_mpy.py
├── Constants (lower thresholds)
├── msg_payload_v1 class
├── msg_envelope_v1 class
├── serialize_text()
├── deserialize_text()
├── serialize_dictionary()
├── deserialize_dictionary()
└── smartpack()/smartunpack() functions
```
**Constraints**:
- Limited to text and dictionary types
- Direct transport only (no file server)
- 100KB threshold for memory constraints
---
## 3. Task List
### Core API Tasks
| Task ID | Description | Assignee | Priority | Status |
|---------|-------------|----------|----------|--------|
| T-001 | Implement `smartpack()` with tuple format | Developer A | P0 | ✅ Complete |
| T-002 | Implement `smartunpack()` with type handling | Developer A | P0 | ✅ Complete |
| T-003 | Create message envelope structure | Developer A | P0 | ✅ Complete |
| T-004 | Implement transport adapter | Developer B | P0 | ✅ Complete |
| T-005 | Add correlation ID support | Developer A | P0 | ✅ Complete |
### File Server Tasks
| Task ID | Description | Assignee | Priority | Status |
|---------|-------------|----------|----------|--------|
| T-006 | Implement Plik upload handler | Developer B | P1 | ✅ Complete |
| T-007 | Implement file download with retry | Developer B | P1 | ✅ Complete |
| T-008 | Add exponential backoff logic | Developer B | P1 | ✅ Complete |
### Platform Tasks
| Task ID | Description | Assignee | Priority | Status |
|---------|-------------|----------|----------|--------|
| T-009 | Implement Arrow IPC (Julia/Python/Node.js) | Developer A | P1 | ✅ Complete |
| T-010 | Implement JSON table (Browser) | Developer B | P1 | ✅ Complete |
| T-011 | Implement MicroPython optimizations | Developer C | P2 | ✅ Complete |
| T-012 | Browser WebSocket transport | Developer B | P1 | ✅ Complete |
### Testing Tasks
| Task ID | Description | Assignee | Priority | Status |
|---------|-------------|----------|------------------|
| T-013 | Text message tests | QA Team | P1 | ✅ Complete |
| T-014 | Dictionary tests | QA Team | P1 | ✅ Complete |
| T-015 | Tabular data tests | QA Team | P1 | ✅ Complete |
| T-016 | Mixed payload tests | QA Team | P1 | ✅ Complete |
| T-017 | Large file tests | QA Team | P1 | ✅ Complete |
---
## 4. Test Strategy
### Unit Tests
| Test Category | Coverage | Files | Requirements |
|---------------|----------|-------|--------------|
| Serialization | All payload types | `test/test_*_sender.*` | FR-001 through FR-012 |
| Deserialization | All payload types | `test/test_*_receiver.*` | FR-001 through FR-012 |
| Transport selection | Direct vs link | `test/test_*_mix_payloads.*` | FR-003, FR-004, FR-006 |
| File server upload | Plik integration | Platform-specific | FR-008, FR-009 |
| File server download | Exponential backoff | Platform-specific | FR-010, FR-011 |
### Integration Tests
| Scenario | Platforms | Payloads | Transport | Requirements |
|----------|-----------|----------|-----------|--------------|
| Single text (small) | All | text | direct | FR-001, FR-012 |
| Single dictionary (small) | All | dictionary | direct | FR-002, FR-012 |
| Single arrow table (small) | Desktop | arrowtable | direct | FR-002, FR-012 |
| Single JSON table (small) | All | jsontable | direct | FR-001, FR-002, FR-006 |
| Single image (small) | All | image | direct | FR-001, FR-006 |
| Single text (large) | All | text | link | FR-003, FR-008, FR-009 |
| Mixed payloads | All | text + dictionary + image | mixed | FR-006, FR-007 |
### Test Coverage Targets
| Phase | Coverage Target | Method |
|-------|----------------|--------|
| Phase 1 | 70% | Unit tests per platform |
| Phase 2 | 80% | Add integration tests |
| Phase 3 | 85% | Add edge case tests |
| Phase 4 | 90% | Add performance tests |
---
## 5. Build and Deployment Preparation
### Continuous Integration
| Check | Command | Purpose |
|-------|---------|---------|
| Linting | `npm run lint` | Code style enforcement |
| Type checking | `npx tsc --noEmit` | Type safety (JavaScript/TypeScript) |
| Unit tests | `npm test` | Functionality validation |
| Integration tests | `npm run test:integration` | Cross-platform validation |
| Coverage | `npm run coverage` | Test coverage tracking |
### Deployment Pipeline
```
GitHub Push
CI/CD Pipeline
├──→ Linting (all platforms)
├──→ Unit tests (all platforms)
├──→ Integration tests (cross-platform)
├──→ Coverage report
└──→ Build documentation
Release (if all checks pass)
├──→ GitHub Releases
├──→ Package registry (npm, PyPI)
└──→ Documentation site
```
---
## 6. Risk Mitigation
### Known Blockers
| Risk | Mitigation Step | Owner |
|------|----------------|-------|
| **Browser Arrow IPC** | Use JSON table as fallback | Developer B |
| **MicroPython memory** | 100KB threshold, direct transport only | Developer C |
| **File server availability** | Exponential backoff with graceful degradation | Developer B |
### Known Unknowns
| Unknown | Monitoring Strategy | Response Plan |
|---------|-------------------|---------------|
| Platform-specific bugs | Comprehensive test coverage | Hotfix with platform-specific handling |
| Performance bottlenecks | Load testing and profiling | Optimized serialization/deserialization |
---
## 7. Requirements Traceability
### Functional Requirements
| Requirement ID | Implementation Location | Status |
|---------------|------------------------|--------|
| FR-001 | All platform modules | ✅ Complete |
| FR-002 | All platform modules | ✅ Complete |
| FR-003 | All platform modules (size_threshold logic) | ✅ Complete |
| FR-004 | All platform modules | ✅ Complete |
| FR-005 | MicroPython module | ✅ Complete |
| FR-006 | All platform modules | ✅ Complete |
| FR-007 | All platform modules | ✅ Complete |
| FR-008 | All platform modules | ✅ Complete |
| FR-009 | All platform modules | ✅ Complete |
| FR-010 | All platform modules | ✅ Complete |
| FR-011 | All platform modules | ✅ Complete |
| FR-012 | All platform modules | ✅ Complete |
| FR-013 | All platform modules | ✅ Complete |
| FR-014 | All platform modules | ✅ Complete |
### Non-Functional Requirements
| NFR ID | Implementation Location | Status |
|--------|------------------------|--------|
| NFR-101 | Serialization functions | ✅ Complete |
| NFR-102 | Deserialization functions | ✅ Complete |
| NFR-103 | Transport adapter | ✅ Complete |
| NFR-104 | File upload handler | ✅ Complete |
| NFR-105 | File download handler | ✅ Complete |
| NFR-106 | MicroPython module | ✅ Complete |
| NFR-107 | Performance benchmarks | ✅ Complete |
| NFR-201 | Transport adapter | ✅ Complete |
| NFR-202 | File download retry logic | ✅ Complete |
| NFR-203 | Transport adapter | ✅ Complete |
| NFR-401 | Message envelope | ✅ Complete |
| NFR-402 | Metrics instrumentation | ✅ Complete |
| NFR-403 | Correlation ID propagation | ✅ Complete |
---
## 8. Validation Gates
### Pre-Release Checklist
| Gate | Check | Pass Criteria |
|------|-------|--------------|
| **G-001** | All unit tests pass | 100% pass rate per platform |
| **G-002** | Integration tests pass | Cross-platform round-trip successful |
| **G-003** | Coverage threshold | ≥80% line coverage |
| **G-004** | Linting clean | No warnings or errors |
| **G-005** | Specification compliance | All spec rules validated |
| **G-006** | Documentation complete | All required docs present |
### CI/CD Validation
| Check | Command | Failure Action |
|-------|---------|---------------|
| Syntax | `julia --check-base` | Block PR |
| Unit tests | `julia test/runtests.jl` | Block PR |
| Integration | `npm run test:integration` | Block PR |
| Coverage | `codecov` | Report only |
---
*This implementation plan is versioned and maintained in git alongside the codebase. All implementations must adhere to this plan.*

View File

@@ -1,7 +1,7 @@
# Requirements Document: msghandler # Requirements Document: msghandler
**Version**: 1.2.0 **Version**: 1.3.0
**Date**: 2026-05-13 **Date**: 2026-05-22
**Status**: Active **Status**: Active
**Ground Truth**: [`src/msghandler.jl`](../src/msghandler.jl) **Ground Truth**: [`src/msghandler.jl`](../src/msghandler.jl)
@@ -11,7 +11,7 @@
### 1.1 Business Goal ### 1.1 Business Goal
msghandler is a cross-platform, bi-directional data bridge that enables seamless communication between **Julia**, **JavaScript**, **Python**, **Dart**, **Rust**, and **MicroPython** applications using NATS as the message bus. The system implements the **Claim-Check pattern** for efficient handling of large payloads (>0.5MB) by uploading them to an HTTP file server instead of sending raw binary data over NATS. msghandler is a cross-platform, bi-directional data bridge that enables seamless communication between **Julia**, **JavaScript**, **Python**, **Dart**, **Rust**, and **MicroPython** applications using a message broker as the transport layer. The system implements the **Claim-Check pattern** for efficient handling of large payloads (>0.5MB) by uploading them to an HTTP file server instead of sending raw binary data over the transport layer.
### 1.2 User Stories (with acceptance criteria) ### 1.2 User Stories (with acceptance criteria)
@@ -19,13 +19,13 @@ msghandler is a cross-platform, bi-directional data bridge that enables seamless
|-------|----------|---------------------| |-------|----------|---------------------|
| **As a Julia developer**, I want to send text messages to JavaScript/Dart applications that lives on a server and also on a browser | P1 | Text messages are serialized, encoded, and received correctly across platforms | | **As a Julia developer**, I want to send text messages to JavaScript/Dart applications that lives on a server and also on a browser | P1 | Text messages are serialized, encoded, and received correctly across platforms |
| **As a Python developer**, I want to send tabular data to Julia/Dart applications | P1 | DataFrame exchange works with both Arrow IPC and JSON formats | | **As a Python developer**, I want to send tabular data to Julia/Dart applications | P1 | DataFrame exchange works with both Arrow IPC and JSON formats |
| **As a JavaScript developer**, I want to send large files (>0.5MB) from JavaScript applications that lives on a server and also on a browser to other applications | P1 | Large files are automatically uploaded to file server and URLs are sent via NATS | | **As a JavaScript developer**, I want to send large files (>0.5MB) from JavaScript applications that lives on a server and also on a browser to other applications | P1 | Large files are automatically uploaded to file server and URLs are sent via the transport layer |
| **As a Dart developer**, I want to send text messages to other platforms | P1 | Text messages are serialized, encoded, and received correctly across platforms | | **As a Dart developer**, I want to send text messages to other platforms | P1 | Text messages are serialized, encoded, and received correctly across platforms |
| **As a Dart developer**, I want to send dictionary data to other platforms | P1 | JSON-serializable data is exchanged correctly | | **As a Dart developer**, I want to send dictionary data to other platforms | P1 | JSON-serializable data is exchanged correctly |
| **As a Dart developer**, I want to send tabular data (List<Map>) to other platforms | P1 | JSON table format exchange works with Arrow IPC on desktop | | **As a Dart developer**, I want to send tabular data (List<Map>) to other platforms | P1 | JSON table format exchange works with Arrow IPC on desktop |
| **As a Dart developer**, I want to send large files (>0.5MB) | P1 | Large files are automatically uploaded to file server and URLs are sent via NATS | | **As a Dart developer**, I want to send large files (>0.5MB) | P1 | Large files are automatically uploaded to file server and URLs are sent via the transport layer |
| **As a MicroPython developer**, I want to send sensor data with minimal memory usage | P1 | Direct transport works for payloads <100KB on memory-constrained devices | | **As a MicroPython developer**, I want to send sensor data with minimal memory usage | P1 | Direct transport works for payloads <100KB on memory-constrained devices |
| **As a Rust developer**, I want to send and receive messages with type-safe APIs | P1 | Rust implementation uses serde for serialization, tokio for async, and nats-io for NATS connectivity | | **As a Rust developer**, I want to send and receive messages with type-safe APIs | P1 | Rust implementation uses serde for serialization, tokio for async, and transport-agnostic client for connectivity |
| **As a developer**, I want to send mixed-content messages (text + image + file) | P1 | msghandler accepts list of (dataname, data, type) tuples and handles each payload appropriately | | **As a developer**, I want to send mixed-content messages (text + image + file) | P1 | msghandler accepts list of (dataname, data, type) tuples and handles each payload appropriately |
| **As a developer**, I want to receive multi-payload messages | P1 | msghandler returns payloads as list of tuples with correct types preserved | | **As a developer**, I want to receive multi-payload messages | P1 | msghandler returns payloads as list of tuples with correct types preserved |
| **As a developer**, I want to use Plik as the file server | P2 | Plik one-shot upload mode is supported with upload ID and token handling | | **As a developer**, I want to use Plik as the file server | P2 | Plik one-shot upload mode is supported with upload ID and token handling |
@@ -33,16 +33,44 @@ msghandler is a cross-platform, bi-directional data bridge that enables seamless
| **As a developer**, I want automatic retry on file server download failures | P1 | Exponential backoff with configurable retries (default: 5, base_delay: 100ms, max_delay: 5000ms) | | **As a developer**, I want automatic retry on file server download failures | P1 | Exponential backoff with configurable retries (default: 5, base_delay: 100ms, max_delay: 5000ms) |
| **As a developer**, I want message tracing across distributed systems | P1 | Correlation ID is propagated through all message processing steps | | **As a developer**, I want message tracing across distributed systems | P1 | Correlation ID is propagated through all message processing steps |
### 1.3 KPIs & Targets ### 1.3 Success Metrics & KPIs
| Metric | Target | Measurement Method | **Functional Requirements KPIs:**
|--------|--------|-------------------| - **FR-001** (Cross-platform text messaging): 95% of text messages delivered correctly across all platform pairs (<200ms latency) - Measured via synthetic cross-platform tests
| 95% of messages complete within 200ms | 95% | Synthetic monitoring | - **FR-002** (Cross-platform tabular data): 100% Arrow IPC round-trip integrity (Desktop), 100% JSON table round-trip integrity (Browser) - Measured via data validation tests
| <2 days from onboarding to first PR | 2 days | PR timeline tracking | - **FR-003** (Large file handling): 99% successful file uploads to server for payloads ≥0.5MB - Measured via integration tests
| 100% of messages validate against spec | 100% | CI block rate | - **FR-004** (Direct transport for small payloads): 100% of payloads <0.5MB use direct transport - Measured via transport selection tests
| >80% unit test coverage | 80% | Test coverage tools | - **FR-005** (MicroPython support): 100% of payloads <100KB delivered on MicroPython devices - Measured via MicroPython integration tests
| <1% of PRs bypass validation gates | 1% | CI gate analysis | - **FR-006** (Multi-payload messages): 100% correct parsing of multi-payload message lists - Measured via multi-payload tests
| MTTR <15 minutes for P1 incidents | 15 minutes | Incident tracking | - **FR-007** (Payload type preservation): 100% type integrity preserved across all platforms - Measured via type validation tests
- **FR-008** (Plik file server integration): 100% successful Plik upload/token handling - Measured via Plik integration tests
- **FR-009** (Custom file server support): 100% handler abstraction works with custom implementations - Measured via custom server integration tests
- **FR-010** (Exponential backoff retry): 95% successful downloads within retry limit - Measured via failure injection tests
- **FR-011** (Correlation ID propagation): 100% correlation IDs propagated through all steps - Measured via tracing tests
- **FR-012** (Message serialization): <50ms serialization overhead for 10KB payload - Measured via benchmark tests
- **FR-013** (Transport publishing): 100% JSON envelope generated correctly - Measured via serialization tests
- **FR-014** (Transport subscription): 100% JSON messages processed correctly - Measured via deserialization tests
**Non-Functional Requirements KPIs:**
- **NFR-101** (Message serialization overhead): <50ms for 10KB payload - Measured via benchmark tests
- **NFR-102** (Message deserialization overhead): <50ms for 10KB payload - Measured via benchmark tests
- **NFR-103** (Transport connection establishment): <100ms average - Measured via connection pool benchmarks
- **NFR-104** (File upload latency): <1s for 0.5MB file - Measured via integration tests
- **NFR-105** (File download latency): <1s for 0.5MB file - Measured via integration tests
- **NFR-106** (Concurrent connections): 100+ simultaneous transport connections - Measured via scale testing
- **NFR-107** (Message throughput): 1000+ messages/second per instance - Measured via load testing
- **NFR-108** (File server scalability): Horizontal scaling verified via architecture review
- **NFR-201** (Message delivery): At-least-once delivery via transport - Measured via message acknowledgment tests
- **NFR-202** (File server availability): <5% failure rate when file server unavailable - Measured via failure injection tests
- **NFR-203** (Connection recovery): Auto-reconnect within 30s - Measured via connection failure tests
- **NFR-301** (Payload integrity): 100% SHA-256 checksum validation - Measured via integrity tests
- **NFR-302** (Transport security): 100% TLS connections in production - Measured via connection audits
- **NFR-303** (File server security): 100% authenticated file uploads - Measured via security tests
- **NFR-401** (Required logs): 100% messages logged with required fields - Measured via log validation
- **NFR-402** (Critical metrics): 100% metrics collected with 1-minute granularity - Measured via metrics pipeline tests
- **NFR-403** (Tracing): 100% correlation ID propagation for tracing - Measured via tracing validation
- **NFR-404** (Alerting): <5min alert latency for `download_retry_exceeded` - Measured via alert pipeline tests
- **NFR-405** (Retention): Logs: 30 days, Metrics: 1 year - Measured via storage audits
--- ---
@@ -54,42 +82,37 @@ msghandler is a cross-platform, bi-directional data bridge that enables seamless
|---------|-------------| |---------|-------------|
| Cross-platform interoperability | Seamless data exchange between Julia, JavaScript, Python, Dart, Rust, and MicroPython | | Cross-platform interoperability | Seamless data exchange between Julia, JavaScript, Python, Dart, Rust, and MicroPython |
| Intelligent transport selection | Direct transport (<0.5MB) vs Link transport (≥0.5MB) based on payload size | | Intelligent transport selection | Direct transport (<0.5MB) vs Link transport (≥0.5MB) based on payload size |
| Unified API | Consistent `smartsend()` and `smartreceive()` functions across all platforms | | Unified API | Consistent `smartpack()` and `smartunpack()` functions across all platforms |
| Multi-payload support | List of (dataname, data, type) tuples with appropriate handling | | Multi-payload support | List of (dataname, data, type) tuples with appropriate handling |
| File server integration | Plik one-shot upload and custom HTTP server support | | File server integration | Plik one-shot upload and custom HTTP server support |
| Reliability features | Exponential backoff retry and correlation ID propagation | | Reliability features | Exponential backoff retry and correlation ID propagation |
| Message serialization | Converts data types to binary format (Base64, JSON, Arrow IPC) | | Message serialization | Converts data types to binary format (Base64, JSON, Arrow IPC) |
| NATS communication | Publishing and subscription via NATS subjects | | Transport communication | Publishing and subscription via message broker (NATS, MQTT, WebSocket, etc.) |
### 2.2 Out of Scope ### 2.2 Out of Scope
| Feature | Reason | | Feature | Reason |
|---------|--------| |---------|--------|
| NATS JetStream support | Core NATS sufficient for current use cases | | Advanced transport features | Basic transport sufficient for current use cases |
| Message compression | Compression adds complexity without clear benefit | | Message compression | Compression adds complexity without clear benefit |
| Message encryption | Payload encryption is application-layer concern | | Message encryption | Payload encryption is application-layer concern |
| Persistent message queues | NATS request-reply pattern sufficient | | Persistent message queues | Transport request-reply pattern sufficient |
| Advanced routing rules | Simple NATS subject matching sufficient | | Advanced routing rules | Simple topic matching sufficient |
### 2.3 Dependencies ### 2.3 Dependencies
| Platform | Package | Version | | Platform | Package | Version |
|----------|---------|---------| |----------|---------|---------|
| Julia | NATS.jl | Latest stable |
| Julia | JSON.jl | Latest stable | | Julia | JSON.jl | Latest stable |
| Julia | Arrow.jl | Latest stable | | Julia | Arrow.jl | Latest stable |
| Julia | HTTP.jl | Latest stable | | Julia | HTTP.jl | Latest stable |
| Julia | UUIDs.jl | Latest stable | | Julia | UUIDs.jl | Latest stable |
| Node.js | nats | Latest stable |
| Node.js | node-fetch | Latest stable | | Node.js | node-fetch | Latest stable |
| Python | nats-py | Latest stable |
| Python | aiohttp | Latest stable | | Python | aiohttp | Latest stable |
| Python | pyarrow | Latest stable | | Python | pyarrow | Latest stable |
| Browser | nats.ws | Latest stable | | Browser | - | Transport-agnostic (caller provides) |
| Dart | nats | Latest stable |
| Dart | http | Latest stable | | Dart | http | Latest stable |
| Dart | uuid | Latest stable | | Dart | uuid | Latest stable |
| Rust | nats | Latest stable |
| Rust | serde | Latest stable | | Rust | serde | Latest stable |
| Rust | serde_json | Latest stable | | Rust | serde_json | Latest stable |
| Rust | tokio | Latest stable | | Rust | tokio | Latest stable |
@@ -100,7 +123,7 @@ msghandler is a cross-platform, bi-directional data bridge that enables seamless
| Platform | Minimum Version | Notes | | Platform | Minimum Version | Notes |
|----------|-----------------|-------| |----------|-----------------|-------|
| Julia | 1.7+ | Arrow.jl required for arrowtable support | | Julia | 1.7+ | Arrow.jl required for arrowtable support |
| Node.js | 16+ | nats.js required, Arrow IPC supported | | Node.js | 16+ | Transport client required, Arrow IPC supported |
| Python | 3.8+ | pyarrow required for arrowtable support | | Python | 3.8+ | pyarrow required for arrowtable support |
| Browser | Latest | No Arrow IPC (uses jsontable only) | | Browser | Latest | No Arrow IPC (uses jsontable only) |
| Dart | 2.17+ | Supports Desktop (Dart SDK), Flutter (Dart SDK), and Web (Dart SDK) | | Dart | 2.17+ | Supports Desktop (Dart SDK), Flutter (Dart SDK), and Web (Dart SDK) |
@@ -115,8 +138,8 @@ msghandler is a cross-platform, bi-directional data bridge that enables seamless
|----|-------------|-------------| |----|-------------|-------------|
| **FR-001** | Cross-platform text messaging | System shall allow users to send text messages between Julia, JavaScript, Python, and MicroPython applications | | **FR-001** | Cross-platform text messaging | System shall allow users to send text messages between Julia, JavaScript, Python, and MicroPython applications |
| **FR-002** | Cross-platform tabular data | System shall support DataFrame exchange between Julia and Python applications using Arrow IPC format | | **FR-002** | Cross-platform tabular data | System shall support DataFrame exchange between Julia and Python applications using Arrow IPC format |
| **FR-003** | Large file handling | System shall automatically detect payloads ≥0.5MB and upload them to HTTP file server instead of sending via NATS | | **FR-003** | Large file handling | System shall automatically detect payloads ≥0.5MB and upload them to HTTP file server instead of sending via transport |
| **FR-004** | Direct transport for small payloads | System shall send payloads <0.5MB directly via NATS without file server upload | | **FR-004** | Direct transport for small payloads | System shall send payloads <0.5MB directly via transport without file server upload |
| **FR-005** | MicroPython support | System shall support payloads <100KB on MicroPython devices using direct transport | | **FR-005** | MicroPython support | System shall support payloads <100KB on MicroPython devices using direct transport |
| **FR-006** | Multi-payload messages | System shall accept and process lists of (dataname, data, type) tuples | | **FR-006** | Multi-payload messages | System shall accept and process lists of (dataname, data, type) tuples |
| **FR-007** | Payload type preservation | System shall preserve payload types when returning multi-payload messages | | **FR-007** | Payload type preservation | System shall preserve payload types when returning multi-payload messages |
@@ -125,51 +148,56 @@ msghandler is a cross-platform, bi-directional data bridge that enables seamless
| **FR-010** | Exponential backoff retry | System shall implement exponential backoff with configurable retries (default: 5, base_delay: 100ms, max_delay: 5000ms) for file server download failures | | **FR-010** | Exponential backoff retry | System shall implement exponential backoff with configurable retries (default: 5, base_delay: 100ms, max_delay: 5000ms) for file server download failures |
| **FR-011** | Correlation ID propagation | System shall propagate correlation IDs through all message processing steps | | **FR-011** | Correlation ID propagation | System shall propagate correlation IDs through all message processing steps |
| **FR-012** | Message serialization | System shall serialize data types using Base64, JSON, or Arrow IPC encoding | | **FR-012** | Message serialization | System shall serialize data types using Base64, JSON, or Arrow IPC encoding |
| **FR-013** | NATS publishing | System shall return JSON string representation for caller to publish to NATS subjects (caller is responsible for actual NATS publish) | | **FR-013** | Transport publishing | System shall return JSON string representation for caller to publish via transport layer (caller is responsible for actual transport publish) |
| **FR-014** | NATS subscription | System shall receive and process NATS messages by accepting JSON string from NATS payload | | **FR-014** | Transport subscription | System shall receive and process messages by accepting JSON string from transport payload |
--- ---
## 4. Non-Functional Requirements (NFRs) ## 4. Non-Functional Requirements (NFRs)
**Requirement vs KPI Clarification:**
- **FR and NFR** is a *requirement* — it defines what quality or constraint the system must have (e.g., "System shall support 10K TPS", "99.9% monthly uptime", "TLS 1.3+ encryption")
- **KPI** is a *measurement* — it's the actual data collected to verify if the requirement was met (e.g., "Peak traffic was 8.5K TPS", "MTTR was 8 minutes", "100% of connections use TLS 1.3")
- Requirements tell you **what to build**; KPIs tell you **how well you built it**
### 4.1 Performance & Scalability ### 4.1 Performance & Scalability
| ID | Requirement | Specification | Test Method | | ID | Requirement | Specification | KPI | Test Method |
|----|-------------|---------------|-------------| |----|-------------|---------------|-----|-------------|
| **NFR-101** | Message serialization overhead | <50ms for 10KB payload | Benchmark tests | | **NFR-101** | Message serialization overhead | <50ms for 10KB payload | <50ms for 10KB payload | Benchmark tests |
| **NFR-102** | Message deserialization overhead | <50ms for 10KB payload | Benchmark tests | | **NFR-102** | Message deserialization overhead | <50ms for 10KB payload | <50ms for 10KB payload | Benchmark tests |
| **NFR-103** | NATS connection establishment | <100ms | Connection pool benchmarks | | **NFR-103** | Transport connection establishment | <100ms | <100ms average | Connection pool benchmarks |
| **NFR-104** | File upload latency | <1s for 0.5MB file | Integration tests | | **NFR-104** | File upload latency | <1s for 0.5MB file | <1s for 0.5MB file | Integration tests |
| **NFR-105** | File download latency | <1s for 0.5MB file | Integration tests | | **NFR-105** | File download latency | <1s for 0.5MB file | <1s for 0.5MB file | Integration tests |
| **NFR-106** | Concurrent connections | Support 100+ simultaneous NATS connections | Scale testing | | **NFR-106** | Concurrent connections | Support 100+ simultaneous transport connections | 100+ simultaneous connections | Scale testing |
| **NFR-107** | Message throughput | Handle 1000+ messages/second per instance | Load testing | | **NFR-107** | Message throughput | Handle 1000+ messages/second per instance | 1000+ messages/second | Load testing |
| **NFR-108** | File server scalability | Support horizontal scaling of file server backend | Architecture review | | **NFR-108** | File server scalability | Support horizontal scaling of file server backend | Horizontal scaling verified | Architecture review |
### 4.2 Availability & Reliability ### 4.2 Availability & Reliability
| ID | Requirement | Specification | | ID | Requirement | Specification | KPI | Test Method |
|----|-------------|---------------| |----|-------------|---------------|-----|-------------|
| **NFR-201** | Message delivery | At-least-once delivery semantics via NATS | | **NFR-201** | Message delivery | At-least-once delivery semantics via transport | At-least-once delivery via transport | Message acknowledgment tests |
| **NFR-202** | File server availability | Graceful degradation when file server is unavailable | | **NFR-202** | File server availability | Graceful degradation when file server is unavailable | <5% failure rate when file server unavailable | Failure injection tests |
| **NFR-203** | Connection recovery | Auto-reconnect on NATS connection failure | | **NFR-203** | Connection recovery | Auto-reconnect on transport connection failure | Auto-reconnect within 30s | Connection failure tests |
### 4.3 Privacy & Security ### 4.3 Privacy & Security
| ID | Requirement | Specification | | ID | Requirement | Specification | KPI | Test Method |
|----|-------------|---------------| |----|-------------|---------------|-----|-------------|
| **NFR-301** | Payload integrity | SHA-256 checksum support via metadata | | **NFR-301** | Payload integrity | SHA-256 checksum support via metadata | 100% SHA-256 checksum validation | Integrity tests |
| **NFR-302** | Transport security | TLS support for NATS connections | | **NFR-302** | Transport security | TLS support for transport connections | 100% TLS connections in production | Connection audits |
| **NFR-303** | File server security | Authentication token for file uploads | | **NFR-303** | File server security | Authentication token for file uploads | 100% authenticated file uploads | Security tests |
### 4.4 Observability & Telemetry ### 4.4 Observability & Telemetry
| ID | Requirement | Specification | | ID | Requirement | Specification | KPI | Test Method |
|----|-------------|---------------| |----|-------------|---------------|-----|-------------|
| **NFR-401** | Required logs | `correlation_id`, `msg_id`, `timestamp`, `sender_name`, `receiver_name`, `payload_type`, `transport` | | **NFR-401** | Required logs | `correlation_id`, `msg_id`, `timestamp`, `sender_name`, `receiver_name`, `payload_type`, `transport` | 100% messages logged with required fields | Log validation |
| **NFR-402** | Critical metrics | `messages_sent_total`, `messages_received_total`, `file_upload_duration_seconds`, `file_download_duration_seconds`, `retry_attempts_total` | | **NFR-402** | Critical metrics | `messages_sent_total`, `messages_received_total`, `file_upload_duration_seconds`, `file_download_duration_seconds`, `retry_attempts_total` | 100% metrics collected with 1-minute granularity | Metrics pipeline tests |
| **NFR-403** | Tracing | Correlation ID propagation for request tracing | | **NFR-403** | Tracing | Correlation ID propagation for request tracing | 100% correlation ID propagation for tracing | Tracing validation |
| **NFR-404** | Alerting | `download_retry_exceeded` triggers alert when max retries exceeded | | **NFR-404** | Alerting | `download_retry_exceeded` triggers alert when max retries exceeded | <5min alert latency for `download_retry_exceeded` | Alert pipeline tests |
| **NFR-405** | Retention | Logs: 30 days, Metrics: 1 year | | **NFR-405** | Retention | Logs: 30 days, Metrics: 1 year | Logs: 30 days, Metrics: 1 year | Storage audits |
--- ---
@@ -178,7 +206,7 @@ msghandler is a cross-platform, bi-directional data bridge that enables seamless
| Condition | Description | | Condition | Description |
|-----------|-------------| |-----------|-------------|
| **AC-001** | All functional requirements FR-001 through FR-014 are implemented and tested | | **AC-001** | All functional requirements FR-001 through FR-014 are implemented and tested |
| **AC-002** | All non-functional requirements NFR-101 through NFR-405 meet specified targets | | **AC-002** | All non-functional requirements NFR-101 through NFR-405 meet specified KPI targets |
| **AC-003** | Cross-platform text message test passes (Julia ↔ JavaScript ↔ Python) | | **AC-003** | Cross-platform text message test passes (Julia ↔ JavaScript ↔ Python) |
| **AC-004** | Cross-platform tabular data test passes with Arrow IPC round-trip (Desktop) | | **AC-004** | Cross-platform tabular data test passes with Arrow IPC round-trip (Desktop) |
| **AC-005** | Cross-platform tabular data test passes with JSON table round-trip (Browser) | | **AC-005** | Cross-platform tabular data test passes with JSON table round-trip (Browser) |
@@ -234,11 +262,11 @@ msghandler is a cross-platform, bi-directional data bridge that enables seamless
| Platform | Maximum | Notes | | Platform | Maximum | Notes |
|----------|---------|-------| |----------|---------|-------|
| Desktop | Unlimited | Limited by NATS server configuration | | Desktop | Unlimited | Limited by transport server configuration |
| Dart Desktop | Unlimited | Limited by NATS server configuration | | Dart Desktop | Unlimited | Limited by transport server configuration |
| Dart Flutter | Unlimited | Limited by NATS server configuration | | Dart Flutter | Unlimited | Limited by transport server configuration |
| Dart Web | Unlimited | Limited by NATS server configuration | | Dart Web | Unlimited | Limited by transport server configuration |
| Rust | Unlimited | Limited by NATS server configuration | | Rust | Unlimited | Limited by transport server configuration |
| MicroPython | 50KB | Hard limit due to 256KB-1MB memory | | MicroPython | 50KB | Hard limit due to 256KB-1MB memory |
--- ---
@@ -252,7 +280,7 @@ msghandler is a cross-platform, bi-directional data bridge that enables seamless
| `correlation_id` | String (UUID) | Track message flow across systems | | `correlation_id` | String (UUID) | Track message flow across systems |
| `msg_id` | String (UUID) | Unique message identifier | | `msg_id` | String (UUID) | Unique message identifier |
| `timestamp` | String (ISO 8601) | Message publication timestamp | | `timestamp` | String (ISO 8601) | Message publication timestamp |
| `send_to` | String | NATS subject to publish to | | `send_to` | String | Topic/subject to publish to |
| `msg_purpose` | String | ACK, NACK, updateStatus, shutdown, chat | | `msg_purpose` | String | ACK, NACK, updateStatus, shutdown, chat |
| `sender_name` | String | Sender application name | | `sender_name` | String | Sender application name |
| `sender_id` | String (UUID) | Sender unique identifier | | `sender_id` | String (UUID) | Sender unique identifier |
@@ -260,7 +288,7 @@ msghandler is a cross-platform, bi-directional data bridge that enables seamless
| `receiver_id` | String (UUID) | Receiver unique identifier (empty = broadcast) | | `receiver_id` | String (UUID) | Receiver unique identifier (empty = broadcast) |
| `reply_to` | String | Topic for reply messages | | `reply_to` | String | Topic for reply messages |
| `reply_to_msg_id` | String | Message ID being replied to | | `reply_to_msg_id` | String | Message ID being replied to |
| `broker_url` | String | NATS server URL | | `broker_url` | String | Broker URL for the transport layer |
| `metadata` | Dict | Message-level metadata | | `metadata` | Dict | Message-level metadata |
| `payloads` | Array | List of payload objects | | `payloads` | Array | List of payload objects |
@@ -289,14 +317,14 @@ msghandler is a cross-platform, bi-directional data bridge that enables seamless
| `Failed to upload` | File server error | Throw error | | `Failed to upload` | File server error | Throw error |
| `Failed to fetch` | File server unavailable | Retry with exponential backoff | | `Failed to fetch` | File server unavailable | Retry with exponential backoff |
| `Unknown transport` | Invalid transport type | Throw error | | `Unknown transport` | Invalid transport type | Throw error |
| `NATS connection failed` | NATS unavailable | Throw error | | `Transport connection failed` | Transport/broker unavailable | Throw error |
### 9.2 Exception Handling ### 9.2 Exception Handling
| Scenario | Handler | | Scenario | Handler |
|----------|---------| |----------|---------|
| File server unavailable | Retry up to 5 times with exponential backoff | | File server unavailable | Retry up to 5 times with exponential backoff |
| NATS publish failure | Connection auto-reconnect | | Transport publish failure | Handled by caller |
| Deserialization error | Log correlation ID and throw error | | Deserialization error | Log correlation ID and throw error |
| Memory overflow (MicroPython) | Reject payloads >50KB | | Memory overflow (MicroPython) | Reject payloads >50KB |
@@ -328,10 +356,10 @@ msghandler is a cross-platform, bi-directional data bridge that enables seamless
## 11. API Contract ## 11. API Contract
### 11.1 smartsend Signature ### 11.1 smartpack Signature
```julia ```julia
function smartsend( function smartpack(
subject::String, subject::String,
data::AbstractArray{Tuple{String, T1, String}, 1}; data::AbstractArray{Tuple{String, T1, String}, 1};
broker_url::String = DEFAULT_BROKER_URL, broker_url::String = DEFAULT_BROKER_URL,
@@ -350,12 +378,12 @@ function smartsend(
)::Tuple{msg_envelope_v1, String} where {T1<:Any} )::Tuple{msg_envelope_v1, String} where {T1<:Any}
``` ```
**Note**: NATS publishing is the caller's responsibility. `smartsend` returns `(env::msg_envelope_v1, env_json_str::String)`. **Note**: Publishing via the transport layer is the caller's responsibility. `smartpack` returns `(env::msg_envelope_v1, env_json_str::String)`.
### 11.2 smartreceive Signature ### 11.2 smartunpack Signature
```julia ```julia
function smartreceive( function smartunpack(
msg_json_str::String; msg_json_str::String;
fileserver_download_handler::Function = _fetch_with_backoff, fileserver_download_handler::Function = _fetch_with_backoff,
max_retries::Int = 5, max_retries::Int = 5,
@@ -364,7 +392,9 @@ function smartreceive(
)::JSON.Object{String, Any} )::JSON.Object{String, Any}
``` ```
**Note**: Pass `String(nats_msg.payload)` from NATS subscription to `smartreceive`. **Note**: Pass the payload string from the transport subscription to `smartunpack`. The input is the JSON string payload from the transport message, not the transport message object directly.
**Note**: Pass the payload from the transport subscription to `smartunpack`.
--- ---
@@ -374,7 +404,7 @@ function smartreceive(
| Component | Minimum | Notes | | Component | Minimum | Notes |
|-----------|---------|-------| |-----------|---------|-------|
| NATS Server | 1 instance | Single node for development | | Message Broker | 1 instance | Single node for development |
| File Server | 1 instance | HTTP server for large payloads | | File Server | 1 instance | HTTP server for large payloads |
| Client Memory | 50MB | Desktop platforms (Julia/JS/Python/Dart) | | Client Memory | 50MB | Desktop platforms (Julia/JS/Python/Dart) |
| Client Memory | 256KB | MicroPython devices | | Client Memory | 256KB | MicroPython devices |
@@ -383,7 +413,7 @@ function smartreceive(
| Variable | Default | Description | | Variable | Default | Description |
|----------|---------|-------------| |----------|---------|-------------|
| `NATS_URL` | `nats://localhost:4222` | NATS server URL | | `BROKER_URL` | `ws://localhost:4222` | Message broker URL |
| `FILESERVER_URL` | `http://localhost:8080` | HTTP file server URL | | `FILESERVER_URL` | `http://localhost:8080` | HTTP file server URL |
| `SIZE_THRESHOLD` | `500000` | Size threshold in bytes (0.5MB) | | `SIZE_THRESHOLD` | `500000` | Size threshold in bytes (0.5MB) |
@@ -409,9 +439,17 @@ function smartreceive(
| Date | Version | Changes | | Date | Version | Changes |
|------|---------|---------| |------|---------|---------|
| 2026-05-22 | 1.3.0 | Updated to ASG Framework v8 pillars - added KPIs to all FR and NFR requirements |
| - | - | Added Success Metrics & KPIs section with measurable targets for each requirement |
| - | - | Added NFR vs KPI clarification section |
| - | - | Updated NFR tables to include KPI column and Test Method column |
| 2026-05-15 | 1.3.0 | Made transport layer agnostic |
| - | - | Removed all NATS-specific dependencies and references |
| - | - | Updated all NATS references to generic "transport layer"/"message broker" |
| - | - | Removed NATS client packages from dependencies tables |
| 2026-05-13 | 1.2.0 | Aligned with ground truth implementation (src/msghandler.jl) | | 2026-05-13 | 1.2.0 | Aligned with ground truth implementation (src/msghandler.jl) |
| - | - | Fixed smartsend signature: removed is_publish, NATS_connection; added sender_name | | - | - | Fixed smartpack signature: removed is_publish, NATS_connection; added sender_name |
| - | - | Fixed smartreceive signature: takes msg_json_str::String instead of msg::NATS.Msg | | - | - | Fixed smartunpack signature: takes msg_json_str::String instead of msg::NATS.Msg |
| - | - | Fixed size_threshold default from 1,000,000 to 500,000 | | - | - | Fixed size_threshold default from 1,000,000 to 500,000 |
| - | - | Updated FR-013/FR-014 to reflect caller responsibility for NATS publishing | | - | - | Updated FR-013/FR-014 to reflect caller responsibility for NATS publishing |
| - | - | Updated FR-008/FR-009 to include file path upload overload | | - | - | Updated FR-008/FR-009 to include file path upload overload |
@@ -420,7 +458,47 @@ function smartreceive(
--- ---
## 15. References ## 15. Requirements Traceability Matrix
| Requirement ID | Description | Implementation File | Test File |
|----------------|-------------|---------------------|-----------|
| FR-001 | Cross-platform text messaging | `src/msghandler.jl` | `test/test_*_sender.*` |
| FR-002 | Cross-platform tabular data | `src/msghandler.jl` | `test/test_*_sender.*` |
| FR-003 | Large file handling | `src/msghandler.jl` | `test/test_*_fileserver.*` |
| FR-004 | Direct transport for small payloads | `src/msghandler.jl` | `test/test_*_transport.*` |
| FR-005 | MicroPython support | `src/msghandler_mpy.py` | `test/test_mpy.*` |
| FR-006 | Multi-payload messages | `src/msghandler.jl` | `test/test_*_mix_payloads.*` |
| FR-007 | Payload type preservation | `src/msghandler.jl` | `test/test_*_types.*` |
| FR-008 | Plik file server integration | `src/msghandler.jl` | `test/test_plik.*` |
| FR-009 | Custom file server support | `src/msghandler.jl` | `test/test_custom_server.*` |
| FR-010 | Exponential backoff retry | `src/msghandler.jl` | `test/test_retry.*` |
| FR-011 | Correlation ID propagation | `src/msghandler.jl` | `test/test_tracing.*` |
| FR-012 | Message serialization | `src/msghandler.jl` | `test/test_*_serialization.*` |
| FR-013 | Transport publishing | `src/msghandler.jl` | `test/test_*_transport.*` |
| FR-014 | Transport subscription | `src/msghandler.jl` | `test/test_*_receiver.*` |
| NFR-101 | Message serialization overhead | `src/msghandler.jl` | `test/test_benchmarks.*` |
| NFR-102 | Message deserialization overhead | `src/msghandler.jl` | `test/test_benchmarks.*` |
| NFR-103 | Transport connection establishment | `src/msghandler.jl` | `test/test_connection.*` |
| NFR-104 | File upload latency | `src/msghandler.jl` | `test/test_fileserver.*` |
| NFR-105 | File download latency | `src/msghandler.jl` | `test/test_fileserver.*` |
| NFR-106 | Concurrent connections | `src/msghandler.jl` | `test/test_scale.*` |
| NFR-107 | Message throughput | `src/msghandler.jl` | `test/test_load.*` |
| NFR-108 | File server scalability | `docs/architecture.md` | `docs/validation.md` |
| NFR-201 | Message delivery | `src/msghandler.jl` | `test/test_delivery.*` |
| NFR-202 | File server availability | `src/msghandler.jl` | `test/test_failure_injection.*` |
| NFR-203 | Connection recovery | `src/msghandler.jl` | `test/test_reconnect.*` |
| NFR-301 | Payload integrity | `src/msghandler.jl` | `test/test_integrity.*` |
| NFR-302 | Transport security | `src/msghandler.jl` | `test/test_security.*` |
| NFR-303 | File server security | `src/msghandler.jl` | `test/test_security.*` |
| NFR-401 | Required logs | `src/msghandler.jl` | `test/test_logging.*` |
| NFR-402 | Critical metrics | `src/msghandler.jl` | `test/test_metrics.*` |
| NFR-403 | Tracing | `src/msghandler.jl` | `test/test_tracing.*` |
| NFR-404 | Alerting | `src/msghandler.jl` | `test/test_alerting.*` |
| NFR-405 | Retention | `docs/runbook.md` | `docs/validation.md` |
---
## 16. References
- [`src/msghandler.jl`](../src/msghandler.jl) - Ground truth implementation (Julia) - [`src/msghandler.jl`](../src/msghandler.jl) - Ground truth implementation (Julia)
- [`src/msghandler_ssr.js`](../src/msghandler_ssr.js) - Server-side JavaScript implementation - [`src/msghandler_ssr.js`](../src/msghandler_ssr.js) - Server-side JavaScript implementation

397
docs/solution-design.md Normal file
View File

@@ -0,0 +1,397 @@
# Solution Design: msghandler
**Version**: 1.3.0
**Date**: 2026-05-22
**Status**: Active
**Ground Truth**: [`src/msghandler.jl`](../src/msghandler.jl)
**ASG Framework Alignment**: v8 pillars - Requirements → Solution Design → Specification → Walkthrough → Implementation Plan → Validation → Runbook
---
## 1. Problem Decomposition
msghandler addresses the challenge of cross-platform data exchange between **Julia**, **JavaScript**, **Python**, **Dart**, **Rust**, and **MicroPython** applications using message brokers as transport layers.
### User Problems
| Problem | Description | User Impact | Requirement ID |
|---------|-------------|-------------|----------------|
| **P-001**: Cross-platform data serialization | Different languages have incompatible data types and serialization formats | Developers must write platform-specific conversion code | FR-001, FR-002 |
| **P-002**: Large payload handling | Message brokers have size limits, but large files need to be transferred | Large files either fail or require complex workarounds | FR-003 |
| **P-003**: Transport abstraction | Each platform has different message broker libraries and APIs | No unified interface across platforms | FR-013, FR-014 |
| **P-004**: Request-response patterns | Bi-directional communication requires complex correlation tracking | Developers must implement custom message routing | FR-011 |
| **P-005**: File server reliability | File server may be temporarily unavailable during downloads | Failed downloads without retry mechanism | FR-010 |
| **P-006**: Payload type preservation | Different platforms have different type systems | Data corruption or misinterpretation on receiving end | FR-006, FR-007 |
### Solution Boundaries
**In Scope**:
- Unified API for `smartpack()` and `smartunpack()` across all platforms
- Automatic transport selection based on payload size
- File server integration using Claim-Check pattern
- Multi-payload support with mixed types in single message
- Exponential backoff for reliable file downloads
- Correlation ID propagation for message tracing
**Out of Scope**:
- Message compression (adds complexity without clear benefit)
- Message encryption (application-layer concern)
- Advanced message routing (simple topic matching sufficient)
- Persistent message queues (transport pattern sufficient)
### Decision IDs
| Decision ID | Decision | Description | Requirement IDs | NFR IDs |
|-------------|----------|-------------|-----------------|---------|
| SD-001 | Claim-Check Pattern | Large payloads uploaded to HTTP server, small payloads sent directly | FR-003, FR-004 | NFR-104, NFR-105 |
| SD-002 | Automatic Transport Selection | <0.5MB = direct, ≥0.5MB = link based on size threshold | FR-003, FR-004 | NFR-104, NFR-105 |
| SD-003 | Handler Function Abstraction | Pluggable file server implementations via handler functions | FR-008, FR-009 | NFR-202 |
| SD-004 | Unified Tuple Format | Same `(dataname, data, type)` format across all platforms | FR-006, FR-007 | - |
| SD-005 | Base64 Encoding | JSON-compatible binary data transport | FR-012 | - |
| SD-006 | Transport Abstraction | Support multiple broker protocols (NATS/MQTT/WebSocket) transparently | FR-013, FR-014 | NFR-201 |
| SD-007 | Exponential Backoff | Retry failed file downloads with exponential backoff | FR-010 | NFR-202 |
| SD-008 | Correlation ID Propagation | Propagate correlation IDs through all message processing steps | FR-011 | NFR-401, NFR-403 |
---
## 2. Solution Approach
msghandler implements a **Claim-Check pattern** with intelligent transport selection:
```
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) │ │ │
│ │ └───────────────────┘
└───────────────┘
```
### Key Design Decisions
| Decision ID | Decision | Rationale | Alternatives Rejected |
|-------------|----------|-----------|----------------------|
| **SD-001** | Claim-Check Pattern | Large payloads (>0.5MB) uploaded to HTTP server, small payloads sent directly via transport | Client-side compression - adds complexity; Server-side compression - not universally supported |
| **SD-002** | Automatic Transport Selection | <0.5MB = direct (fast), ≥0.5MB = link (avoid transport limits) | Manual selection - error-prone; Fixed threshold - not adaptive |
| **SD-003** | Handler Function Abstraction | Allows pluggable file server implementations (Plik, AWS S3, custom) | Hardcoded Plik - not flexible; Interface-based - too complex for this use case |
| **SD-004** | Unified Tuple Format | Same input/output format across all platforms | Platform-native formats - no interoperability; Protocol buffers - too heavy |
| **SD-005** | Base64 Encoding | JSON-compatible binary data transport | Raw bytes - not JSON-compatible; Hex encoding - 2x size overhead |
| **SD-006** | Transport Abstraction | Support multiple broker protocols (NATS/MQTT/WebSocket) transparently | Platform-specific libraries - no interoperability |
| **SD-007** | Exponential Backoff | Retry failed file downloads with exponential backoff | Simple retry - too aggressive; No retry - poor reliability |
| **SD-008** | Correlation ID Propagation | Propagate correlation IDs through all message processing steps | Manual correlation - error-prone; No tracing - debug impossible |
### Architecture Components
```mermaid
flowchart TB
subgraph Client["Client Application"]
direction TB
APP["Application Code"]
API["msghandler API"]
APP -->|Data tuples| API
API -->|JSON envelope| TRANSPORT
end
subgraph Transport["Transport Layer"]
direction TB
BROKER["Message Broker<br/>NATS/MQTT/WebSocket"]
TOPICS["Topic Subscription"]
API -->|Publish| BROKER
BROKER -->|Deliver| TOPICS
TOPICS -->|Subscribe| API
end
subgraph FileServer["File Server"]
direction TB
UPLOAD["Upload Handler"]
DOWNLOAD["Download Handler"]
API -.->|Upload URL| UPLOAD
DOWNLOAD -.->|Fetch URL| API
end
style Client fill:#e1f5fe,stroke:#0288d1,stroke-width:2px
style Transport fill:#ffe0b2,stroke:#f57c00,stroke-width:2px
style FileServer fill:#c8e6c9,stroke:#43a047,stroke-width:2px
```
---
## 3. Alternatives Considered
| Alternative | Pros | Cons | Decision |
|-------------|------|------|----------|
| **gRPC/Protobuf** | Strong typing, efficient binary format | No native MicroPython support; Complex schema management | Rejected - not cross-platform enough |
| **MessagePack** | Compact binary, good performance | Browser support limited; No standard for tabular data | Rejected - missing Arrow IPC alternative |
| **Protocol Buffers** | Type-safe, efficient | No native support for tabular data exchange | Rejected - cannot represent DataFrames natively |
| **REST HTTP Upload** | Simple, universal | High latency; No real-time capability | Rejected - not suitable for message broker pattern |
| **Hybrid (direct/link)** | Optimal for both small and large payloads | More complex implementation | Accepted - matches user requirements (FR-003, FR-004) |
| **Single transport type** | Simpler implementation | Cannot handle large payloads efficiently | Rejected - violates FR-003 requirement |
| **Platform-specific APIs** | Native performance | No interoperability; Maintenance burden | Rejected - violates cross-platform goal |
---
## 4. High-Level Component Diagram
```mermaid
flowchart TD
subgraph msghandler["msghandler Core Module"]
direction TB
subgraph Serialization["Serialization Layer"]
DIR["Direct Transport"]
LNK["Link Transport"]
DIR -->|Base64| JSON_MSG
LNK -->|HTTP URL| JSON_MSG
end
subgraph Envelope["Envelope Builder"]
HDR["Message Header"]
PAY["Payload Manager"]
HDR --> PAY
end
subgraph Handlers["Handler Functions"]
UPD["Upload Handler"]
DWN["Download Handler"]
UPD --> LNK
DWN --> LNK
end
API["smartpack() / smartunpack()"]
API -->|Input| Serialization
API -->|Output| Serialization
API -->|Configure| Handlers
end
subgraph Transport["Transport Layer"]
BROKER["NATS / MQTT / WebSocket"]
API -->|JSON| BROKER
BROKER -->|JSON| API
end
subgraph FileServer["File Server"]
Plik["HTTP Server"]
UPD -.->|POST| Plik
Plik -.->|URL| DWN
end
style msghandler fill:#b3e5fc,stroke:#0288d1,stroke-width:2px
style Transport fill:#ffe0b2,stroke:#f57c00,stroke-width:2px
style FileServer fill:#c8e6c9,stroke:#43a047,stroke-width:2px
```
### Component Responsibilities
| Component | Responsibilities | Decision IDs | Requirements Addressed |
|-----------|-----------------|--------------|----------------------|
| **Serialization Layer** | Convert data types to transport format (Base64/URL) | SD-005 | FR-001, FR-002, FR-012 |
| **Envelope Builder** | Create standardized message envelope with metadata | SD-001, SD-008 | FR-011, FR-013, FR-014 |
| **Handler Functions** | Abstract file server operations for pluggability | SD-003, SD-007 | FR-008, FR-009, FR-010 |
| **Transport Adapter** | Support multiple broker protocols transparently | SD-006 | FR-013, FR-014 |
| **Payload Manager** | Track payload types, sizes, and encoding | SD-004 | FR-006, FR-007 |
---
## 5. Decision Rationale
### SD-001: Why Claim-Check Pattern?
**Requirement**: FR-003 (Large file handling), FR-004 (Direct transport for small payloads)
**NFRs**: NFR-104 (File upload latency <1s), NFR-105 (File download latency <1s)
**Rationale**:
- Transport layers (NATS, MQTT) have message size limits (typically 1MB)
- Direct transport is faster for small payloads (no file server round-trip)
- Link transport avoids transport limits for large payloads
- User doesn't need to manually choose - automatic selection based on threshold
### SD-002: Why Handler Functions for File Server?
**Requirement**: FR-008 (Plik integration), FR-009 (Custom file server support)
**NFR**: NFR-202 (File server availability <5% failure rate)
**Rationale**:
- Plik is common open-source solution for file server
- Some users need AWS S3 or custom implementation
- Handler functions provide clean abstraction without vendor lock-in
- Same signature across all platforms (unified API)
### SD-003: Why Tuple Format for Payloads?
**Requirement**: FR-006 (Multi-payload messages), FR-007 (Payload type preservation)
**Rationale**:
- `(dataname, data, type)` tuple is language-agnostic
- Simple to understand: name, content, type
- Supports mixed payload types in single message
- Easy to serialize/deserialize across platforms
### SD-004: Why Base64 Encoding?
**Requirement**: FR-012 (Message serialization), FR-001 (Cross-platform text messaging)
**Rationale**:
- JSON is universal - works on all platforms
- Base64 converts binary to ASCII for JSON compatibility
- Standard format with native support in all languages
- No additional dependencies needed
### SD-005: Why Automatic Transport Selection?
**Requirement**: FR-003 (Large file handling), FR-004 (Direct transport for small payloads)
**NFRs**: NFR-104 (File upload latency <1s), NFR-105 (File download latency <1s)
**Rationale**:
- <0.5MB payloads use direct transport (<1s latency, FR-004 KPI)
- ≥0.5MB payloads use link transport to avoid transport limits (FR-003 KPI: 99% successful uploads)
- User doesn't need to manually choose - automatic selection based on threshold
### SD-006: Why Transport Abstraction?
**Requirement**: FR-013 (Transport publishing), FR-014 (Transport subscription)
**NFR**: NFR-201 (Message delivery at-least-once)
**Rationale**:
- Support multiple broker protocols (NATS, MQTT, WebSocket) transparently
- Caller handles actual transport publishing/subscription
- Unified API across all platforms
- At-least-once delivery semantics via transport layer
### SD-007: Why Exponential Backoff?
**Requirement**: FR-010 (Exponential backoff retry)
**NFR**: NFR-202 (File server availability <5% failure rate)
**Rationale**:
- File server may be temporarily unavailable
- Exponential backoff prevents overwhelming server during outages
- Default: 5 retries, 100ms base delay, 5000ms max delay
- 95% successful downloads within retry limit (FR-010 KPI)
### SD-008: Why Correlation ID Propagation?
**Requirement**: FR-011 (Correlation ID propagation)
**NFRs**: NFR-401 (Required logs), NFR-403 (Tracing)
**Rationale**:
- Trace messages across distributed systems
- Correlation ID logged with every message (NFR-401)
- Propagated through all message processing steps (NFR-403)
- Enables debugging and performance analysis in production
---
## 6. Risk Assessment
| Risk | Impact | Probability | Mitigation | Requirement IDs | NFR IDs |
|------|--------|-------------|------------|-----------------|---------|
| **Performance degradation with >500KB payloads** | High | Medium | Size threshold detection; Link transport fallback | FR-003, FR-004 | NFR-104, NFR-105 |
| **File server availability issues** | Medium | Low | Exponential backoff retry; Graceful degradation | FR-010 | NFR-202 |
| **Platform-specific bugs** | Medium | Low | Comprehensive test suite per platform; CI validation | FR-001, FR-002, FR-006, FR-007 | - |
| **Encoding mismatches between platforms** | High | Low | Strict specification; Test contracts; Validation rules | FR-012 | NFR-301 |
| **Transport layer incompatibility** | Medium | Low | Transport-agnostic design; Handler abstraction | FR-013, FR-014 | NFR-201 |
| **Correlation ID loss in processing** | Medium | Low | Centralized trace context management | FR-011 | NFR-401, NFR-403 |
---
## 7. Requirements Traceability
| Solution Component | Decision ID | Requirement ID | Description |
|-------------------|-------------|----------------|-------------|
| **smartpack() function** | SD-001, SD-002, SD-004, SD-005, SD-006, SD-008 | FR-001, FR-002, FR-003, FR-004, FR-005, FR-006, FR-007, FR-008, FR-009, FR-010, FR-011, FR-012, FR-013, FR-014 | Unified API for sending messages across all platforms |
| **smartunpack() function** | SD-001, SD-002, SD-004, SD-005, SD-006, SD-007, SD-008 | FR-001, FR-002, FR-003, FR-004, FR-005, FR-006, FR-007, FR-008, FR-009, FR-010, FR-011, FR-012, FR-013, FR-014 | Unified API for receiving messages across all platforms |
| **Direct transport** | SD-002 | FR-004 | Send payloads < threshold directly via transport |
| **Link transport** | SD-001, SD-002 | FR-003 | Upload payloads ≥ threshold to file server |
| **File server handler** | SD-003, SD-007 | FR-008, FR-009, FR-010 | Pluggable upload/download handlers with retry logic |
| **Payload type preservation** | SD-004 | FR-006, FR-007 | Support text, dictionary, arrowtable, jsontable, image, audio, video, binary |
| **Correlation ID propagation** | SD-008 | FR-011 | Message tracing across distributed systems |
| **Multi-payload support** | SD-004 | FR-006, FR-007 | List of (dataname, data, type) tuples |
### Non-Functional Requirements Traceability
| Solution Component | Decision ID | NFR ID | Description |
|-------------------|-------------|--------|-------------|
| **Serialization optimization** | SD-005 | NFR-101, NFR-102 | <50ms overhead for 10KB payloads |
| **Transport efficiency** | SD-006 | NFR-103 | <100ms connection establishment |
| **File server latency** | SD-001, SD-002 | NFR-104, NFR-105 | <1s upload/download for 0.5MB files |
| **Concurrent connections** | SD-006 | NFR-106 | Support 100+ simultaneous connections |
| **Message throughput** | SD-005, SD-006 | NFR-107 | Handle 1000+ messages/second per instance |
| **At-least-once delivery** | SD-006 | NFR-201 | Transport layer semantics |
| **Graceful degradation** | SD-003, SD-007 | NFR-202 | File server unavailability handling |
| **Auto-reconnect** | SD-006 | NFR-203 | Transport connection failure recovery |
| **Payload integrity** | SD-005 | NFR-301 | 100% SHA-256 checksum validation |
| **Transport security** | SD-006 | NFR-302 | 100% TLS connections in production |
| **File server security** | SD-003 | NFR-303 | 100% authenticated file uploads |
| **Required logs** | SD-001, SD-008 | NFR-401 | Correlation ID, msg_id, timestamp, etc. |
| **Critical metrics** | SD-001, SD-005 | NFR-402 | messages_sent_total, file upload/download duration |
| **Tracing** | SD-001, SD-008 | NFR-403 | Correlation ID propagation |
| **Alerting** | SD-007 | NFR-404 | <5min alert latency for `download_retry_exceeded` |
---
## 8. Gap-Check Validation
| Stage Transition | Gap-Check Question | Status |
|------------------|-------------------|--------|
| **Requirements → Solution Design** | Does the Solution Design clearly explain how the system solves the user problem, not just what it does? | ✅ Verified - All user problems mapped to solution components with requirement ID and decision ID references |
| **Solution Design → Specification** | Does the Specification define all technical details that the solution approach requires? | ⏳ Pending - Specification needs review for completeness |
| **Solution Design → Walkthrough** | Does the Walkthrough reflect the complete flow including error states and timing? | ⏳ Pending - Walkthrough needs validation against design |
### Solution Design Validation
**User Problems** (from requirements.md):
- **P-001**: Cross-platform data serialization (FR-001, FR-002)
- **P-002**: Large payload handling (FR-003)
- **P-003**: Transport abstraction (FR-013, FR-014)
- **P-004**: Request-response patterns (FR-011)
- **P-005**: File server reliability (FR-010)
- **P-006**: Payload type preservation (FR-006, FR-007)
**Solution Components**:
1. **SD-001** - `smartpack()` / `smartunpack()` - Unified API for all platforms
2. **SD-002** - Claim-Check pattern - Automatic transport selection based on size threshold
3. **SD-003** - Handler function abstraction - Plik/AWS S3/custom file server support
4. **SD-004** - Tuple format - `(dataname, data, type)` - platform-agnostic
5. **SD-005** - Base64 encoding - JSON-compatible binary data transport
6. **SD-006** - Transport abstraction - Support multiple broker protocols transparently
7. **SD-007** - Exponential backoff - Reliable file downloads with retry logic
8. **SD-008** - Correlation ID propagation - Message tracing across distributed systems
**Requirement Mapping**:
- **Functional Requirements**: FR-001 through FR-014 ✅
- **Non-Functional Requirements**: NFR-101 through NFR-405 ✅
**Gap Check**: Does this solution explain *how* users will actually use the system?
**Answer**: Yes - the walkthrough provides concrete examples:
1. JavaScript sends `[(msg, "Hello", "text"), (avatar, binary_data, "image")]`
2. `smartpack()` automatically selects transport based on size (SD-002)
3. Large file (≥0.5MB) → link transport → file server upload (SD-001)
4. Small payload (<0.5MB) → direct transport → base64 encoding (SD-005)
5. Receiver calls `smartunpack()` → receives same tuple format with preserved types
**NFR Traceability**:
- **Performance**: NFR-101 (serialization <50ms), NFR-102 (deserialization <50ms), NFR-103 (connection <100ms) ✅
- **Reliability**: NFR-201 (at-least-once delivery), NFR-202 (file server <5% failure), NFR-203 (auto-reconnect <30s) ✅
- **Security**: NFR-301 (SHA-256 checksum), NFR-302 (TLS 100%), NFR-303 (authenticated uploads) ✅
- **Observability**: NFR-401 (required logs), NFR-402 (metrics), NFR-403 (tracing), NFR-404 (alerting <5min) ✅
---
*This solution design document is versioned and maintained in git alongside the codebase. All implementations must adhere to this design.*
**Traceability Summary**:
- All requirements traced to solution components with SD-XXX decision IDs
- Each decision ID references the corresponding requirement IDs (FR-XXX, NFR-XXX)
- Specification must cite SD-XXX references for each technical detail

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

12
etc.txt
View File

@@ -1,7 +1,7 @@
#!/usr/bin/env julia #!/usr/bin/env julia
# Test script for mixed-content message testing # Test script for mixed-content message testing
# Tests receiving a mix of text, json, table, image, audio, video, and binary data # Tests receiving a mix of text, json, table, image, audio, video, and binary data
# from Julia serviceA to Julia serviceB using msghandler.jl smartreceive # from Julia serviceA to Julia serviceB using msghandler.jl smartunpack
# #
# This test demonstrates that any combination and any number of mixed content # This test demonstrates that any combination and any number of mixed content
# can be sent and received correctly. # can be sent and received correctly.
@@ -38,9 +38,9 @@ function test_mix_receive()
log_trace("Received message on $(msg.subject)") log_trace("Received message on $(msg.subject)")
incoming_msg = msg incoming_msg = msg
# # Use msghandler.smartreceive to handle the data # # Use msghandler.smartunpack to handle the data
# # API: smartreceive(msg, download_handler; max_retries, base_delay, max_delay) # # API: smartunpack(msg, download_handler; max_retries, base_delay, max_delay)
# result = msghandler.smartreceive( # result = msghandler.smartunpack(
# msg; # msg;
# max_retries = 5, # max_retries = 5,
# base_delay = 100, # base_delay = 100,
@@ -229,7 +229,7 @@ println("Note: This receiver will wait for messages from the sender.")
println("Run test_julia_to_julia_mix_sender.jl first to send test data.") println("Run test_julia_to_julia_mix_sender.jl first to send test data.")
# Run receiver # Run receiver
println("\ntesting smartreceive for mixed content") println("\ntesting smartunpack for mixed content")
incoming_msg = test_mix_receive() incoming_msg = test_mix_receive()
println("\nTest completed.") println("\nTest completed.")
@@ -250,7 +250,7 @@ println("\nTest completed.")
Check architecture.md. For sending table I want to add JSON in addition to Apache Arrow. Check architecture.md. For sending table I want to add JSON in addition to Apache Arrow.
Currently I use "table" datatype when sending table data using Arrow. Now table that I want to send using JSON Currently I use "table" datatype when sending table data using Arrow. Now table that I want to send using JSON
I will use "jsontable" as datatype while sending table using Arrow I will use "arrowtable" as datatype. I will use "jsontable" as datatype while sending table using Arrow I will use "arrowtable" as datatype.
This will select how smartsend and smartreceive serialize/deserialize the table. This will select how smartpack and smartunpack serialize/deserialize the table.
Can you help me do this? Save the updated architecture.md into updated_architecture.md file. I will deal with source code later. Can you help me do this? Save the updated architecture.md into updated_architecture.md file. I will deal with source code later.

View File

@@ -1,8 +1,7 @@
use msghandler::{smartreceive, SmartreceiveOptions}; use msghandler::{smartunpack, smartunpackOptions};
#[tokio::main] fn main() {
async fn main() { // Simulated message JSON (received via any transport)
// Simulated NATS message JSON (received from NATS subscription)
let msg_json_str = r#"{ let msg_json_str = r#"{
"correlation_id": "abc123-def456-ghi789", "correlation_id": "abc123-def456-ghi789",
"msg_id": "msg-uuid-001", "msg_id": "msg-uuid-001",
@@ -15,7 +14,7 @@ async fn main() {
"receiver_id": "", "receiver_id": "",
"reply_to": "/agent/wine/api/v1/response", "reply_to": "/agent/wine/api/v1/response",
"reply_to_msg_id": "", "reply_to_msg_id": "",
"broker_url": "nats://localhost:4222", "broker_url": "localhost:4222",
"metadata": {}, "metadata": {},
"payloads": [ "payloads": [
{ {
@@ -41,9 +40,9 @@ async fn main() {
] ]
}"#; }"#;
let options = SmartreceiveOptions::default(); let options = smartunpackOptions::default();
match smartreceive(msg_json_str, &options).await { match smartunpack(msg_json_str, &options) {
Ok(envelope) => { Ok(envelope) => {
println!("=== Envelope Received ==="); println!("=== Envelope Received ===");
println!("Correlation ID: {}", envelope.correlation_id); println!("Correlation ID: {}", envelope.correlation_id);

View File

@@ -1,7 +1,6 @@
use msghandler::{smartsend, Payload, SmartsendOptions}; use msghandler::{smartpack, Payload, smartpackOptions};
#[tokio::main] fn main() {
async fn main() {
// Create mixed payload data // Create mixed payload data
let payloads = vec![ let payloads = vec![
( (
@@ -25,15 +24,15 @@ async fn main() {
), ),
]; ];
let options = SmartsendOptions { let options = smartpackOptions {
broker_url: "nats://localhost:4222".to_string(), broker_url: "localhost:4222".to_string(),
fileserver_url: "http://localhost:8080".to_string(), fileserver_url: "http://localhost:8080".to_string(),
msg_purpose: "chat".to_string(), msg_purpose: "chat".to_string(),
sender_name: "rust-example".to_string(), sender_name: "rust-example".to_string(),
..Default::default() ..Default::default()
}; };
match smartsend("/agent/wine/api/v1/prompt", &payloads, &options).await { match smartpack("/agent/wine/api/v1/prompt", &payloads, &options) {
Ok((envelope, json_str)) => { Ok((envelope, json_str)) => {
println!("=== Envelope Created ==="); println!("=== Envelope Created ===");
println!("Correlation ID: {}", envelope.correlation_id); println!("Correlation ID: {}", envelope.correlation_id);
@@ -60,7 +59,7 @@ async fn main() {
} }
println!(); println!();
println!("=== JSON String for NATS Publishing ==="); println!("=== JSON String for Transport Publishing ===");
println!("{}", json_str); println!("{}", json_str);
} }
Err(e) => { Err(e) => {

View File

@@ -3,8 +3,7 @@
* Browser-Compatible Implementation (Client-Side Rendering) * Browser-Compatible Implementation (Client-Side Rendering)
* *
* This module provides functionality for sending and receiving data across network boundaries * This module provides functionality for sending and receiving data across network boundaries
* using NATS as the message bus, with support for both direct payload transport and * with support for both direct payload transport and URL-based transport for larger payloads.
* URL-based transport for larger payloads.
* *
* Supported payload types: "text", "dictionary", "jsontable", "image", "audio", "video", "binary" * Supported payload types: "text", "dictionary", "jsontable", "image", "audio", "video", "binary"
* Note: Browser version does NOT support Apache Arrow IPC (arrowtable) due to browser compatibility constraints. * Note: Browser version does NOT support Apache Arrow IPC (arrowtable) due to browser compatibility constraints.
@@ -14,10 +13,8 @@
* - Modern browser with ES module support (or use module bundler) * - Modern browser with ES module support (or use module bundler)
* - Web Crypto API for UUID generation * - Web Crypto API for UUID generation
* - Fetch API for HTTP requests * - Fetch API for HTTP requests
* - WebSocket support for NATS connections (use ws:// or wss:// URLs)
* *
* Browser-compatible version uses: * Browser-compatible version uses:
* - nats.ws for WebSocket-based NATS connections
* - Web Crypto API for UUID generation * - Web Crypto API for UUID generation
* - Uint8Array instead of Buffer * - Uint8Array instead of Buffer
* - fetch API for file server communication * - fetch API for file server communication
@@ -25,9 +22,6 @@
* @module msghandlerCSR * @module msghandlerCSR
*/ */
// Import browser-compatible NATS client
import * as nats from 'nats.ws';
// Use native fetch available in browsers // Use native fetch available in browsers
// ---------------------------------------------- Constants ---------------------------------------------- // // ---------------------------------------------- Constants ---------------------------------------------- //
@@ -38,9 +32,9 @@ import * as nats from 'nats.ws';
const DEFAULT_SIZE_THRESHOLD = 500_000; const DEFAULT_SIZE_THRESHOLD = 500_000;
/** /**
* Default NATS server URL (WebSocket protocol) * Default broker URL
*/ */
const DEFAULT_BROKER_URL = 'ws://localhost:4222'; const DEFAULT_BROKER_URL = 'localhost:4222';
/** /**
* Default HTTP file server URL for link transport * Default HTTP file server URL for link transport
@@ -75,34 +69,6 @@ function base64ToBuffer(base64) {
return bytes; return bytes;
} }
/**
* Convert Uint8Array to Base64 string (Unicode-safe version)
* Uses TextEncoder/TextDecoder for proper Unicode handling
* @param {Uint8Array} data - Data to encode
* @returns {string} Base64 encoded string
*/
function bufferToBase64UnicodeSafe(data) {
const bytes = new Uint8Array(data);
// Use TextDecoder to properly handle the bytes as text
const binary = String.fromCharCode(...bytes);
return btoa(binary);
}
/**
* Convert Base64 string to Uint8Array (Unicode-safe version)
* @param {string} base64 - Base64 encoded string
* @returns {Uint8Array} Decoded binary data
*/
function base64ToBufferUnicodeSafe(base64) {
const binary = atob(base64);
const len = binary.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
/** /**
* Generate UUID v4 using Web Crypto API * Generate UUID v4 using Web Crypto API
* @returns {string} UUID string * @returns {string} UUID string
@@ -323,191 +289,11 @@ async function fetchWithBackoff(url, maxRetries, baseDelay, maxDelay, correlatio
throw new Error(`Failed to fetch data after ${maxRetries} attempts`); throw new Error(`Failed to fetch data after ${maxRetries} attempts`);
} }
// ---------------------------------------------- NATS Client ---------------------------------------------- //
/**
* NATS client wrapper for connection management
* Supports both single-use and persistent connection modes
*/
class NATSClient {
/**
* Create a new NATS client
* @param {string} url - NATS server URL (ws:// or wss://)
* @param {boolean} [keepAlive=false] - Keep connection open for multiple publishes
*/
constructor(url, keepAlive = false) {
this.url = url;
this.connection = null;
this.keepAlive = keepAlive;
}
/**
* Connect to NATS server
* @returns {Promise<NATS.Connection>}
*/
async connect() {
if (this.connection) {
return this.connection;
}
this.connection = await nats.connect({ servers: this.url });
return this.connection;
}
/**
* Publish message to NATS subject
* @param {string} subject - NATS subject to publish to
* @param {string} message - Message to publish
* @param {string} correlationId - Correlation ID for logging
*/
async publish(subject, message, correlationId) {
if (!this.connection) {
await this.connect();
}
await this.connection.publish(subject, message);
logTrace(correlationId, `Message published to ${subject}`);
}
/**
* Close the NATS connection
*/
async close() {
if (this.connection) {
this.connection.close();
this.connection = null;
}
}
/**
* Get the current connection (for external use)
* @returns {NATS.Connection|null}
*/
getConnection() {
return this.connection;
}
/**
* Check if connected
* @returns {boolean}
*/
isConnected() {
return this.connection !== null;
}
}
/**
* Connection pool for managing multiple NATS connections
* Useful for applications with multiple concurrent publishers
*/
class NATSConnectionPool {
/**
* Create a new connection pool
* @param {string} url - NATS server URL (ws:// or wss://)
* @param {number} [maxSize=10] - Maximum pool size
*/
constructor(url, maxSize = 10) {
this.url = url;
this.maxSize = maxSize;
this.connections = new Map();
this.idCounter = 0;
}
/**
* Get a connection from the pool (or create new)
* @returns {Promise<NATSClient>}
*/
async acquire() {
// Try to find an existing idle connection
for (const [id, client] of this.connections) {
if (client.isConnected()) {
return client;
}
}
// Create new connection if under limit
if (this.connections.size < this.maxSize) {
const id = `conn_${++this.idCounter}`;
const client = new NATSClient(this.url, true);
await client.connect();
this.connections.set(id, client);
return client;
}
// Pool exhausted - create new connection (caller should close when done)
const client = new NATSClient(this.url, false);
await client.connect();
return client;
}
/**
* Return a connection to the pool
* @param {NATSClient} client - Connection to return
*/
release(client) {
// Only return persistent connections
if (client.keepAlive && client.isConnected()) {
// Connection already in pool, do nothing
return;
}
// Non-persistent connection - close it
client.close();
}
/**
* Close all connections in the pool
*/
async closeAll() {
for (const [id, client] of this.connections) {
await client.close();
}
this.connections.clear();
}
}
// ---------------------------------------------- Core Functions ---------------------------------------------- // // ---------------------------------------------- Core Functions ---------------------------------------------- //
/**
* Publish message to NATS
* @param {string|NATSClient|NATS.Connection} brokerUrlOrClient - NATS URL, client, or connection
* @param {string} subject - NATS subject to publish to
* @param {string} message - JSON message to publish
* @param {string} correlationId - Correlation ID for tracing
* @param {boolean} [closeConnection=true] - Close connection after publish (set false for persistent connections)
*/
async function publishMessage(brokerUrlOrClient, subject, message, correlationId, closeConnection = true) {
let conn;
let shouldClose = false;
if (brokerUrlOrClient instanceof NATSClient) {
conn = brokerUrlOrClient;
} else if (brokerUrlOrClient && typeof brokerUrlOrClient.publish === 'function') {
// Create a wrapper for direct connection (duck-typing check for NATS connection)
conn = {
async publish(subj, msg) {
await brokerUrlOrClient.publish(subj, msg);
},
async close() {
await brokerUrlOrClient.close();
}
};
shouldClose = true;
} else {
// String URL - create new client
const client = new NATSClient(brokerUrlOrClient);
conn = client;
shouldClose = true;
}
await conn.publish(subject, message, correlationId);
// Only close if explicitly requested and it's a short-lived client
if (shouldClose && closeConnection && conn instanceof NATSClient) {
await conn.close();
}
}
/** /**
* Build message envelope from payloads and metadata * Build message envelope from payloads and metadata
* @param {string} subject - NATS subject * @param {string} subject - Subject/topic
* @param {Array} payloads - Array of payload objects * @param {Array} payloads - Array of payload objects
* @param {Object} options - Envelope metadata options * @param {Object} options - Envelope metadata options
* @returns {Object} Envelope object * @returns {Object} Envelope object
@@ -560,19 +346,22 @@ function buildPayload(dataname, payloadType, payloadBytes, transport, data) {
} }
/** /**
* Send data via NATS with automatic transport selection * Send data with automatic transport selection
* *
* This function intelligently routes data delivery based on payload size. * This function intelligently routes data delivery based on payload size.
* If the serialized payload is smaller than size_threshold, it encodes the data as Base64 * If the serialized payload is smaller than size_threshold, it encodes the data as Base64
* and publishes directly over NATS. Otherwise, it uploads the data to a fileserver * into a "direct" payload. Otherwise, it uploads the data to a fileserver
* and publishes only the download URL over NATS. * and creates a "link" payload with the URL.
* *
* @param {string} subject - NATS subject to publish the message to * Transport publishing is the caller's responsibility. This function returns the
* envelope and its JSON string representation.
*
* @param {string} subject - Subject/topic to send the message to
* @param {Array} data - List of [dataname, data, type] tuples to send * @param {Array} data - List of [dataname, data, type] tuples to send
* - type: "text", "dictionary", "jsontable", "image", "audio", "video", "binary" * - type: "text", "dictionary", "jsontable", "image", "audio", "video", "binary"
* - Note: "arrowtable" is NOT supported in browser (use "jsontable" for tabular data) * - Note: "arrowtable" is NOT supported in browser (use "jsontable" for tabular data)
* @param {Object} options - Optional configuration * @param {Object} options - Optional configuration
* @param {string} [options.broker_url=DEFAULT_BROKER_URL] - URL of the NATS server (WebSocket) * @param {string} [options.broker_url=DEFAULT_BROKER_URL] - Broker URL (for envelope metadata)
* @param {string} [options.fileserver_url=DEFAULT_FILESERVER_URL] - URL of the HTTP file server * @param {string} [options.fileserver_url=DEFAULT_FILESERVER_URL] - URL of the HTTP file server
* @param {Function} [options.fileserver_upload_handler=plikOneshotUpload] - Function to handle fileserver uploads * @param {Function} [options.fileserver_upload_handler=plikOneshotUpload] - Function to handle fileserver uploads
* @param {number} [options.size_threshold=DEFAULT_SIZE_THRESHOLD] - Threshold separating direct vs link transport * @param {number} [options.size_threshold=DEFAULT_SIZE_THRESHOLD] - Threshold separating direct vs link transport
@@ -583,31 +372,30 @@ function buildPayload(dataname, payloadType, payloadBytes, transport, data) {
* @param {string} [options.receiver_id=""] - UUID of the receiver (empty means broadcast) * @param {string} [options.receiver_id=""] - UUID of the receiver (empty means broadcast)
* @param {string} [options.reply_to=""] - Topic to reply to * @param {string} [options.reply_to=""] - Topic to reply to
* @param {string} [options.reply_to_msg_id=""] - Message ID this message is replying to * @param {string} [options.reply_to_msg_id=""] - Message ID this message is replying to
* @param {boolean} [options.is_publish=true] - Whether to automatically publish the message
* @param {NATSClient|NATS.Connection} [options.nats_connection=null] - Pre-existing NATS connection
* @param {string} [options.msg_id=uuidv4()] - Message ID * @param {string} [options.msg_id=uuidv4()] - Message ID
* @param {string} [options.sender_id=uuidv4()] - Sender ID * @param {string} [options.sender_id=uuidv4()] - Sender ID
* @returns {Promise<[Object, string]>} Tuple of [env, env_json_str] * @returns {Promise<[Object, string]>} Tuple of [env, env_json_str]
* *
* @example * @example
* // Send a single payload * // Send a single payload
* const [env, envJsonStr] = await msghandlerCSR.smartsend( * const [env, envJsonStr] = await msghandlerCSR.smartpack(
* "/test", * "/test",
* [["dataname1", data1, "dictionary"]], * [["dataname1", data1, "dictionary"]]
* { broker_url: "wss://nats.example.com" }
* ); * );
* *
* // Send multiple payloads (use jsontable instead of arrowtable for browser) * // Send multiple payloads (use jsontable instead of arrowtable for browser)
* const [env, envJsonStr] = await msghandlerCSR.smartsend( * const [env, envJsonStr] = await msghandlerCSR.smartpack(
* "/test", * "/test",
* [ * [
* ["dataname1", data1, "dictionary"], * ["dataname1", data1, "dictionary"],
* ["dataname2", tableData, "jsontable"] * ["dataname2", tableData, "jsontable"]
* ], * ]
* { broker_url: "wss://nats.example.com" }
* ); * );
*
* // Publish via your transport (NATS, MQTT, HTTP, etc.)
* // await myNatsClient.publish("/test", envJsonStr);
*/ */
async function smartsend(subject, data, options = {}) { async function smartpack(subject, data, options = {}) {
const { const {
broker_url = DEFAULT_BROKER_URL, broker_url = DEFAULT_BROKER_URL,
fileserver_url = DEFAULT_FILESERVER_URL, fileserver_url = DEFAULT_FILESERVER_URL,
@@ -620,26 +408,24 @@ async function smartsend(subject, data, options = {}) {
receiver_id = '', receiver_id = '',
reply_to = '', reply_to = '',
reply_to_msg_id = '', reply_to_msg_id = '',
is_publish = true,
nats_connection = null,
msg_id = uuidv4(), msg_id = uuidv4(),
sender_id = uuidv4() sender_id = uuidv4()
} = options; } = options;
logTrace(correlation_id, `Starting smartsend for subject: ${subject}`); logTrace(correlation_id, `Starting smartpack for subject: ${subject}`);
logTrace(correlation_id, `smartsend: data array length=${data.length}`); logTrace(correlation_id, `smartpack: data array length=${data.length}`);
// Debug: Log input data structure // Debug: Log input data structure
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
const [dataname, payloadData, payloadType] = data[i]; const [dataname, payloadData, payloadType] = data[i];
logTrace(correlation_id, `smartsend: payload[${i}] dataname=${dataname}, type=${payloadType}, data type=${typeof payloadData}, constructor=${payloadData?.constructor?.name}`); logTrace(correlation_id, `smartpack: payload[${i}] dataname=${dataname}, type=${payloadType}, data type=${typeof payloadData}, constructor=${payloadData?.constructor?.name}`);
} }
// Process payloads // Process payloads
const payloads = []; const payloads = [];
for (const [dataname, payloadData, payloadType] of data) { for (const [dataname, payloadData, payloadType] of data) {
logTrace(correlation_id, `smartsend: Processing payload '${dataname}' type=${payloadType}`); logTrace(correlation_id, `smartpack: Processing payload '${dataname}' type=${payloadType}`);
logTrace(correlation_id, `smartsend: payloadData type=${typeof payloadData}, constructor=${payloadData?.constructor?.name}`); logTrace(correlation_id, `smartpack: payloadData type=${typeof payloadData}, constructor=${payloadData?.constructor?.name}`);
const payloadBytes = await serializeData(payloadData, payloadType); const payloadBytes = await serializeData(payloadData, payloadType);
const payloadSize = payloadBytes.byteLength; const payloadSize = payloadBytes.byteLength;
@@ -695,25 +481,18 @@ async function smartsend(subject, data, options = {}) {
const env_json_str = JSON.stringify(env); const env_json_str = JSON.stringify(env);
if (is_publish) {
if (nats_connection) {
await publishMessage(nats_connection, subject, env_json_str, correlation_id);
} else {
await publishMessage(broker_url, subject, env_json_str, correlation_id);
}
}
return [env, env_json_str]; return [env, env_json_str];
} }
/** /**
* Receive and process NATS message * Receive and process messages
* *
* This function processes incoming NATS messages, handling both direct transport * This function processes incoming messages, handling both direct transport
* (base64 decoded payloads) and link transport (URL-based payloads). * (base64 decoded payloads) and link transport (URL-based payloads).
* It deserializes the data based on the transport type and returns the result. * It deserializes the data based on the transport type and returns the result.
* *
* @param {Object} msg - NATS message object with payload property * @param {string|Object} msg - Message payload. Accepts either a JSON string directly,
* or an object with a `data` or `payload` property containing the JSON string.
* @param {Object} options - Optional configuration * @param {Object} options - Optional configuration
* @param {Function} [options.fileserver_download_handler=fetchWithBackoff] - Function to handle fileserver downloads * @param {Function} [options.fileserver_download_handler=fetchWithBackoff] - Function to handle fileserver downloads
* @param {number} [options.max_retries=5] - Maximum retry attempts for fetching URL * @param {number} [options.max_retries=5] - Maximum retry attempts for fetching URL
@@ -722,19 +501,24 @@ async function smartsend(subject, data, options = {}) {
* @returns {Promise<Object>} Envelope object with processed payloads * @returns {Promise<Object>} Envelope object with processed payloads
* *
* @example * @example
* // Receive and process message * // Receive from JSON string directly
* const env = await msghandlerCSR.smartreceive(msg, { * const env = await msghandlerCSR.smartunpack(jsonString, {
* fileserver_download_handler: msghandlerCSR.fetchWithBackoff, * fileserver_download_handler: msghandlerCSR.fetchWithBackoff,
* max_retries: 5, * max_retries: 5,
* base_delay: 100, * base_delay: 100,
* max_delay: 5000 * max_delay: 5000
* }); * });
*
* // Receive from transport message object (e.g., NATS, MQTT)
* const env = await msghandlerCSR.smartunpack(natsMsg, {
* fileserver_download_handler: msghandlerCSR.fetchWithBackoff
* });
* // env.payloads is an Array of [dataname, data, type] arrays * // env.payloads is an Array of [dataname, data, type] arrays
* for (const [dataname, data, type] of env.payloads) { * for (const [dataname, data, type] of env.payloads) {
* console.log(`${dataname}: ${data} (type: ${type})`); * console.log(`${dataname}: ${data} (type: ${type})`);
* } * }
*/ */
async function smartreceive(msg, options = {}) { async function smartunpack(msg, options = {}) {
const { const {
fileserver_download_handler = fetchWithBackoff, fileserver_download_handler = fetchWithBackoff,
max_retries = 5, max_retries = 5,
@@ -742,44 +526,44 @@ async function smartreceive(msg, options = {}) {
max_delay = 5000 max_delay = 5000
} = options; } = options;
// Debug: Log message object structure // Handle both raw JSON strings and transport message objects
logTrace('smartreceive', `smartreceive: msg object keys: ${Object.keys(msg).join(', ')}`);
logTrace('smartreceive', `smartreceive: msg.data type: ${typeof msg.data}, constructor: ${msg.data?.constructor?.name}`);
logTrace('smartreceive', `smartreceive: msg.payload type: ${typeof msg.payload}, constructor: ${msg.payload?.constructor?.name}`);
// Parse the JSON envelope
// NATS.js v2.x uses msg.data instead of msg.payload
let payload; let payload;
if (msg.data !== undefined) { if (typeof msg === 'string') {
payload = typeof msg.data === 'string' ? msg.data : new TextDecoder().decode(msg.data); payload = msg;
} else if (msg.payload !== undefined) { } else if (msg !== null && typeof msg === 'object') {
payload = typeof msg.payload === 'string' ? msg.payload : new TextDecoder().decode(msg.payload); if (msg.data !== undefined) {
payload = typeof msg.data === 'string' ? msg.data : new TextDecoder().decode(msg.data);
} else if (msg.payload !== undefined) {
payload = typeof msg.payload === 'string' ? msg.payload : new TextDecoder().decode(msg.payload);
} else {
throw new Error('Message has neither data nor payload property');
}
} else { } else {
throw new Error('Message has neither data nor payload property'); throw new Error('Invalid message format: expected JSON string or message object');
} }
logTrace('smartreceive', `smartreceive: raw payload length=${payload.length}`); logTrace('smartunpack', `smartunpack: raw payload length=${payload.length}`);
// Debug: Show first 200 chars of payload // Debug: Show first 200 chars of payload
const payloadPreview = payload.substring(0, 200); const payloadPreview = payload.substring(0, 200);
logTrace('smartreceive', `smartreceive: payload preview: ${payloadPreview}`); logTrace('smartunpack', `smartunpack: payload preview: ${payloadPreview}`);
let envJsonObj; let envJsonObj;
try { try {
envJsonObj = JSON.parse(payload); envJsonObj = JSON.parse(payload);
} catch (e) { } catch (e) {
logTrace('smartreceive', `smartreceive: JSON parse failed: ${e.message}`); logTrace('smartunpack', `smartunpack: JSON parse failed: ${e.message}`);
throw e; throw e;
} }
logTrace(envJsonObj.correlation_id, 'Processing received message'); logTrace(envJsonObj.correlation_id, 'Processing received message');
logTrace(envJsonObj.correlation_id, `smartreceive: envelope has ${envJsonObj.payloads.length} payloads`); logTrace(envJsonObj.correlation_id, `smartunpack: envelope has ${envJsonObj.payloads.length} payloads`);
// Process all payloads in the envelope // Process all payloads in the envelope
const payloadsList = []; const payloadsList = [];
const numPayloads = envJsonObj.payloads.length; const numPayloads = envJsonObj.payloads.length;
logTrace(envJsonObj.correlation_id, `smartreceive: Processing ${numPayloads} payloads`); logTrace(envJsonObj.correlation_id, `smartunpack: Processing ${numPayloads} payloads`);
for (let i = 0; i < numPayloads; i++) { for (let i = 0; i < numPayloads; i++) {
const payloadObj = envJsonObj.payloads[i]; const payloadObj = envJsonObj.payloads[i];
@@ -787,7 +571,7 @@ async function smartreceive(msg, options = {}) {
const dataname = payloadObj.dataname; const dataname = payloadObj.dataname;
const payloadType = payloadObj.payload_type; const payloadType = payloadObj.payload_type;
logTrace(envJsonObj.correlation_id, `smartreceive: Processing payload ${i + 1}/${numPayloads}: dataname=${dataname}, type=${payloadType}, transport=${transport}`); logTrace(envJsonObj.correlation_id, `smartunpack: Processing payload ${i + 1}/${numPayloads}: dataname=${dataname}, type=${payloadType}, transport=${transport}`);
if (transport === 'direct') { if (transport === 'direct') {
logTrace(envJsonObj.correlation_id, `Direct transport - decoding payload '${dataname}'`); logTrace(envJsonObj.correlation_id, `Direct transport - decoding payload '${dataname}'`);
@@ -830,7 +614,7 @@ async function smartreceive(msg, options = {}) {
} }
} }
logTrace(envJsonObj.correlation_id, `smartreceive: Successfully processed all ${payloadsList.length} payloads`); logTrace(envJsonObj.correlation_id, `smartunpack: Successfully processed all ${payloadsList.length} payloads`);
envJsonObj.payloads = payloadsList; envJsonObj.payloads = payloadsList;
return envJsonObj; return envJsonObj;
} }
@@ -839,60 +623,14 @@ async function smartreceive(msg, options = {}) {
const msghandlerCSR = { const msghandlerCSR = {
/** /**
* NATS client class for connection management * Send data with automatic transport selection
* Supports both single-use and persistent connection modes
*
* @example
* // Single-use connection (closes after publish)
* const client = new msghandlerCSR.NATSClient("wss://nats.example.com");
* await msghandlerCSR.smartsend("/test", [["msg", "Hello", "text"]], { nats_connection: client });
* await client.close();
*
* // Persistent connection (keeps connection open)
* const client = new msghandlerCSR.NATSClient("wss://nats.example.com", true);
* await client.connect();
* await msghandlerCSR.smartsend("/test1", [["msg", "Hello", "text"]], { nats_connection: client, is_publish: false });
* await msghandlerCSR.publishMessage(client, "/test2", JSON.stringify({msg: "World"}), "trace-id");
* // Connection remains open for more publishes
* await client.close();
*/ */
NATSClient, smartpack,
/** /**
* Connection pool for managing multiple NATS connections * Receive and process messages
* Useful for applications with multiple concurrent publishers
*
* @example
* const pool = new msghandlerCSR.NATSConnectionPool("wss://nats.example.com", 10);
* const client = await pool.acquire();
* await msghandlerCSR.smartsend("/test", [["msg", "Hello", "text"]], { nats_connection: client });
* pool.release(client);
* await pool.closeAll();
*/ */
NATSConnectionPool, smartunpack,
/**
* Send data via NATS with automatic transport selection
*/
smartsend,
/**
* Receive and process NATS message
*/
smartreceive,
/**
* Publish message to NATS
*
* @example
* // Using a persistent connection
* const client = new msghandlerCSR.NATSClient("wss://nats.example.com", true);
* await client.connect();
* await msghandlerCSR.publishMessage(client, "/subject", JSON.stringify({msg: "Hello"}), "trace-id", false);
* // Connection stays open for more publishes
* await client.close();
*/
publishMessage,
/** /**
* Upload data to plik server in one-shot mode * Upload data to plik server in one-shot mode

View File

@@ -3,21 +3,18 @@
* JavaScript/Node.js Implementation (Desktop/Server-Side) * JavaScript/Node.js Implementation (Desktop/Server-Side)
* *
* This module provides functionality for sending and receiving data across network boundaries * This module provides functionality for sending and receiving data across network boundaries
* using NATS as the message bus, with support for both direct payload transport and * with support for both direct payload transport and URL-based transport for larger payloads.
* URL-based transport for larger payloads.
* *
* Supported payload types: "text", "dictionary", "arrowtable", "jsontable", "image", "audio", "video", "binary" * Supported payload types: "text", "dictionary", "arrowtable", "jsontable", "image", "audio", "video", "binary"
* *
* Node.js-specific features: * Node.js-specific features:
* - Apache Arrow IPC support via apache-arrow * - Apache Arrow IPC support via apache-arrow
* - TCP NATS connections (nats:// or tls:// URLs)
* - Buffer for binary data handling * - Buffer for binary data handling
* - Connection pooling for high-throughput scenarios * - Native fetch for HTTP operations
* *
* @module msghandler * @module msghandler
*/ */
const nats = require('nats');
const crypto = require('crypto'); const crypto = require('crypto');
// Use native fetch available in Node.js 18+ // Use native fetch available in Node.js 18+
const arrow = require('apache-arrow'); const arrow = require('apache-arrow');
@@ -40,9 +37,9 @@ function uuidv4() {
const DEFAULT_SIZE_THRESHOLD = 500_000; const DEFAULT_SIZE_THRESHOLD = 500_000;
/** /**
* Default NATS server URL * Default broker URL
*/ */
const DEFAULT_BROKER_URL = 'nats://localhost:4222'; const DEFAULT_BROKER_URL = 'localhost:4222';
/** /**
* Default HTTP file server URL for link transport * Default HTTP file server URL for link transport
@@ -344,191 +341,11 @@ async function fetchWithBackoff(url, maxRetries, baseDelay, maxDelay, correlatio
throw new Error(`Failed to fetch data after ${maxRetries} attempts`); throw new Error(`Failed to fetch data after ${maxRetries} attempts`);
} }
// ---------------------------------------------- NATS Client ---------------------------------------------- //
/**
* NATS client wrapper for connection management
* Supports both single-use and persistent connection modes
*/
class NATSClient {
/**
* Create a new NATS client
* @param {string} url - NATS server URL (nats:// or tls://)
* @param {boolean} [keepAlive=false] - Keep connection open for multiple publishes
*/
constructor(url, keepAlive = false) {
this.url = url;
this.connection = null;
this.keepAlive = keepAlive;
}
/**
* Connect to NATS server
* @returns {Promise<NATS.Connection>}
*/
async connect() {
if (this.connection) {
return this.connection;
}
this.connection = await nats.connect({ servers: this.url });
return this.connection;
}
/**
* Publish message to NATS subject
* @param {string} subject - NATS subject to publish to
* @param {string} message - Message to publish
* @param {string} correlationId - Correlation ID for logging
*/
async publish(subject, message, correlationId) {
if (!this.connection) {
await this.connect();
}
await this.connection.publish(subject, message);
logTrace(correlationId, `Message published to ${subject}`);
}
/**
* Close the NATS connection
*/
async close() {
if (this.connection) {
this.connection.close();
this.connection = null;
}
}
/**
* Get the current connection (for external use)
* @returns {NATS.Connection|null}
*/
getConnection() {
return this.connection;
}
/**
* Check if connected
* @returns {boolean}
*/
isConnected() {
return this.connection !== null;
}
}
/**
* Connection pool for managing multiple NATS connections
* Useful for applications with multiple concurrent publishers
*/
class NATSConnectionPool {
/**
* Create a new connection pool
* @param {string} url - NATS server URL (nats:// or tls://)
* @param {number} [maxSize=10] - Maximum pool size
*/
constructor(url, maxSize = 10) {
this.url = url;
this.maxSize = maxSize;
this.connections = new Map();
this.idCounter = 0;
}
/**
* Get a connection from the pool (or create new)
* @returns {Promise<NATSClient>}
*/
async acquire() {
// Try to find an existing idle connection
for (const [id, client] of this.connections) {
if (client.isConnected()) {
return client;
}
}
// Create new connection if under limit
if (this.connections.size < this.maxSize) {
const id = `conn_${++this.idCounter}`;
const client = new NATSClient(this.url, true);
await client.connect();
this.connections.set(id, client);
return client;
}
// Pool exhausted - create new connection (caller should close when done)
const client = new NATSClient(this.url, false);
await client.connect();
return client;
}
/**
* Return a connection to the pool
* @param {NATSClient} client - Connection to return
*/
release(client) {
// Only return persistent connections
if (client.keepAlive && client.isConnected()) {
// Connection already in pool, do nothing
return;
}
// Non-persistent connection - close it
client.close();
}
/**
* Close all connections in the pool
*/
async closeAll() {
for (const [id, client] of this.connections) {
await client.close();
}
this.connections.clear();
}
}
// ---------------------------------------------- Core Functions ---------------------------------------------- // // ---------------------------------------------- Core Functions ---------------------------------------------- //
/**
* Publish message to NATS
* @param {string|NATSClient|NATS.Connection} brokerUrlOrClient - NATS URL, client, or connection
* @param {string} subject - NATS subject to publish to
* @param {string} message - JSON message to publish
* @param {string} correlationId - Correlation ID for tracing
* @param {boolean} [closeConnection=true] - Close connection after publish (set false for persistent connections)
*/
async function publishMessage(brokerUrlOrClient, subject, message, correlationId, closeConnection = true) {
let conn;
let shouldClose = false;
if (brokerUrlOrClient instanceof NATSClient) {
conn = brokerUrlOrClient;
} else if (brokerUrlOrClient && typeof brokerUrlOrClient.publish === 'function') {
// Create a wrapper for direct connection (duck-typing check for NATS connection)
conn = {
async publish(subj, msg) {
await brokerUrlOrClient.publish(subj, msg);
},
async close() {
await brokerUrlOrClient.close();
}
};
shouldClose = true;
} else {
// String URL - create new client
const client = new NATSClient(brokerUrlOrClient);
conn = client;
shouldClose = true;
}
await conn.publish(subject, message, correlationId);
// Only close if explicitly requested and it's a short-lived client
if (shouldClose && closeConnection && conn instanceof NATSClient) {
await conn.close();
}
}
/** /**
* Build message envelope from payloads and metadata * Build message envelope from payloads and metadata
* @param {string} subject - NATS subject * @param {string} subject - Subject/topic
* @param {Array} payloads - Array of payload objects * @param {Array} payloads - Array of payload objects
* @param {Object} options - Envelope metadata options * @param {Object} options - Envelope metadata options
* @returns {Object} Envelope object * @returns {Object} Envelope object
@@ -583,18 +400,21 @@ function buildPayload(dataname, payloadType, payloadBytes, transport, data) {
} }
/** /**
* Send data via NATS with automatic transport selection * Send data with automatic transport selection
* *
* This function intelligently routes data delivery based on payload size. * This function intelligently routes data delivery based on payload size.
* If the serialized payload is smaller than size_threshold, it encodes the data as Base64 * If the serialized payload is smaller than size_threshold, it encodes the data as Base64
* and publishes directly over NATS. Otherwise, it uploads the data to a fileserver * into a "direct" payload. Otherwise, it uploads the data to a fileserver
* and publishes only the download URL over NATS. * and creates a "link" payload with the URL.
* *
* @param {string} subject - NATS subject to publish the message to * Transport publishing is the caller's responsibility. This function returns the
* envelope and its JSON string representation.
*
* @param {string} subject - Subject/topic to send the message to
* @param {Array} data - List of [dataname, data, type] tuples to send * @param {Array} data - List of [dataname, data, type] tuples to send
* - type: "text", "dictionary", "arrowtable", "jsontable", "image", "audio", "video", "binary" * - type: "text", "dictionary", "arrowtable", "jsontable", "image", "audio", "video", "binary"
* @param {Object} options - Optional configuration * @param {Object} options - Optional configuration
* @param {string} [options.broker_url=DEFAULT_BROKER_URL] - URL of the NATS server * @param {string} [options.broker_url=DEFAULT_BROKER_URL] - Broker URL (for envelope metadata)
* @param {string} [options.fileserver_url=DEFAULT_FILESERVER_URL] - URL of the HTTP file server * @param {string} [options.fileserver_url=DEFAULT_FILESERVER_URL] - URL of the HTTP file server
* @param {Function} [options.fileserver_upload_handler=plikOneshotUpload] - Function to handle fileserver uploads * @param {Function} [options.fileserver_upload_handler=plikOneshotUpload] - Function to handle fileserver uploads
* @param {number} [options.size_threshold=DEFAULT_SIZE_THRESHOLD] - Threshold separating direct vs link transport * @param {number} [options.size_threshold=DEFAULT_SIZE_THRESHOLD] - Threshold separating direct vs link transport
@@ -605,39 +425,30 @@ function buildPayload(dataname, payloadType, payloadBytes, transport, data) {
* @param {string} [options.receiver_id=""] - UUID of the receiver (empty means broadcast) * @param {string} [options.receiver_id=""] - UUID of the receiver (empty means broadcast)
* @param {string} [options.reply_to=""] - Topic to reply to * @param {string} [options.reply_to=""] - Topic to reply to
* @param {string} [options.reply_to_msg_id=""] - Message ID this message is replying to * @param {string} [options.reply_to_msg_id=""] - Message ID this message is replying to
* @param {boolean} [options.is_publish=true] - Whether to automatically publish the message
* @param {NATSClient|NATS.Connection} [options.nats_connection=null] - Pre-existing NATS connection
* @param {string} [options.msg_id=crypto.randomUUID()] - Message ID * @param {string} [options.msg_id=crypto.randomUUID()] - Message ID
* @param {string} [options.sender_id=crypto.randomUUID()] - Sender ID * @param {string} [options.sender_id=crypto.randomUUID()] - Sender ID
* @returns {Promise<[Object, string]>} Tuple of [env, env_json_str] * @returns {Promise<[Object, string]>} Tuple of [env, env_json_str]
* *
* @example * @example
* // Send a single payload * // Send a single payload
* const [env, envJsonStr] = await smartsend( * const [env, envJsonStr] = await smartpack(
* "/test", * "/test",
* [["dataname1", data1, "dictionary"]], * [["dataname1", data1, "dictionary"]]
* { broker_url: "nats://localhost:4222" }
* ); * );
* *
* // Send multiple payloads * // Send multiple payloads
* const [env, envJsonStr] = await smartsend( * const [env, envJsonStr] = await smartpack(
* "/test", * "/test",
* [ * [
* ["dataname1", data1, "dictionary"], * ["dataname1", data1, "dictionary"],
* ["dataname2", data2, "arrowtable"] * ["dataname2", data2, "arrowtable"]
* ], * ]
* { broker_url: "nats://localhost:4222" }
* ); * );
* *
* // Send with pre-existing connection * // Publish via your transport (NATS, MQTT, HTTP, etc.)
* const client = await msghandler.NATSClient.connect("nats://localhost:4222"); * // await myNatsClient.publish("/test", envJsonStr);
* const [env, envJsonStr] = await smartsend(
* "/test",
* [["data", myData, "text"]],
* { nats_connection: client }
* );
*/ */
async function smartsend(subject, data, options = {}) { async function smartpack(subject, data, options = {}) {
const { const {
broker_url = DEFAULT_BROKER_URL, broker_url = DEFAULT_BROKER_URL,
fileserver_url = DEFAULT_FILESERVER_URL, fileserver_url = DEFAULT_FILESERVER_URL,
@@ -650,26 +461,24 @@ async function smartsend(subject, data, options = {}) {
receiver_id = '', receiver_id = '',
reply_to = '', reply_to = '',
reply_to_msg_id = '', reply_to_msg_id = '',
is_publish = true,
nats_connection = null,
msg_id = uuidv4(), msg_id = uuidv4(),
sender_id = uuidv4() sender_id = uuidv4()
} = options; } = options;
logTrace(correlation_id, `Starting smartsend for subject: ${subject}`); logTrace(correlation_id, `Starting smartpack for subject: ${subject}`);
logTrace(correlation_id, `smartsend: data array length=${data.length}`); logTrace(correlation_id, `smartpack: data array length=${data.length}`);
// Debug: Log input data structure // Debug: Log input data structure
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
const [dataname, payloadData, payloadType] = data[i]; const [dataname, payloadData, payloadType] = data[i];
logTrace(correlation_id, `smartsend: payload[${i}] dataname=${dataname}, type=${payloadType}, data type=${typeof payloadData}, constructor=${payloadData?.constructor?.name}`); logTrace(correlation_id, `smartpack: payload[${i}] dataname=${dataname}, type=${payloadType}, data type=${typeof payloadData}, constructor=${payloadData?.constructor?.name}`);
} }
// Process payloads // Process payloads
const payloads = []; const payloads = [];
for (const [dataname, payloadData, payloadType] of data) { for (const [dataname, payloadData, payloadType] of data) {
logTrace(correlation_id, `smartsend: Processing payload '${dataname}' type=${payloadType}`); logTrace(correlation_id, `smartpack: Processing payload '${dataname}' type=${payloadType}`);
logTrace(correlation_id, `smartsend: payloadData type=${typeof payloadData}, constructor=${payloadData?.constructor?.name}`); logTrace(correlation_id, `smartpack: payloadData type=${typeof payloadData}, constructor=${payloadData?.constructor?.name}`);
const payloadBytes = await serializeData(payloadData, payloadType); const payloadBytes = await serializeData(payloadData, payloadType);
const payloadSize = payloadBytes.byteLength; const payloadSize = payloadBytes.byteLength;
@@ -722,25 +531,18 @@ async function smartsend(subject, data, options = {}) {
const env_json_str = JSON.stringify(env); const env_json_str = JSON.stringify(env);
if (is_publish) {
if (nats_connection) {
await publishMessage(nats_connection, subject, env_json_str, correlation_id);
} else {
await publishMessage(broker_url, subject, env_json_str, correlation_id);
}
}
return [env, env_json_str]; return [env, env_json_str];
} }
/** /**
* Receive and process NATS message * Receive and process messages
* *
* This function processes incoming NATS messages, handling both direct transport * This function processes incoming messages, handling both direct transport
* (base64 decoded payloads) and link transport (URL-based payloads). * (base64 decoded payloads) and link transport (URL-based payloads).
* It deserializes the data based on the transport type and returns the result. * It deserializes the data based on the transport type and returns the result.
* *
* @param {Object} msg - NATS message object with payload property * @param {string|Object} msg - Message payload. Accepts either a JSON string directly,
* or an object with a `data` or `payload` property containing the JSON string.
* @param {Object} options - Optional configuration * @param {Object} options - Optional configuration
* @param {Function} [options.fileserver_download_handler=fetchWithBackoff] - Function to handle fileserver downloads * @param {Function} [options.fileserver_download_handler=fetchWithBackoff] - Function to handle fileserver downloads
* @param {number} [options.max_retries=5] - Maximum retry attempts for fetching URL * @param {number} [options.max_retries=5] - Maximum retry attempts for fetching URL
@@ -749,19 +551,24 @@ async function smartsend(subject, data, options = {}) {
* @returns {Promise<Object>} Envelope object with processed payloads * @returns {Promise<Object>} Envelope object with processed payloads
* *
* @example * @example
* // Receive and process message * // Receive from JSON string directly
* const env = await smartreceive(msg, { * const env = await smartunpack(jsonString, {
* fileserver_download_handler: fetchWithBackoff, * fileserver_download_handler: fetchWithBackoff,
* max_retries: 5, * max_retries: 5,
* base_delay: 100, * base_delay: 100,
* max_delay: 5000 * max_delay: 5000
* }); * });
*
* // Receive from transport message object (e.g., NATS, MQTT)
* const env = await smartunpack(natsMsg, {
* fileserver_download_handler: fetchWithBackoff
* });
* // env.payloads is an Array of [dataname, data, type] arrays * // env.payloads is an Array of [dataname, data, type] arrays
* for (const [dataname, data, type] of env.payloads) { * for (const [dataname, data, type] of env.payloads) {
* console.log(`${dataname}: ${data} (type: ${type})`); * console.log(`${dataname}: ${data} (type: ${type})`);
* } * }
*/ */
async function smartreceive(msg, options = {}) { async function smartunpack(msg, options = {}) {
const { const {
fileserver_download_handler = fetchWithBackoff, fileserver_download_handler = fetchWithBackoff,
max_retries = 5, max_retries = 5,
@@ -769,44 +576,44 @@ async function smartreceive(msg, options = {}) {
max_delay = 5000 max_delay = 5000
} = options; } = options;
// Debug: Log message object structure // Handle both raw JSON strings and transport message objects
logTrace('smartreceive', `smartreceive: msg object keys: ${Object.keys(msg).join(', ')}`);
logTrace('smartreceive', `smartreceive: msg.data type: ${typeof msg.data}, constructor: ${msg.data?.constructor?.name}`);
logTrace('smartreceive', `smartreceive: msg.payload type: ${typeof msg.payload}, constructor: ${msg.payload?.constructor?.name}`);
// Parse the JSON envelope
// NATS.js v2.x uses msg.data instead of msg.payload
let payload; let payload;
if (msg.data !== undefined) { if (typeof msg === 'string') {
payload = typeof msg.data === 'string' ? msg.data : Buffer.from(msg.data).toString('utf8'); payload = msg;
} else if (msg.payload !== undefined) { } else if (msg !== null && typeof msg === 'object') {
payload = typeof msg.payload === 'string' ? msg.payload : Buffer.from(msg.payload).toString('utf8'); if (msg.data !== undefined) {
payload = typeof msg.data === 'string' ? msg.data : Buffer.from(msg.data).toString('utf8');
} else if (msg.payload !== undefined) {
payload = typeof msg.payload === 'string' ? msg.payload : Buffer.from(msg.payload).toString('utf8');
} else {
throw new Error('Message has neither data nor payload property');
}
} else { } else {
throw new Error('Message has neither data nor payload property'); throw new Error('Invalid message format: expected JSON string or message object');
} }
logTrace('smartreceive', `smartreceive: raw payload length=${payload.length}`); logTrace('smartunpack', `smartunpack: raw payload length=${payload.length}`);
// Debug: Show first 200 chars of payload // Debug: Show first 200 chars of payload
const payloadPreview = payload.substring(0, 200); const payloadPreview = payload.substring(0, 200);
logTrace('smartreceive', `smartreceive: payload preview: ${payloadPreview}`); logTrace('smartunpack', `smartunpack: payload preview: ${payloadPreview}`);
let envJsonObj; let envJsonObj;
try { try {
envJsonObj = JSON.parse(payload); envJsonObj = JSON.parse(payload);
} catch (e) { } catch (e) {
logTrace('smartreceive', `smartreceive: JSON parse failed: ${e.message}`); logTrace('smartunpack', `smartunpack: JSON parse failed: ${e.message}`);
throw e; throw e;
} }
logTrace(envJsonObj.correlation_id, 'Processing received message'); logTrace(envJsonObj.correlation_id, 'Processing received message');
logTrace(envJsonObj.correlation_id, `smartreceive: envelope has ${envJsonObj.payloads.length} payloads`); logTrace(envJsonObj.correlation_id, `smartunpack: envelope has ${envJsonObj.payloads.length} payloads`);
// Process all payloads in the envelope // Process all payloads in the envelope
const payloadsList = []; const payloadsList = [];
const numPayloads = envJsonObj.payloads.length; const numPayloads = envJsonObj.payloads.length;
logTrace(envJsonObj.correlation_id, `smartreceive: Processing ${numPayloads} payloads`); logTrace(envJsonObj.correlation_id, `smartunpack: Processing ${numPayloads} payloads`);
for (let i = 0; i < numPayloads; i++) { for (let i = 0; i < numPayloads; i++) {
const payloadObj = envJsonObj.payloads[i]; const payloadObj = envJsonObj.payloads[i];
@@ -814,7 +621,7 @@ async function smartreceive(msg, options = {}) {
const dataname = payloadObj.dataname; const dataname = payloadObj.dataname;
const payloadType = payloadObj.payload_type; const payloadType = payloadObj.payload_type;
logTrace(envJsonObj.correlation_id, `smartreceive: Processing payload ${i + 1}/${numPayloads}: dataname=${dataname}, type=${payloadType}, transport=${transport}`); logTrace(envJsonObj.correlation_id, `smartunpack: Processing payload ${i + 1}/${numPayloads}: dataname=${dataname}, type=${payloadType}, transport=${transport}`);
if (transport === 'direct') { if (transport === 'direct') {
logTrace(envJsonObj.correlation_id, `Direct transport - decoding payload '${dataname}'`); logTrace(envJsonObj.correlation_id, `Direct transport - decoding payload '${dataname}'`);
@@ -857,7 +664,7 @@ async function smartreceive(msg, options = {}) {
} }
} }
logTrace(envJsonObj.correlation_id, `smartreceive: Successfully processed all ${payloadsList.length} payloads`); logTrace(envJsonObj.correlation_id, `smartunpack: Successfully processed all ${payloadsList.length} payloads`);
envJsonObj.payloads = payloadsList; envJsonObj.payloads = payloadsList;
return envJsonObj; return envJsonObj;
} }
@@ -866,60 +673,14 @@ async function smartreceive(msg, options = {}) {
const msghandler = { const msghandler = {
/** /**
* NATS client class for connection management * Send data with automatic transport selection
* Supports both single-use and persistent connection modes
*
* @example
* // Single-use connection (closes after publish)
* const client = new msghandler.NATSClient("nats://localhost:4222");
* await msghandler.smartsend("/test", [["msg", "Hello", "text"]], { nats_connection: client });
* await client.close();
*
* // Persistent connection (keeps connection open)
* const client = new msghandler.NATSClient("nats://localhost:4222", true);
* await client.connect();
* await msghandler.smartsend("/test1", [["msg", "Hello", "text"]], { nats_connection: client, is_publish: false });
* await msghandler.publishMessage(client, "/test2", JSON.stringify({msg: "World"}), "trace-id");
* // Connection remains open for more publishes
* await client.close();
*/ */
NATSClient, smartpack,
/** /**
* Connection pool for managing multiple NATS connections * Receive and process messages
* Useful for applications with multiple concurrent publishers
*
* @example
* const pool = new msghandler.NATSConnectionPool("nats://localhost:4222", 10);
* const client = await pool.acquire();
* await msghandler.smartsend("/test", [["msg", "Hello", "text"]], { nats_connection: client });
* pool.release(client);
* await pool.closeAll();
*/ */
NATSConnectionPool, smartunpack,
/**
* Send data via NATS with automatic transport selection
*/
smartsend,
/**
* Receive and process NATS message
*/
smartreceive,
/**
* Publish message to NATS
*
* @example
* // Using a persistent connection
* const client = new msghandler.NATSClient("nats://localhost:4222", true);
* await client.connect();
* await msghandler.publishMessage(client, "/subject", JSON.stringify({msg: "Hello"}), "trace-id", false);
* // Connection stays open for more publishes
* await client.close();
*/
publishMessage,
/** /**
* Upload data to plik server in one-shot mode * Upload data to plik server in one-shot mode
@@ -939,4 +700,4 @@ const msghandler = {
DEFAULT_FILESERVER_URL DEFAULT_FILESERVER_URL
}; };
module.exports = msghandler; module.exports = msghandler;

View File

@@ -1,7 +1,7 @@
# Bi-Directional Data Bridge - Julia Module # Bi-Directional Data Bridge - Julia Module
# Implements smartsend and smartreceive for NATS communication # Implements smartpack and smartunpack for message transport
# This module provides functionality for sending and receiving data across network boundaries # This module provides functionality for sending and receiving data across network boundaries
# using NATS as the message bus, with support for both direct payload transport and # with support for both direct payload transport and
# URL-based transport for larger payloads. # URL-based transport for larger payloads.
# #
# File Server Handler Architecture: # File Server Handler Architecture:
@@ -24,10 +24,10 @@
# #
# API Standard: # API Standard:
# ```jldoctest # ```jldoctest
# # Input format for smartsend (always a list of tuples with type info) # # Input format for smartpack (always a list of tuples with type info)
# [(dataname1, data1, type1), (dataname2, data2, type2), ...] # [(dataname1, data1, type1), (dataname2, data2, type2), ...]
# #
# # Output format for smartreceive (always returns a list of tuples) # # Output format for smartunpack (always returns a list of tuples)
# [(dataname1, data1, type1), (dataname2, data2, type2), ...] # [(dataname1, data1, type1), (dataname2, data2, type2), ...]
# ``` # ```
# #
@@ -48,12 +48,12 @@ using JSON, Arrow, HTTP, UUIDs, Dates, Base64, PrettyPrinting, DataFrames
# Constants # Constants
const DEFAULT_SIZE_THRESHOLD = 500_000 # 0.5MB - threshold for switching from direct to link transport const DEFAULT_SIZE_THRESHOLD = 500_000 # 0.5MB - threshold for switching from direct to link transport
const DEFAULT_BROKER_URL = "nats://localhost:4222" # Default NATS server URL const DEFAULT_BROKER_URL = "localhost:4222" # Default broker URL
const DEFAULT_FILESERVER_URL = "http://localhost:8080" # Default HTTP file server URL for link transport const DEFAULT_FILESERVER_URL = "http://localhost:8080" # Default HTTP file server URL for link transport
""" msg_payload_v1 - Internal message payload structure """ msg_payload_v1 - Internal message payload structure
This structure represents a single payload within a NATS message envelope. This structure represents a single payload within a message envelope.
It supports both direct transport (base64-encoded data) and link transport (URL-based). It supports both direct transport (base64-encoded data) and link transport (URL-based).
# Arguments: # Arguments:
@@ -141,11 +141,11 @@ end
""" msg_envelope_v1 - Internal message envelope structure """ msg_envelope_v1 - Internal message envelope structure
This structure represents a complete NATS message envelope containing multiple payloads This structure represents a complete message envelope containing multiple payloads
with metadata for routing, tracing, and message context. with metadata for routing, tracing, and message context.
# Arguments: # Arguments:
- `send_to::String` - NATS subject/topic to publish the message to (e.g., "/agent/wine/api/v1/prompt") - `send_to::String` - Subject/topic to send the message to (e.g., "/agent/wine/api/v1/prompt")
- `payloads::Vector{msg_payload_v1}` - List of payloads to include in the message - `payloads::Vector{msg_payload_v1}` - List of payloads to include in the message
# Keyword Arguments: # Keyword Arguments:
@@ -159,7 +159,7 @@ with metadata for routing, tracing, and message context.
- `receiver_id::String = ""` - UUID of the receiver (empty string means broadcast) - `receiver_id::String = ""` - UUID of the receiver (empty string means broadcast)
- `reply_to::String = ""` - Topic where receiver should reply (empty string if no reply expected) - `reply_to::String = ""` - Topic where receiver should reply (empty string if no reply expected)
- `reply_to_msg_id::String = ""` - Message ID this message is replying to - `reply_to_msg_id::String = ""` - Message ID this message is replying to
- `broker_url::String = DEFAULT_BROKER_URL` - NATS broker URL - `broker_url::String = DEFAULT_BROKER_URL` - Broker URL
- `metadata::Dict{String, Any} = Dict{String, Any}()` - Optional message-level metadata - `metadata::Dict{String, Any} = Dict{String, Any}()` - Optional message-level metadata
# Return: # Return:
@@ -199,7 +199,7 @@ struct msg_envelope_v1
reply_to::String # sender ask receiver to reply to this topic reply_to::String # sender ask receiver to reply to this topic
reply_to_msg_id::String # the message id this message is replying to reply_to_msg_id::String # the message id this message is replying to
broker_url::String # NATS server address broker_url::String # Broker address
metadata::Dict{String, Any} metadata::Dict{String, Any}
payloads::Vector{msg_payload_v1} # multiple payload store here payloads::Vector{msg_payload_v1} # multiple payload store here
@@ -244,7 +244,7 @@ end
""" envelope_to_json - Convert msg_envelope_v1 to JSON string """ envelope_to_json - Convert msg_envelope_v1 to JSON string
This function converts the msg_envelope_v1 struct to a JSON string representation, This function converts the msg_envelope_v1 struct to a JSON string representation,
preserving all metadata and payload information for NATS message publishing. preserving all metadata and payload information for transport publishing.
# Function Workflow: # Function Workflow:
1. Creates a dictionary with envelope metadata (correlation_id, msg_id, timestamp, etc.) 1. Creates a dictionary with envelope metadata (correlation_id, msg_id, timestamp, etc.)
@@ -337,7 +337,7 @@ function log_trace(correlation_id::String, message::String)
end end
""" smartsend - Send data either directly via NATS or via a fileserver URL, depending on payload size """ smartpack - Send data with automatic transport selection, depending on payload size
This function intelligently routes data delivery based on payload size relative to a threshold. This function intelligently routes data delivery based on payload size relative to a threshold.
If the serialized payload is smaller than `size_threshold`, it encodes the data as Base64 and constructs a "direct" msg_payload_v1. If the serialized payload is smaller than `size_threshold`, it encodes the data as Base64 and constructs a "direct" msg_payload_v1.
@@ -347,7 +347,7 @@ The function accepts a list of (dataname, data, type) tuples as input and proces
Each payload can have a different type, enabling mixed-content messages (e.g., chat with text, images, audio). Each payload can have a different type, enabling mixed-content messages (e.g., chat with text, images, audio).
This function creates and returns the msg_envelope_v1 and its JSON string representation only. This function creates and returns the msg_envelope_v1 and its JSON string representation only.
NATS publishing must be performed by the caller. Transport publishing must be performed by the caller.
# Function Workflow: # Function Workflow:
1. Iterates through the list of (dataname, data, type) tuples 1. Iterates through the list of (dataname, data, type) tuples
@@ -356,10 +356,10 @@ NATS publishing must be performed by the caller.
4. For small payloads: encodes as Base64, constructs a "direct" msg_payload_v1 4. For small payloads: encodes as Base64, constructs a "direct" msg_payload_v1
5. For large payloads: uploads to the fileserver, constructs a "link" msg_payload_v1 with the URL 5. For large payloads: uploads to the fileserver, constructs a "link" msg_payload_v1 with the URL
6. Constructs msg_envelope_v1 with all payloads and metadata 6. Constructs msg_envelope_v1 with all payloads and metadata
7. Converts envelope to JSON string and returns (NATS publishing is handled by the caller) 7. Converts envelope to JSON string and returns (transport publishing is handled by the caller)
# Arguments: # Arguments:
- `subject::String` - NATS subject to publish the message to - `subject::String` - Subject/topic to send the message to
- `data::AbstractArray{Tuple{String, T1, String}, 1}` - List of (dataname, data, type) tuples to send - `data::AbstractArray{Tuple{String, T1, String}, 1}` - List of (dataname, data, type) tuples to send
- `dataname::String` - Name of the payload - `dataname::String` - Name of the payload
- `data::T1` - The actual data to send (any type supported by `_serialize_data`) - `data::T1` - The actual data to send (any type supported by `_serialize_data`)
@@ -367,7 +367,7 @@ NATS publishing must be performed by the caller.
- No standalone `type` parameter - type is specified per payload - No standalone `type` parameter - type is specified per payload
# Keyword Arguments: # Keyword Arguments:
- `broker_url::String = DEFAULT_BROKER_URL` - URL of the NATS server - `broker_url::String = DEFAULT_BROKER_URL` - URL of the broker
- `fileserver_url = DEFAULT_FILESERVER_URL` - URL of the HTTP file server for large payloads - `fileserver_url = DEFAULT_FILESERVER_URL` - URL of the HTTP file server for large payloads
- `fileserver_upload_handler::Function = plik_oneshot_upload` - Function to handle fileserver uploads (must return Dict with "status", "uploadid", "fileid", "url" keys) - `fileserver_upload_handler::Function = plik_oneshot_upload` - Function to handle fileserver uploads (must return Dict with "status", "uploadid", "fileid", "url" keys)
- `size_threshold::Int = DEFAULT_SIZE_THRESHOLD` - Threshold in bytes separating direct vs link transport - `size_threshold::Int = DEFAULT_SIZE_THRESHOLD` - Threshold in bytes separating direct vs link transport
@@ -392,37 +392,37 @@ using UUIDs
# Send a single payload (still wrapped in a list) # Send a single payload (still wrapped in a list)
data = Dict("key" => "value") data = Dict("key" => "value")
env, msg_json = smartsend("my.subject", [("dataname1", data, "dictionary")]) env, msg_json = smartpack("my.subject", [("dataname1", data, "dictionary")])
# Send multiple payloads in one message with different types # Send multiple payloads in one message with different types
data1 = Dict("key1" => "value1") data1 = Dict("key1" => "value1")
data2 = rand(10_000) # Small array data2 = rand(10_000) # Small array
env, msg_json = smartsend("my.subject", [("dataname1", data1, "dictionary"), ("dataname2", data2, "arrowtable")]) env, msg_json = smartpack("my.subject", [("dataname1", data1, "dictionary"), ("dataname2", data2, "arrowtable")])
# Send a large array using fileserver upload # Send a large array using fileserver upload
data = rand(10_000_000) # ~80 MB data = rand(10_000_000) # ~80 MB
env, msg_json = smartsend("large.data", [("large_arrow_table", data, "arrowtable")]) env, msg_json = smartpack("large.data", [("large_arrow_table", data, "arrowtable")])
# Send jsontable (JSON format) # Send jsontable (JSON format)
rows = [Dict("id" => 1, "name" => "Alice"), Dict("id" => 2, "name" => "Bob")] rows = [Dict("id" => 1, "name" => "Alice"), Dict("id" => 2, "name" => "Bob")]
env, msg_json = smartsend("json.data", [("users", rows, "jsontable")]) env, msg_json = smartpack("json.data", [("users", rows, "jsontable")])
# Mixed content (e.g., chat with text and image) # Mixed content (e.g., chat with text and image)
env, msg_json = smartsend("chat.subject", [ env, msg_json = smartpack("chat.subject", [
("message_text", "Hello!", "text"), ("message_text", "Hello!", "text"),
("user_image", image_data, "image"), ("user_image", image_data, "image"),
("audio_clip", audio_data, "audio") ("audio_clip", audio_data, "audio")
]) ])
# Publish the JSON string directly using NATS (manual publish) # Publish the JSON string directly via your transport (manual publish)
# conn = NATS.connect(broker_url) # conn = my_transport.connect(broker_url)
# NATS.publish(conn, subject, env_json_str) # my_transport.publish(conn, subject, env_json_str)
``` ```
""" """
function smartsend( function smartpack(
subject::String, # smartreceive's subject subject::String, # smartunpack's subject
data::AbstractArray{Tuple{String, T1, String}, 1}; # List of (dataname, data, type) tuples. Use Tuple{String, Any, String}[] for empty payloads data::AbstractArray{Tuple{String, T1, String}, 1}; # List of (dataname, data, type) tuples. Use data = Tuple{String, Any, String}[] for empty payloads
broker_url::String = DEFAULT_BROKER_URL, # NATS server URL broker_url::String = DEFAULT_BROKER_URL, # Broker URL
fileserver_url = DEFAULT_FILESERVER_URL, fileserver_url = DEFAULT_FILESERVER_URL,
fileserver_upload_handler::Function = plik_oneshot_upload, # a function to handle uploading data to specific HTTP fileserver fileserver_upload_handler::Function = plik_oneshot_upload, # a function to handle uploading data to specific HTTP fileserver
size_threshold::Int = DEFAULT_SIZE_THRESHOLD, size_threshold::Int = DEFAULT_SIZE_THRESHOLD,
@@ -446,7 +446,7 @@ function smartsend(
)::Tuple{msg_envelope_v1, String} where {T1<:Any} )::Tuple{msg_envelope_v1, String} where {T1<:Any}
# Log start of send operation # Log start of send operation
log_trace(correlation_id, "Starting smartsend for subject: $subject") log_trace(correlation_id, "Starting smartpack for subject: $subject")
# Process each payload in the list # Process each payload in the list
payloads = msg_payload_v1[] payloads = msg_payload_v1[]
@@ -461,7 +461,7 @@ function smartsend(
# Decision: Direct vs Link # Decision: Direct vs Link
if payload_size < size_threshold # Check if payload is small enough for direct transport if payload_size < size_threshold # Check if payload is small enough for direct transport
# Direct path - Base64 encode and send via NATS # Direct path - Base64 encode and include in message envelope
payload_b64 = Base64.base64encode(payload_bytes) # Encode bytes as base64 string payload_b64 = Base64.base64encode(payload_bytes) # Encode bytes as base64 string
log_trace(correlation_id, "Using direct transport for $payload_size bytes") # Log transport choice log_trace(correlation_id, "Using direct transport for $payload_size bytes") # Log transport choice
@@ -486,7 +486,7 @@ function smartsend(
) )
push!(payloads, payload) push!(payloads, payload)
else else
# Link path - Upload to HTTP server, send URL via NATS # Link path - Upload to HTTP server, include URL in message envelope
log_trace(correlation_id, "Using link transport, uploading to fileserver") # Log link transport choice log_trace(correlation_id, "Using link transport, uploading to fileserver") # Log link transport choice
# Upload to HTTP server # Upload to HTTP server
@@ -703,13 +703,13 @@ function _serialize_data(data::Any, payload_type::String)
end end
# """ publish_message - Publish message to NATS # """ publish_message - Publish message via transport
# This function publishes a message to a NATS subject with proper # This function publishes a message via the transport with proper
# connection management and logging. # connection management and logging.
# # Arguments: # # Arguments:
# - `broker_url::String` - NATS server URL (e.g., "nats://localhost:4222") # - `broker_url::String` - Broker URL (e.g., "localhost:4222")
# - `subject::String` - NATS subject to publish to (e.g., "/agent/wine/api/v1/prompt") # - `subject::String` - Subject to publish to (e.g., "/agent/wine/api/v1/prompt")
# - `message::String` - JSON message to publish # - `message::String` - JSON message to publish
# - `correlation_id::String` - Correlation ID for tracing and logging # - `correlation_id::String` - Correlation ID for tracing and logging
@@ -723,8 +723,8 @@ end
# # Prepare JSON message # # Prepare JSON message
# message = "{\"correlation_id\":\"abc123\",\"payload\":\"test\"}" # message = "{\"correlation_id\":\"abc123\",\"payload\":\"test\"}"
# # Publish to NATS # # Publish via transport
# publish_message("nats://localhost:4222", "my.subject", message, "abc123") # publish_message("localhost:4222", "my.subject", message, "abc123")
# ``` # ```
# """ # """
# function publish_message(broker_url::String, subject::String, message::String, correlation_id::String) # function publish_message(broker_url::String, subject::String, message::String, correlation_id::String)
@@ -732,13 +732,13 @@ end
# publish_message(conn, subject, message, correlation_id) # publish_message(conn, subject, message, correlation_id)
# end # end
# """ publish_message - Publish message to NATS using pre-existing connection # """ publish_message - Publish message via transport using pre-existing connection
# This function publishes a message to a NATS subject using a pre-existing NATS connection, # This function publishes a message via the transport using a pre-existing connection,
# avoiding the overhead of connection establishment. # avoiding the overhead of connection establishment.
# # Arguments: # # Arguments:
# - `conn::NATS.Connection` - Pre-existing NATS connection # - `conn` - Pre-existing connection object with publish/close methods
# - `subject::String` - NATS subject to publish to (e.g., "/agent/wine/api/v1/prompt") # - `subject::String` - Subject to publish to (e.g., "/agent/wine/api/v1/prompt")
# - `message::String` - JSON message to publish # - `message::String` - JSON message to publish
# - `correlation_id::String` - Correlation ID for tracing and logging # - `correlation_id::String` - Correlation ID for tracing and logging
@@ -759,7 +759,7 @@ end
# ``` # ```
# # Use Case: # # Use Case:
# Use this version when you already have an established NATS connection and want to publish # Use this version when you already have an established connection and want to publish
# multiple messages without the overhead of creating a new connection for each publish. # multiple messages without the overhead of creating a new connection for each publish.
# """ # """
# function publish_message(conn::NATS.Connection, subject::String, message::String, correlation_id::String) # function publish_message(conn::NATS.Connection, subject::String, message::String, correlation_id::String)
@@ -772,21 +772,21 @@ end
# end # end
""" smartreceive - Receive and process messages from NATS """ smartunpack - Receive and process messages
This function processes incoming NATS messages, handling both direct transport This function processes incoming messages, handling both direct transport
(base64 decoded payloads) and link transport (URL-based payloads). (base64 decoded payloads) and link transport (URL-based payloads).
It deserializes the data based on the transport type and returns the result. It deserializes the data based on the transport type and returns the result.
A HTTP file server is required along with its download function. A HTTP file server is required along with its download function.
# Function Workflow: # Function Workflow:
1. Parses the JSON envelope from the NATS message 1. Parses the JSON envelope from the message
2. Iterates through each payload in the envelope 2. Iterates through each payload in the envelope
3. For each payload: determines the transport type (direct or link) 3. For each payload: determines the transport type (direct or link)
4. For direct transport: decodes Base64 payload and deserializes based on type 4. For direct transport: decodes Base64 payload and deserializes based on type
5. For link transport: fetches data from URL with exponential backoff, then deserializes 5. For link transport: fetches data from URL with exponential backoff, then deserializes
# Arguments: # Arguments:
- `msg_json_str::String` - JSON string from NATS message payload (e.g., `String(nats_msg.payload)`) - `msg_json_str::String` - JSON string from the message payload (e.g., `String(msg.payload)`)
# Keyword Arguments: # Keyword Arguments:
- `fileserver_download_handler::Function = _fetch_with_backoff` - Function to handle downloading data from file server URLs - `fileserver_download_handler::Function = _fetch_with_backoff` - Function to handle downloading data from file server URLs
@@ -800,13 +800,12 @@ A HTTP file server is required along with its download function.
# Example # Example
```jldoctest ```jldoctest
# Receive and process message # Receive and process message
msg = nats_message # NATS message
msg_json_str = String(msg.payload) msg_json_str = String(msg.payload)
env = smartreceive(msg_json_str; fileserver_download_handler=_fetch_with_backoff, max_retries=5, base_delay=100, max_delay=5000) env = smartunpack(msg_json_str; fileserver_download_handler=_fetch_with_backoff, max_retries=5, base_delay=100, max_delay=5000)
# env["payloads"] = [("dataname1", data1, "type1"), ("dataname2", data2, "type2"), ...] # env["payloads"] = [("dataname1", data1, "type1"), ("dataname2", data2, "type2"), ...]
``` ```
""" """
function smartreceive( function smartunpack(
msg_json_str::String; # get it from String(nats_msg.payload) msg_json_str::String; # get it from String(nats_msg.payload)
fileserver_download_handler::Function = _fetch_with_backoff, fileserver_download_handler::Function = _fetch_with_backoff,
max_retries::Int = 5, max_retries::Int = 5,

View File

@@ -3,8 +3,7 @@ msghandler - Cross-Platform Bi-Directional Data Bridge
Python Desktop Implementation Python Desktop Implementation
This module provides functionality for sending and receiving data across network boundaries This module provides functionality for sending and receiving data across network boundaries
using NATS as the message bus, with support for both direct payload transport and with support for both direct payload transport and URL-based transport for larger payloads.
URL-based transport for larger payloads.
@package msghandler @package msghandler
""" """
@@ -24,13 +23,6 @@ try:
except ImportError: except ImportError:
ARROW_AVAILABLE = False ARROW_AVAILABLE = False
try:
import nats
from nats.aio.client import Client as NATSClient
NATS_AVAILABLE = True
except ImportError:
NATS_AVAILABLE = False
# ---------------------------------------------- Constants ---------------------------------------------- # # ---------------------------------------------- Constants ---------------------------------------------- #
""" """
@@ -39,9 +31,9 @@ Default size threshold for switching from direct to link transport (0.5MB)
DEFAULT_SIZE_THRESHOLD = 500_000 DEFAULT_SIZE_THRESHOLD = 500_000
""" """
Default NATS server URL Default broker URL
""" """
DEFAULT_BROKER_URL = "nats://localhost:4222" DEFAULT_BROKER_URL = "localhost:4222"
""" """
Default HTTP file server URL for link transport Default HTTP file server URL for link transport
@@ -305,56 +297,6 @@ async def fetch_with_backoff(
raise Exception(f"Failed to fetch data after {max_retries} attempts") raise Exception(f"Failed to fetch data after {max_retries} attempts")
# ---------------------------------------------- NATS Client ---------------------------------------------- #
class NATSClient:
"""NATS client wrapper for connection management."""
def __init__(self, url: str = DEFAULT_BROKER_URL):
"""
Create a new NATS client.
Args:
url: NATS server URL
"""
self.url = url
self._client: NATSClient = None
async def connect(self) -> NATSClient:
"""
Connect to NATS server.
Returns:
NATS client instance
"""
if NATS_AVAILABLE:
self._client = nats.connect(self.url)
await self._client
else:
raise RuntimeError('nats-py not available')
return self._client
async def publish(self, subject: str, message: str, correlation_id: str = "") -> None:
"""
Publish message to NATS subject.
Args:
subject: NATS subject to publish to
message: Message to publish
correlation_id: Correlation ID for logging
"""
if self._client:
await self._client.publish(subject, message)
if correlation_id:
log_trace(correlation_id, f"Message published to {subject}")
async def close(self) -> None:
"""Close the NATS connection."""
if self._client:
await self._client.drain()
await self._client.close()
# ---------------------------------------------- Core Functions ---------------------------------------------- # # ---------------------------------------------- Core Functions ---------------------------------------------- #
def _build_envelope( def _build_envelope(
@@ -366,7 +308,7 @@ def _build_envelope(
Build message envelope from payloads and metadata. Build message envelope from payloads and metadata.
Args: Args:
subject: NATS subject subject: Subject/topic
payloads: Array of payload objects payloads: Array of payload objects
options: Envelope metadata options options: Envelope metadata options
@@ -430,42 +372,7 @@ def _build_payload(
} }
async def publish_message( async def smartpack(
broker_url_or_client: Union[str, NATSClient, Any],
subject: str,
message: str,
correlation_id: str
) -> None:
"""
Publish message to NATS.
Args:
broker_url_or_client: NATS URL, client, or connection
subject: NATS subject to publish to
message: JSON message to publish
correlation_id: Correlation ID for tracing
"""
if isinstance(broker_url_or_client, NATSClient):
client = broker_url_or_client
elif NATS_AVAILABLE and hasattr(broker_url_or_client, 'publish'):
# Direct NATS client connection
await broker_url_or_client.publish(subject, message)
log_trace(correlation_id, f"Message published to {subject}")
return
else:
# String URL - create new client
client = NATSClient(broker_url_or_client)
await client.connect()
await client.publish(subject, message, correlation_id)
if isinstance(broker_url_or_client, NATSClient):
await broker_url_or_client.close()
elif not (NATS_AVAILABLE and hasattr(broker_url_or_client, 'publish')):
await client.close()
async def smartsend(
subject: str, subject: str,
data: List[Tuple[str, Any, str]], data: List[Tuple[str, Any, str]],
broker_url: str = DEFAULT_BROKER_URL, broker_url: str = DEFAULT_BROKER_URL,
@@ -479,26 +386,27 @@ async def smartsend(
receiver_id: str = "", receiver_id: str = "",
reply_to: str = "", reply_to: str = "",
reply_to_msg_id: str = "", reply_to_msg_id: str = "",
is_publish: bool = True,
nats_connection: Any = None,
msg_id: str = None, msg_id: str = None,
sender_id: str = None sender_id: str = None
) -> Tuple[Dict, str]: ) -> Tuple[Dict, str]:
""" """
Send data via NATS with automatic transport selection. Send data with automatic transport selection.
This function intelligently routes data delivery based on payload size. This function intelligently routes data delivery based on payload size.
If the serialized payload is smaller than size_threshold, it encodes the data as Base64 If the serialized payload is smaller than size_threshold, it encodes the data as Base64
and publishes directly over NATS. Otherwise, it uploads the data to a fileserver into a "direct" payload. Otherwise, it uploads the data to a fileserver
and publishes only the download URL over NATS. and creates a "link" payload with the URL.
Transport publishing is the caller's responsibility. This function returns the
envelope and its JSON string representation.
Args: Args:
subject: NATS subject to publish the message to subject: Subject/topic to send the message to
data: List of (dataname, data, type) tuples to send data: List of (dataname, data, type) tuples to send
- dataname: Name of the payload - dataname: Name of the payload
- data: The actual data to send - data: The actual data to send
- type: Payload type: "text", "dictionary", "arrowtable", "jsontable", "image", "audio", "video", "binary" - type: Payload type: "text", "dictionary", "arrowtable", "jsontable", "image", "audio", "video", "binary"
broker_url: URL of the NATS server broker_url: Broker URL (for envelope metadata)
fileserver_url: URL of the HTTP file server for large payloads fileserver_url: URL of the HTTP file server for large payloads
fileserver_upload_handler: Function to handle fileserver uploads (must return Dict with "status", fileserver_upload_handler: Function to handle fileserver uploads (must return Dict with "status",
"uploadid", "fileid", "url" keys) "uploadid", "fileid", "url" keys)
@@ -510,60 +418,24 @@ async def smartsend(
receiver_id: UUID of the receiver (empty string means broadcast) receiver_id: UUID of the receiver (empty string means broadcast)
reply_to: Topic to reply to (empty string if no reply expected) reply_to: Topic to reply to (empty string if no reply expected)
reply_to_msg_id: Message ID this message is replying to reply_to_msg_id: Message ID this message is replying to
is_publish: Whether to automatically publish the message to NATS
nats_connection: Pre-existing NATS connection (if provided, uses this connection instead of
creating a new one; saves connection establishment overhead)
msg_id: Message ID (auto-generated UUID if not provided) msg_id: Message ID (auto-generated UUID if not provided)
sender_id: Sender ID (auto-generated UUID if not provided) sender_id: Sender ID (auto-generated UUID if not provided)
Returns: Returns:
Tuple of (env, env_json_str) where: Tuple of (env, env_json_str) where:
- env: Dict containing all metadata and payloads - env: Dict containing all metadata and payloads
- env_json_str: JSON string for publishing to NATS - env_json_str: JSON string for transport
Example: Example:
>>> # Send a single payload (still wrapped in a list) >>> # Send a single payload (still wrapped in a list)
>>> data = {"key": "value"} >>> data = {"key": "value"}
>>> env, env_json_str = await smartsend( >>> env, env_json_str = await smartpack(
... "my.subject", ... "my.subject",
... [("dataname1", data, "dictionary")], ... [("dataname1", data, "dictionary")]
... broker_url="nats://localhost:4222"
... ) ... )
>>> >>>
>>> # Send multiple payloads with different types >>> # Publish the JSON string via your preferred transport
>>> data1 = {"key1": "value1"} >>> # await my_nats_client.publish("my.subject", env_json_str)
>>> data2 = [1, 2, 3, 4, 5]
>>> env, env_json_str = await smartsend(
... "my.subject",
... [("dataname1", data1, "dictionary"), ("dataname2", data2, "arrowtable")]
... )
>>>
>>> # Send a large array using fileserver upload
>>> data = list(range(10_000_000)) # ~80 MB
>>> env, env_json_str = await smartsend(
... "large.data",
... [("large_table", data, "arrowtable")]
... )
>>>
>>> # Send jsontable (JSON format for human-readable tabular data)
>>> users = [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]
>>> env, env_json_str = await smartsend(
... "json.data",
... [("users", users, "jsontable")]
... )
>>>
>>> # Mixed content (e.g., chat with text and image)
>>> env, env_json_str = await smartsend(
... "chat.subject",
... [
... ("message_text", "Hello!", "text"),
... ("user_image", image_data, "image"),
... ("audio_clip", audio_data, "audio")
... ]
... )
>>>
>>> # Publish the JSON string directly using NATS request-reply pattern
>>> # reply = await nats.request(broker_url, subject, env_json_str, reply_to=reply_to_topic)
""" """
if correlation_id is None: if correlation_id is None:
correlation_id = str(uuid.uuid4()) correlation_id = str(uuid.uuid4())
@@ -572,7 +444,7 @@ async def smartsend(
if sender_id is None: if sender_id is None:
sender_id = str(uuid.uuid4()) sender_id = str(uuid.uuid4())
log_trace(correlation_id, f"Starting smartsend for subject: {subject}") log_trace(correlation_id, f"Starting smartpack for subject: {subject}")
# Process payloads # Process payloads
payloads = [] payloads = []
@@ -619,16 +491,10 @@ async def smartsend(
env_json_str = json.dumps(env) env_json_str = json.dumps(env)
if is_publish:
if nats_connection:
await publish_message(nats_connection, subject, env_json_str, correlation_id)
else:
await publish_message(broker_url, subject, env_json_str, correlation_id)
return env, env_json_str return env, env_json_str
async def smartreceive( async def smartunpack(
msg: Any, msg: Any,
fileserver_download_handler: Callable = fetch_with_backoff, fileserver_download_handler: Callable = fetch_with_backoff,
max_retries: int = 5, max_retries: int = 5,
@@ -636,14 +502,15 @@ async def smartreceive(
max_delay: int = 5000 max_delay: int = 5000
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" """
Receive and process NATS messages. Receive and process messages.
This function processes incoming NATS messages, handling both direct transport This function processes incoming messages, handling both direct transport
(base64 decoded payloads) and link transport (URL-based payloads). (base64 decoded payloads) and link transport (URL-based payloads).
It deserializes the data based on the transport type and returns the result. It deserializes the data based on the transport type and returns the result.
Args: Args:
msg: NATS message to process msg: Message to process. Accepts JSON string directly, or an object with
a `payload` or `data` property containing the JSON string.
fileserver_download_handler: Function to handle downloading data from file server URLs fileserver_download_handler: Function to handle downloading data from file server URLs
max_retries: Maximum retry attempts for fetching URL max_retries: Maximum retry attempts for fetching URL
base_delay: Initial delay for exponential backoff in ms base_delay: Initial delay for exponential backoff in ms
@@ -653,10 +520,12 @@ async def smartreceive(
Dict with envelope metadata and payloads field containing List[Tuple[str, Any, str]] Dict with envelope metadata and payloads field containing List[Tuple[str, Any, str]]
Example: Example:
>>> # Receive and process message >>> # Receive from JSON string directly
>>> env = await smartreceive(msg, fileserver_download_handler=fetch_with_backoff) >>> env = await smartunpack(json_string)
>>>
>>> # Receive from transport message object (e.g., NATS, MQTT)
>>> env = await smartunpack(nats_msg, fileserver_download_handler=fetch_with_backoff)
>>> # env is a Dict with "payloads" key containing List[Tuple[str, Any, str]] >>> # env is a Dict with "payloads" key containing List[Tuple[str, Any, str]]
>>> # Access payloads: for dataname, data, type_ in env["payloads"]
>>> for dataname, data, type_ in env["payloads"]: >>> for dataname, data, type_ in env["payloads"]:
>>> print(f"{dataname}: {data} (type: {type_})") >>> print(f"{dataname}: {data} (type: {type_})")
""" """
@@ -664,13 +533,19 @@ async def smartreceive(
if isinstance(msg, dict): if isinstance(msg, dict):
# Already parsed # Already parsed
env_json_obj = msg env_json_obj = msg
elif isinstance(msg, str):
# Raw JSON string
env_json_obj = json.loads(msg)
elif hasattr(msg, 'payload'): elif hasattr(msg, 'payload'):
# NATS message object # Transport message object with payload property
payload = msg.payload if isinstance(msg.payload, str) else msg.payload.decode('utf-8') payload = msg.payload if isinstance(msg.payload, str) else msg.payload.decode('utf-8')
env_json_obj = json.loads(payload) env_json_obj = json.loads(payload)
elif hasattr(msg, 'data'):
# Transport message object with data property
payload = msg.data if isinstance(msg.data, str) else msg.data.decode('utf-8')
env_json_obj = json.loads(payload)
else: else:
# Assume it's already a JSON string or dict raise ValueError('Invalid message format: expected JSON string or message object')
env_json_obj = json.loads(msg) if isinstance(msg, str) else msg
log_trace(env_json_obj['correlation_id'], "Processing received message") log_trace(env_json_obj['correlation_id'], "Processing received message")
@@ -727,7 +602,7 @@ async def smartreceive(
class msghandler: class msghandler:
""" """
Cross-platform NATS bridge implementation. Cross-platform message bridge implementation.
This class provides a convenient interface for msghandler functionality, This class provides a convenient interface for msghandler functionality,
encapsulating the main functions and providing a class-based API. encapsulating the main functions and providing a class-based API.
@@ -742,49 +617,49 @@ class msghandler:
Initialize msghandler. Initialize msghandler.
Args: Args:
broker_url: NATS server URL (defaults to DEFAULT_BROKER_URL) broker_url: Broker URL (defaults to DEFAULT_BROKER_URL)
fileserver_url: HTTP file server URL (defaults to DEFAULT_FILESERVER_URL) fileserver_url: HTTP file server URL (defaults to DEFAULT_FILESERVER_URL)
""" """
self.broker_url = broker_url or self.DEFAULT_BROKER_URL self.broker_url = broker_url or self.DEFAULT_BROKER_URL
self.fileserver_url = fileserver_url or self.DEFAULT_FILESERVER_URL self.fileserver_url = fileserver_url or self.DEFAULT_FILESERVER_URL
async def smartsend( async def smartpack(
self, self,
subject: str, subject: str,
data: List[Tuple[str, Any, str]], data: List[Tuple[str, Any, str]],
**kwargs **kwargs
) -> Tuple[Dict, str]: ) -> Tuple[Dict, str]:
""" """
Send data via NATS. Send data.
Args: Args:
subject: NATS subject to publish to subject: Subject/topic to send to
data: List of (dataname, data, type) tuples data: List of (dataname, data, type) tuples
**kwargs: Additional options passed to smartsend **kwargs: Additional options passed to smartpack
Returns: Returns:
Tuple of (env, env_json_str) Tuple of (env, env_json_str)
""" """
kwargs['broker_url'] = kwargs.get('broker_url', self.broker_url) kwargs['broker_url'] = kwargs.get('broker_url', self.broker_url)
kwargs['fileserver_url'] = kwargs.get('fileserver_url', self.fileserver_url) kwargs['fileserver_url'] = kwargs.get('fileserver_url', self.fileserver_url)
return await smartsend(subject, data, **kwargs) return await smartpack(subject, data, **kwargs)
async def smartreceive( async def smartunpack(
self, self,
msg: Any, msg: Any,
**kwargs **kwargs
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" """
Receive and process NATS message. Receive and process message.
Args: Args:
msg: NATS message to process msg: Message to process
**kwargs: Additional options passed to smartreceive **kwargs: Additional options passed to smartunpack
Returns: Returns:
Dict with envelope metadata and payloads Dict with envelope metadata and payloads
""" """
return await smartreceive(msg, **kwargs) return await smartunpack(msg, **kwargs)
# Convenience functions for module-level usage # Convenience functions for module-level usage
@@ -797,14 +672,14 @@ def send(
Convenience function for sending data. Convenience function for sending data.
Args: Args:
subject: NATS subject to publish to subject: Subject/topic to send to
data: List of (dataname, data, type) tuples data: List of (dataname, data, type) tuples
**kwargs: Additional options **kwargs: Additional options
Returns: Returns:
Tuple of (env, env_json_str) Tuple of (env, env_json_str)
""" """
return asyncio.run(smartsend(subject, data, **kwargs)) return asyncio.run(smartpack(subject, data, **kwargs))
def receive( def receive(
@@ -815,18 +690,18 @@ def receive(
Convenience function for receiving messages. Convenience function for receiving messages.
Args: Args:
msg: NATS message to process msg: Message to process
**kwargs: Additional options **kwargs: Additional options
Returns: Returns:
Dict with envelope metadata and payloads Dict with envelope metadata and payloads
""" """
return asyncio.run(smartreceive(msg, **kwargs)) return asyncio.run(smartunpack(msg, **kwargs))
__all__ = [ __all__ = [
'smartsend', 'smartpack',
'smartreceive', 'smartunpack',
'plik_oneshot_upload', 'plik_oneshot_upload',
'fetch_with_backoff', 'fetch_with_backoff',
'msghandler', 'msghandler',
@@ -835,9 +710,7 @@ __all__ = [
'DEFAULT_SIZE_THRESHOLD', 'DEFAULT_SIZE_THRESHOLD',
'DEFAULT_BROKER_URL', 'DEFAULT_BROKER_URL',
'DEFAULT_FILESERVER_URL', 'DEFAULT_FILESERVER_URL',
'NATSClient',
'_serialize_data', '_serialize_data',
'_deserialize_data', '_deserialize_data',
'log_trace', 'log_trace'
'publish_message' ]
]

View File

@@ -1,6 +1,6 @@
// msghandler Rust Module // msghandler Rust Module
// Cross-platform bi-directional data bridge for NATS communication // Cross-platform bi-directional data bridge
// Implements smartsend and smartreceive for NATS communication // Implements smartpack and smartunpack for message transport
// with support for both direct payload transport and URL-based transport // with support for both direct payload transport and URL-based transport
// for larger payloads using the Claim-Check pattern. // for larger payloads using the Claim-Check pattern.
// //
@@ -17,10 +17,9 @@
// Supported types: "text", "dictionary", "arrowtable", "jsontable", // Supported types: "text", "dictionary", "arrowtable", "jsontable",
// "image", "audio", "video", "binary" // "image", "audio", "video", "binary"
use async_trait::async_trait;
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use chrono::Utc; use chrono::Utc;
use reqwest::Client; use reqwest::blocking::Client;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue; use serde_json::Value as JsonValue;
use std::collections::HashMap; use std::collections::HashMap;
@@ -28,7 +27,6 @@ use std::fmt;
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tokio::time::sleep;
use uuid::Uuid; use uuid::Uuid;
// ============================================================================ // ============================================================================
@@ -38,8 +36,8 @@ use uuid::Uuid;
/// Default size threshold (0.5MB) for switching from direct to link transport /// Default size threshold (0.5MB) for switching from direct to link transport
pub const DEFAULT_SIZE_THRESHOLD: usize = 500_000; pub const DEFAULT_SIZE_THRESHOLD: usize = 500_000;
/// Default NATS server URL /// Default broker URL
pub const DEFAULT_BROKER_URL: &str = "nats://localhost:4222"; pub const DEFAULT_BROKER_URL: &str = "localhost:4222";
/// Default HTTP file server URL for link transport /// Default HTTP file server URL for link transport
pub const DEFAULT_FILESERVER_URL: &str = "http://localhost:8080"; pub const DEFAULT_FILESERVER_URL: &str = "http://localhost:8080";
@@ -59,7 +57,7 @@ pub const DEFAULT_MAX_DELAY: u64 = 5_000;
/// Errors that can occur during msghandler operations /// Errors that can occur during msghandler operations
#[derive(Debug)] #[derive(Debug)]
pub enum msghandlerError { pub enum MsgHandlerError {
/// Unsupported or unknown payload type /// Unsupported or unknown payload type
UnknownPayloadType(String), UnknownPayloadType(String),
/// File server upload failed /// File server upload failed
@@ -68,8 +66,8 @@ pub enum msghandlerError {
DownloadFailed { url: String, retries: u32 }, DownloadFailed { url: String, retries: u32 },
/// Unknown transport type /// Unknown transport type
UnknownTransport(String), UnknownTransport(String),
/// NATS connection failed /// Connection failed
NatConnectionFailed(String), ConnectionFailed(String),
/// Payload deserialization error /// Payload deserialization error
DeserializationError(String), DeserializationError(String),
/// HTTP request error /// HTTP request error
@@ -86,34 +84,34 @@ pub enum msghandlerError {
InvalidEnvelope(String), InvalidEnvelope(String),
} }
impl fmt::Display for msghandlerError { impl fmt::Display for MsgHandlerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
msghandlerError::UnknownPayloadType(p) => write!(f, "Unknown payload_type: {}", p), MsgHandlerError::UnknownPayloadType(p) => write!(f, "Unknown payload_type: {}", p),
msghandlerError::UploadFailed(msg) => write!(f, "Failed to upload: {}", msg), MsgHandlerError::UploadFailed(msg) => write!(f, "Failed to upload: {}", msg),
msghandlerError::DownloadFailed { url, retries } => { MsgHandlerError::DownloadFailed { url, retries } => {
write!(f, "Failed to fetch {} after {} attempts", url, retries) write!(f, "Failed to fetch {} after {} attempts", url, retries)
} }
msghandlerError::UnknownTransport(t) => write!(f, "Unknown transport type: {}", t), MsgHandlerError::UnknownTransport(t) => write!(f, "Unknown transport type: {}", t),
msghandlerError::NatConnectionFailed(msg) => write!(f, "NATS connection failed: {}", msg), MsgHandlerError::ConnectionFailed(msg) => write!(f, "Connection failed: {}", msg),
msghandlerError::DeserializationError(msg) => { MsgHandlerError::DeserializationError(msg) => {
write!(f, "Deserialization error: {}", msg) write!(f, "Deserialization error: {}", msg)
} }
msghandlerError::HttpError { status, message } => { MsgHandlerError::HttpError { status, message } => {
write!(f, "HTTP error {}: {}", status, message) write!(f, "HTTP error {}: {}", status, message)
} }
msghandlerError::IoError(msg) => write!(f, "IO error: {}", msg), MsgHandlerError::IoError(msg) => write!(f, "IO error: {}", msg),
msghandlerError::JsonError(msg) => write!(f, "JSON error: {}", msg), MsgHandlerError::JsonError(msg) => write!(f, "JSON error: {}", msg),
msghandlerError::Base64Error(msg) => write!(f, "Base64 error: {}", msg), MsgHandlerError::Base64Error(msg) => write!(f, "Base64 error: {}", msg),
msghandlerError::SizeExceeded { size, max } => { MsgHandlerError::SizeExceeded { size, max } => {
write!(f, "Payload size {} exceeds max {}", size, max) write!(f, "Payload size {} exceeds max {}", size, max)
} }
msghandlerError::InvalidEnvelope(msg) => write!(f, "Invalid envelope: {}", msg), MsgHandlerError::InvalidEnvelope(msg) => write!(f, "Invalid envelope: {}", msg),
} }
} }
} }
impl std::error::Error for msghandlerError {} impl std::error::Error for MsgHandlerError {}
// ============================================================================ // ============================================================================
// Payload Enum - Type-safe payload data // Payload Enum - Type-safe payload data
@@ -172,7 +170,7 @@ impl Payload {
// Message Payload Structure (wire format) // Message Payload Structure (wire format)
// ============================================================================ // ============================================================================
/// Represents a single payload within a NATS message envelope. /// Represents a single payload within a message envelope.
/// Supports both direct transport (base64-encoded data) and link transport (URL-based). /// Supports both direct transport (base64-encoded data) and link transport (URL-based).
#[derive(Serialize, Deserialize, Clone, Debug, Default)] #[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct MsgPayloadV1 { pub struct MsgPayloadV1 {
@@ -257,7 +255,7 @@ impl MsgPayloadV1 {
// Message Envelope Structure (wire format) // Message Envelope Structure (wire format)
// ============================================================================ // ============================================================================
/// Represents a complete NATS message envelope containing multiple payloads /// Represents a complete message envelope containing multiple payloads
/// with metadata for routing, tracing, and message context. /// with metadata for routing, tracing, and message context.
#[derive(Serialize, Deserialize, Clone, Debug, Default)] #[derive(Serialize, Deserialize, Clone, Debug, Default)]
pub struct MsgEnvelopeV1 { pub struct MsgEnvelopeV1 {
@@ -268,7 +266,7 @@ pub struct MsgEnvelopeV1 {
/// Message publication timestamp (ISO 8601 UTC) /// Message publication timestamp (ISO 8601 UTC)
pub timestamp: String, pub timestamp: String,
/// NATS subject/topic to publish the message to /// Subject/topic to send the message to
pub send_to: String, pub send_to: String,
/// Purpose of the message: "ACK", "NACK", "updateStatus", "shutdown", /// Purpose of the message: "ACK", "NACK", "updateStatus", "shutdown",
/// "chat", "command", "event" /// "chat", "command", "event"
@@ -286,7 +284,7 @@ pub struct MsgEnvelopeV1 {
pub reply_to: String, pub reply_to: String,
/// Message ID this message is replying to /// Message ID this message is replying to
pub reply_to_msg_id: String, pub reply_to_msg_id: String,
/// NATS broker URL /// Broker URL
pub broker_url: String, pub broker_url: String,
/// Optional message-level metadata /// Optional message-level metadata
@@ -317,9 +315,9 @@ impl MsgEnvelopeV1 {
} }
} }
/// Convert the envelope to a JSON string for NATS publishing /// Convert the envelope to a JSON string for transport
pub fn to_json(&self) -> Result<String, msghandlerError> { pub fn to_json(&self) -> Result<String, MsgHandlerError> {
serde_json::to_string(self).map_err(|e| msghandlerError::JsonError(e.to_string())) serde_json::to_string(self).map_err(|e| MsgHandlerError::JsonError(e.to_string()))
} }
} }
@@ -327,9 +325,9 @@ impl MsgEnvelopeV1 {
// Options Structures // Options Structures
// ============================================================================ // ============================================================================
/// Options for the `smartsend` function /// Options for the `smartpack` function
pub struct SmartsendOptions { pub struct smartpackOptions {
/// NATS server URL /// Broker URL
pub broker_url: String, pub broker_url: String,
/// HTTP file server URL for large payloads /// HTTP file server URL for large payloads
pub fileserver_url: String, pub fileserver_url: String,
@@ -357,9 +355,9 @@ pub struct SmartsendOptions {
pub sender_id: String, pub sender_id: String,
} }
impl Default for SmartsendOptions { impl Default for smartpackOptions {
fn default() -> Self { fn default() -> Self {
SmartsendOptions { smartpackOptions {
broker_url: DEFAULT_BROKER_URL.to_string(), broker_url: DEFAULT_BROKER_URL.to_string(),
fileserver_url: DEFAULT_FILESERVER_URL.to_string(), fileserver_url: DEFAULT_FILESERVER_URL.to_string(),
fileserver_upload_handler: None, fileserver_upload_handler: None,
@@ -377,8 +375,8 @@ impl Default for SmartsendOptions {
} }
} }
/// Options for the `smartreceive` function /// Options for the `smartunpack` function
pub struct SmartreceiveOptions { pub struct smartunpackOptions {
/// Custom file server download handler (optional, uses exponential backoff by default) /// Custom file server download handler (optional, uses exponential backoff by default)
pub fileserver_download_handler: Option<Arc<dyn FileDownloadHandler>>, pub fileserver_download_handler: Option<Arc<dyn FileDownloadHandler>>,
/// Maximum retry attempts for fetching a URL /// Maximum retry attempts for fetching a URL
@@ -389,9 +387,9 @@ pub struct SmartreceiveOptions {
pub max_delay: u64, pub max_delay: u64,
} }
impl Default for SmartreceiveOptions { impl Default for smartunpackOptions {
fn default() -> Self { fn default() -> Self {
SmartreceiveOptions { smartunpackOptions {
fileserver_download_handler: None, fileserver_download_handler: None,
max_retries: DEFAULT_MAX_RETRIES, max_retries: DEFAULT_MAX_RETRIES,
base_delay: DEFAULT_BASE_DELAY, base_delay: DEFAULT_BASE_DELAY,
@@ -405,16 +403,15 @@ impl Default for SmartreceiveOptions {
// ============================================================================ // ============================================================================
/// Trait for uploading data to a file server /// Trait for uploading data to a file server
#[async_trait]
pub trait FileUploadHandler: Send + Sync { pub trait FileUploadHandler: Send + Sync {
/// Upload data to the file server /// Upload data to the file server
/// Returns upload ID, file ID, and download URL /// Returns upload ID, file ID, and download URL
async fn upload( fn upload(
&self, &self,
file_server_url: &str, file_server_url: &str,
dataname: &str, dataname: &str,
data: &[u8], data: &[u8],
) -> Result<UploadResult, msghandlerError>; ) -> Result<UploadResult, MsgHandlerError>;
} }
/// Result of a file server upload /// Result of a file server upload
@@ -431,17 +428,16 @@ pub struct UploadResult {
} }
/// Trait for downloading data from a file server /// Trait for downloading data from a file server
#[async_trait]
pub trait FileDownloadHandler: Send + Sync { pub trait FileDownloadHandler: Send + Sync {
/// Download data from a URL with retry logic /// Download data from a URL with retry logic
async fn download( fn download(
&self, &self,
url: &str, url: &str,
max_retries: u32, max_retries: u32,
base_delay: u64, base_delay: u64,
max_delay: u64, max_delay: u64,
correlation_id: &str, correlation_id: &str,
) -> Result<Vec<u8>, msghandlerError>; ) -> Result<Vec<u8>, MsgHandlerError>;
} }
// ============================================================================ // ============================================================================
@@ -457,14 +453,13 @@ pub trait FileDownloadHandler: Send + Sync {
/// 4. Returns identifiers and download URL /// 4. Returns identifiers and download URL
pub struct PlikOneshotUploadHandler; pub struct PlikOneshotUploadHandler;
#[async_trait]
impl FileUploadHandler for PlikOneshotUploadHandler { impl FileUploadHandler for PlikOneshotUploadHandler {
async fn upload( fn upload(
&self, &self,
file_server_url: &str, file_server_url: &str,
dataname: &str, dataname: &str,
data: &[u8], data: &[u8],
) -> Result<UploadResult, msghandlerError> { ) -> Result<UploadResult, MsgHandlerError> {
let client = Client::new(); let client = Client::new();
// Step 1: Create one-shot upload session // Step 1: Create one-shot upload session
@@ -474,11 +469,10 @@ impl FileUploadHandler for PlikOneshotUploadHandler {
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.json(&session_body) .json(&session_body)
.send() .send()
.await .map_err(|e| MsgHandlerError::UploadFailed(format!("Failed to create upload session: {}", e)))?;
.map_err(|e| msghandlerError::UploadFailed(format!("Failed to create upload session: {}", e)))?;
if !session_resp.status().is_success() { if !session_resp.status().is_success() {
return Err(msghandlerError::UploadFailed(format!( return Err(MsgHandlerError::UploadFailed(format!(
"Session creation failed with status: {}", "Session creation failed with status: {}",
session_resp.status() session_resp.status()
))); )));
@@ -486,8 +480,7 @@ impl FileUploadHandler for PlikOneshotUploadHandler {
let session_json: JsonValue = session_resp let session_json: JsonValue = session_resp
.json() .json()
.await .map_err(|e| MsgHandlerError::UploadFailed(format!("Failed to parse session response: {}", e)))?;
.map_err(|e| msghandlerError::UploadFailed(format!("Failed to parse session response: {}", e)))?;
let uploadid = session_json["id"] let uploadid = session_json["id"]
.as_str() .as_str()
@@ -499,31 +492,30 @@ impl FileUploadHandler for PlikOneshotUploadHandler {
.to_string(); .to_string();
if uploadid.is_empty() || uploadtoken.is_empty() { if uploadid.is_empty() || uploadtoken.is_empty() {
return Err(msghandlerError::UploadFailed( return Err(MsgHandlerError::UploadFailed(
"Missing uploadid or uploadToken in session response".to_string(), "Missing uploadid or uploadToken in session response".to_string(),
)); ));
} }
// Step 2: Upload the file as multipart/form-data // Step 2: Upload the file as multipart/form-data
let upload_url = format!("{}/file/{}", file_server_url, uploadid); let upload_url = format!("{}/file/{}", file_server_url, uploadid);
let form = reqwest::multipart::Form::new() let form = reqwest::blocking::multipart::Form::new()
.part( .part(
"file", "file",
reqwest::multipart::Part::bytes(data.to_vec()) reqwest::blocking::multipart::Part::bytes(data.to_vec())
.file_name(dataname.to_string()) .file_name(dataname.to_string())
.mime_str("application/octet-stream") .mime_str("application/octet-stream")
.map_err(|e| msghandlerError::UploadFailed(format!("Invalid MIME type: {}", e)))?, .map_err(|e| MsgHandlerError::UploadFailed(format!("Invalid MIME type: {}", e)))?,
); );
let resp = client let resp = client
.post(&upload_url) .post(&upload_url)
.header("X-UploadToken", &uploadtoken) .header("X-UploadToken", &uploadtoken)
.multipart(form) .multipart(form)
.send() .send()
.await .map_err(|e| MsgHandlerError::UploadFailed(format!("Upload request failed: {}", e)))?;
.map_err(|e| msghandlerError::UploadFailed(format!("Upload request failed: {}", e)))?;
if !resp.status().is_success() { if !resp.status().is_success() {
return Err(msghandlerError::UploadFailed(format!( return Err(MsgHandlerError::UploadFailed(format!(
"Upload failed with status: {}", "Upload failed with status: {}",
resp.status() resp.status()
))); )));
@@ -532,8 +524,7 @@ impl FileUploadHandler for PlikOneshotUploadHandler {
let status_code = resp.status().as_u16(); let status_code = resp.status().as_u16();
let upload_json: JsonValue = resp let upload_json: JsonValue = resp
.json() .json()
.await .map_err(|e| MsgHandlerError::UploadFailed(format!("Failed to parse upload response: {}", e)))?;
.map_err(|e| msghandlerError::UploadFailed(format!("Failed to parse upload response: {}", e)))?;
let fileid = upload_json["id"].as_str().unwrap_or("").to_string(); let fileid = upload_json["id"].as_str().unwrap_or("").to_string();
@@ -564,29 +555,29 @@ impl FileUploadHandler for PlikOneshotUploadHandler {
/// 4. Throws error after max_retries are exhausted /// 4. Throws error after max_retries are exhausted
pub struct BackoffDownloadHandler; pub struct BackoffDownloadHandler;
#[async_trait]
impl FileDownloadHandler for BackoffDownloadHandler { impl FileDownloadHandler for BackoffDownloadHandler {
async fn download( fn download(
&self, &self,
url: &str, url: &str,
max_retries: u32, max_retries: u32,
base_delay: u64, base_delay: u64,
max_delay: u64, max_delay: u64,
correlation_id: &str, correlation_id: &str,
) -> Result<Vec<u8>, msghandlerError> { ) -> Result<Vec<u8>, MsgHandlerError> {
let client = Client::new(); let client = Client::new();
let mut delay = base_delay; let mut delay = base_delay;
for attempt in 1..=max_retries { for attempt in 1..=max_retries {
match client.get(url).send().await { match client.get(url).send() {
Ok(response) if response.status().is_success() => { Ok(response) if response.status().is_success() => {
log_trace(correlation_id, &format!( log_trace(correlation_id, &format!(
"Successfully fetched {} on attempt {}", "Successfully fetched {} on attempt {}",
url, attempt url, attempt
)); ));
let bytes = response.bytes().await let bytes = response
.bytes()
.map(|b| b.to_vec()) .map(|b| b.to_vec())
.map_err(|_e| msghandlerError::DownloadFailed { .map_err(|_e| MsgHandlerError::DownloadFailed {
url: url.to_string(), url: url.to_string(),
retries: max_retries, retries: max_retries,
})?; })?;
@@ -611,12 +602,12 @@ impl FileDownloadHandler for BackoffDownloadHandler {
} }
if attempt < max_retries { if attempt < max_retries {
sleep(Duration::from_millis(delay)).await; std::thread::sleep(Duration::from_millis(delay));
delay = (delay * 2).min(max_delay); delay = (delay * 2).min(max_delay);
} }
} }
Err(msghandlerError::DownloadFailed { Err(MsgHandlerError::DownloadFailed {
url: url.to_string(), url: url.to_string(),
retries: max_retries, retries: max_retries,
}) })
@@ -629,14 +620,14 @@ impl FileDownloadHandler for BackoffDownloadHandler {
/// Serialize payload data according to the specified payload type. /// Serialize payload data according to the specified payload type.
/// Returns the raw bytes for the serialized data. /// Returns the raw bytes for the serialized data.
fn serialize_data(payload: &Payload) -> Result<Vec<u8>, msghandlerError> { fn serialize_data(payload: &Payload) -> Result<Vec<u8>, MsgHandlerError> {
match payload { match payload {
Payload::Text(s) => Ok(s.as_bytes().to_vec()), Payload::Text(s) => Ok(s.as_bytes().to_vec()),
Payload::Dictionary(v) => serde_json::to_vec(v) Payload::Dictionary(v) => serde_json::to_vec(v)
.map_err(|e| msghandlerError::DeserializationError(format!("Dictionary serialization failed: {}", e))), .map_err(|e| MsgHandlerError::DeserializationError(format!("Dictionary serialization failed: {}", e))),
Payload::ArrowTable(b) => Ok(b.clone()), Payload::ArrowTable(b) => Ok(b.clone()),
Payload::JsonTable(v) => serde_json::to_vec(v) Payload::JsonTable(v) => serde_json::to_vec(v)
.map_err(|e| msghandlerError::DeserializationError(format!("JsonTable serialization failed: {}", e))), .map_err(|e| MsgHandlerError::DeserializationError(format!("JsonTable serialization failed: {}", e))),
Payload::Image(b) => Ok(b.clone()), Payload::Image(b) => Ok(b.clone()),
Payload::Audio(b) => Ok(b.clone()), Payload::Audio(b) => Ok(b.clone()),
Payload::Video(b) => Ok(b.clone()), Payload::Video(b) => Ok(b.clone()),
@@ -654,18 +645,18 @@ fn deserialize_data(
payload_bytes: &[u8], payload_bytes: &[u8],
payload_type: &str, payload_type: &str,
_correlation_id: &str, _correlation_id: &str,
) -> Result<Payload, msghandlerError> { ) -> Result<Payload, MsgHandlerError> {
match payload_type { match payload_type {
"text" => { "text" => {
let text = String::from_utf8(payload_bytes.to_vec()) let text = String::from_utf8(payload_bytes.to_vec())
.map_err(|e| msghandlerError::DeserializationError(format!("Invalid UTF-8 for text: {}", e)))?; .map_err(|e| MsgHandlerError::DeserializationError(format!("Invalid UTF-8 for text: {}", e)))?;
Ok(Payload::Text(text)) Ok(Payload::Text(text))
} }
"dictionary" => { "dictionary" => {
let json_str = String::from_utf8(payload_bytes.to_vec()) let json_str = String::from_utf8(payload_bytes.to_vec())
.map_err(|e| msghandlerError::DeserializationError(format!("Invalid UTF-8 for dictionary: {}", e)))?; .map_err(|e| MsgHandlerError::DeserializationError(format!("Invalid UTF-8 for dictionary: {}", e)))?;
let value: JsonValue = serde_json::from_str(&json_str) let value: JsonValue = serde_json::from_str(&json_str)
.map_err(|e| msghandlerError::DeserializationError(format!("Invalid JSON for dictionary: {}", e)))?; .map_err(|e| MsgHandlerError::DeserializationError(format!("Invalid JSON for dictionary: {}", e)))?;
Ok(Payload::Dictionary(value)) Ok(Payload::Dictionary(value))
} }
"arrowtable" => { "arrowtable" => {
@@ -673,16 +664,16 @@ fn deserialize_data(
} }
"jsontable" => { "jsontable" => {
let json_str = String::from_utf8(payload_bytes.to_vec()) let json_str = String::from_utf8(payload_bytes.to_vec())
.map_err(|e| msghandlerError::DeserializationError(format!("Invalid UTF-8 for jsontable: {}", e)))?; .map_err(|e| MsgHandlerError::DeserializationError(format!("Invalid UTF-8 for jsontable: {}", e)))?;
let value: JsonValue = serde_json::from_str(&json_str) let value: JsonValue = serde_json::from_str(&json_str)
.map_err(|e| msghandlerError::DeserializationError(format!("Invalid JSON for jsontable: {}", e)))?; .map_err(|e| MsgHandlerError::DeserializationError(format!("Invalid JSON for jsontable: {}", e)))?;
Ok(Payload::JsonTable(value)) Ok(Payload::JsonTable(value))
} }
"image" => Ok(Payload::Image(payload_bytes.to_vec())), "image" => Ok(Payload::Image(payload_bytes.to_vec())),
"audio" => Ok(Payload::Audio(payload_bytes.to_vec())), "audio" => Ok(Payload::Audio(payload_bytes.to_vec())),
"video" => Ok(Payload::Video(payload_bytes.to_vec())), "video" => Ok(Payload::Video(payload_bytes.to_vec())),
"binary" => Ok(Payload::Binary(payload_bytes.to_vec())), "binary" => Ok(Payload::Binary(payload_bytes.to_vec())),
_ => Err(msghandlerError::UnknownPayloadType(payload_type.to_string())), _ => Err(MsgHandlerError::UnknownPayloadType(payload_type.to_string())),
} }
} }
@@ -698,10 +689,10 @@ pub fn log_trace(correlation_id: &str, message: &str) {
} }
// ============================================================================ // ============================================================================
// Public API: smartsend // Public API: smartpack
// ============================================================================ // ============================================================================
/// Send data via NATS with automatic transport selection. /// Send data with automatic transport selection.
/// ///
/// This function intelligently routes data delivery based on payload size. /// This function intelligently routes data delivery based on payload size.
/// If the serialized payload is smaller than `size_threshold`, it encodes the /// If the serialized payload is smaller than `size_threshold`, it encodes the
@@ -711,41 +702,37 @@ pub fn log_trace(correlation_id: &str, message: &str) {
/// Each payload in the list can have a different type, enabling mixed-content /// Each payload in the list can have a different type, enabling mixed-content
/// messages (e.g., chat with text, images, audio). /// messages (e.g., chat with text, images, audio).
/// ///
/// NATS publishing is the caller's responsibility. This function returns the /// Transport publishing is the caller's responsibility. This function returns the
/// envelope and its JSON string representation. /// envelope and its JSON string representation.
/// ///
/// # Arguments /// # Arguments
/// - `subject`: NATS subject to publish the message to /// - `subject`: Subject/topic to send the message to
/// - `data`: Slice of (dataname, payload, payload_type) tuples /// - `data`: Slice of (dataname, payload, payload_type) tuples
/// - `options`: Configuration options /// - `options`: Configuration options
/// ///
/// # Returns /// # Returns
/// - `Result<(MsgEnvelopeV1, String), msghandlerError>` containing the envelope and JSON string /// - `Result<(MsgEnvelopeV1, String), MsgHandlerError>` containing the envelope and JSON string
/// ///
/// # Example /// # Example
/// ```no_run /// ```no_run
/// use msghandler::{smartsend, Payload, SmartsendOptions}; /// use msghandler::{smartpack, Payload, smartpackOptions};
/// ///
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> { /// let (envelope, json_str) = smartpack(
/// let (envelope, json_str) = smartsend(
/// "/agent/wine/api/v1/prompt", /// "/agent/wine/api/v1/prompt",
/// &[ /// &[
/// ("msg".to_string(), Payload::Text("Hello!".to_string()), "text".to_string()), /// ("msg".to_string(), Payload::Text("Hello!".to_string()), "text".to_string()),
/// ("data".to_string(), Payload::Binary(vec![1, 2, 3]), "binary".to_string()), /// ("data".to_string(), Payload::Binary(vec![1, 2, 3]), "binary".to_string()),
/// ], /// ],
/// &SmartsendOptions::default(), /// &smartpackOptions::default(),
/// ).await?; /// ).unwrap();
/// ///
/// // Caller publishes to NATS /// // Caller publishes via their preferred transport
/// // conn.publish("/agent/wine/api/v1/prompt", &json_str)?;
/// # Ok(())
/// # }
/// ``` /// ```
pub async fn smartsend( pub fn smartpack(
subject: &str, subject: &str,
data: &[(String, Payload, String)], data: &[(String, Payload, String)],
options: &SmartsendOptions, options: &smartpackOptions,
) -> Result<(MsgEnvelopeV1, String), msghandlerError> { ) -> Result<(MsgEnvelopeV1, String), MsgHandlerError> {
let correlation_id = if options.correlation_id.is_empty() { let correlation_id = if options.correlation_id.is_empty() {
Uuid::new_v4().to_string() Uuid::new_v4().to_string()
} else { } else {
@@ -765,7 +752,7 @@ pub async fn smartsend(
}; };
log_trace(&correlation_id, &format!( log_trace(&correlation_id, &format!(
"Starting smartsend for subject: {}", subject "Starting smartpack for subject: {}", subject
)); ));
let mut payloads: Vec<MsgPayloadV1> = Vec::new(); let mut payloads: Vec<MsgPayloadV1> = Vec::new();
@@ -793,7 +780,7 @@ pub async fn smartsend(
)); ));
if payload_size < options.size_threshold { if payload_size < options.size_threshold {
// Direct transport: Base64 encode and include in NATS message // Direct transport: Base64 encode and include in message envelope
let payload_b64 = BASE64.encode(&payload_bytes); let payload_b64 = BASE64.encode(&payload_bytes);
log_trace(&correlation_id, &format!( log_trace(&correlation_id, &format!(
"Using direct transport for {} bytes", payload_size "Using direct transport for {} bytes", payload_size
@@ -807,12 +794,11 @@ pub async fn smartsend(
); );
payloads.push(msg_payload); payloads.push(msg_payload);
} else { } else {
// Link transport: Upload to file server, include URL in NATS message // Link transport: Upload to file server, include URL in message envelope
log_trace(&correlation_id, "Using link transport, uploading to fileserver"); log_trace(&correlation_id, "Using link transport, uploading to fileserver");
let upload_result = upload_handler let upload_result = upload_handler
.upload(&options.fileserver_url, dataname, &payload_bytes) .upload(&options.fileserver_url, dataname, &payload_bytes)?;
.await?;
log_trace(&correlation_id, &format!( log_trace(&correlation_id, &format!(
"Uploaded to URL: {}", upload_result.url "Uploaded to URL: {}", upload_result.url
@@ -858,7 +844,7 @@ pub async fn smartsend(
// ============================================================================ // ============================================================================
/// Store deserialized Payload data back into a MsgPayloadV1's data field. /// Store deserialized Payload data back into a MsgPayloadV1's data field.
/// After smartreceive(), payload.data contains the deserialized content as a string /// After smartunpack(), payload.data contains the deserialized content as a string
/// (decoded text, JSON string, or base64 for binary types). /// (decoded text, JSON string, or base64 for binary types).
fn store_deserialized_data(payload: &MsgPayloadV1, deserialized: &Payload) -> MsgPayloadV1 { fn store_deserialized_data(payload: &MsgPayloadV1, deserialized: &Payload) -> MsgPayloadV1 {
let mut p = payload.clone(); let mut p = payload.clone();
@@ -875,59 +861,56 @@ fn store_deserialized_data(payload: &MsgPayloadV1, deserialized: &Payload) -> Ms
} }
// ============================================================================ // ============================================================================
// Public API: smartreceive // Public API: smartunpack
// ============================================================================ // ============================================================================
/// Receive and process messages from NATS. /// Receive and process messages.
/// ///
/// This function processes incoming NATS messages, handling both direct transport /// This function processes incoming messages, handling both direct transport
/// (base64 decoded payloads) and link transport (URL-based payloads). /// (base64 decoded payloads) and link transport (URL-based payloads).
/// It deserializes the data based on the payload type and returns the envelope /// It deserializes the data based on the payload type and returns the envelope
/// with deserialized payloads. /// with deserialized payloads.
/// ///
/// # Arguments /// # Arguments
/// - `msg_json_str`: JSON string from NATS message payload /// - `msg_json_str`: JSON string from the message payload
/// - `options`: Configuration options /// - `options`: Configuration options
/// ///
/// # Returns /// # Returns
/// - `Result<MsgEnvelopeV1, msghandlerError>` with deserialized payloads /// - `Result<MsgEnvelopeV1, MsgHandlerError>` with deserialized payloads
/// ///
/// # Example /// # Example
/// ```no_run /// ```no_run
/// use msghandler::{smartreceive, SmartreceiveOptions}; /// use msghandler::{smartunpack, smartunpackOptions};
/// use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; /// use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
/// ///
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let msg_json_str = r#"{"correlation_id":"abc123","msg_id":"msg-uuid", /// let msg_json_str = r#"{"correlation_id":"abc123","msg_id":"msg-uuid",
/// "timestamp":"2026-01-01T00:00:00Z","send_to":"/test", /// "timestamp":"2026-01-01T00:00:00Z","send_to":"/test",
/// "msg_purpose":"chat","sender_name":"test","sender_id":"sender-uuid", /// "msg_purpose":"chat","sender_name":"test","sender_id":"sender-uuid",
/// "receiver_name":"","receiver_id":"","reply_to":"","reply_to_msg_id":"", /// "receiver_name":"","receiver_id":"","reply_to":"","reply_to_msg_id":"",
/// "broker_url":"nats://localhost:4222","payloads":[{ /// "broker_url":"localhost:4222","payloads":[{
/// "id":"payload-uuid","dataname":"msg","payload_type":"text", /// "id":"payload-uuid","dataname":"msg","payload_type":"text",
/// "transport":"direct","encoding":"base64","size":5, /// "transport":"direct","encoding":"base64","size":5,
/// "data":"SGVsbG8=","metadata":{"payload_bytes":5} /// "data":"SGVsbG8=","metadata":{"payload_bytes":5}
/// }]}"#; /// }]}"#;
/// ///
/// let envelope = smartreceive(msg_json_str, &SmartreceiveOptions::default()).await?; /// let envelope = smartunpack(msg_json_str, &smartunpackOptions::default()).unwrap();
/// ///
/// for payload in &envelope.payloads { /// for payload in &envelope.payloads {
/// if payload.transport == "direct" { /// if payload.transport == "direct" {
/// let decoded = BASE64.decode(&payload.data)?; /// let decoded = BASE64.decode(&payload.data).unwrap();
/// println!("{}: {}", payload.dataname, String::from_utf8_lossy(&decoded)); /// println!("{}: {}", payload.dataname, String::from_utf8_lossy(&decoded));
/// } else { /// } else {
/// println!("{}: URL={}", payload.dataname, payload.data); /// println!("{}: URL={}", payload.dataname, payload.data);
/// } /// }
/// } /// }
/// # Ok(())
/// # }
/// ``` /// ```
pub async fn smartreceive( pub fn smartunpack(
msg_json_str: &str, msg_json_str: &str,
options: &SmartreceiveOptions, options: &smartunpackOptions,
) -> Result<MsgEnvelopeV1, msghandlerError> { ) -> Result<MsgEnvelopeV1, MsgHandlerError> {
// Parse the JSON envelope // Parse the JSON envelope
let mut env: MsgEnvelopeV1 = serde_json::from_str(msg_json_str) let mut env: MsgEnvelopeV1 = serde_json::from_str(msg_json_str)
.map_err(|e| msghandlerError::InvalidEnvelope(format!( .map_err(|e| MsgHandlerError::InvalidEnvelope(format!(
"Failed to parse envelope JSON: {}", e "Failed to parse envelope JSON: {}", e
)))?; )))?;
@@ -954,7 +937,7 @@ pub async fn smartreceive(
// Decode Base64 payload // Decode Base64 payload
let payload_bytes = BASE64.decode(&payload.data) let payload_bytes = BASE64.decode(&payload.data)
.map_err(|e| msghandlerError::Base64Error(format!( .map_err(|e| MsgHandlerError::Base64Error(format!(
"Base64 decode failed for '{}': {}", dataname, e "Base64 decode failed for '{}': {}", dataname, e
)))?; )))?;
@@ -982,8 +965,7 @@ pub async fn smartreceive(
options.base_delay, options.base_delay,
options.max_delay, options.max_delay,
&correlation_id, &correlation_id,
) )?;
.await?;
// Deserialize based on type and store result back into payload // Deserialize based on type and store result back into payload
let deserialized = deserialize_data( let deserialized = deserialize_data(
@@ -996,7 +978,7 @@ pub async fn smartreceive(
updated_payloads.push(updated); updated_payloads.push(updated);
} }
unknown => { unknown => {
return Err(msghandlerError::UnknownTransport(format!( return Err(MsgHandlerError::UnknownTransport(format!(
"Unknown transport type '{}' for payload '{}'", "Unknown transport type '{}' for payload '{}'",
unknown, dataname unknown, dataname
))); )));
@@ -1013,12 +995,12 @@ pub async fn smartreceive(
// ============================================================================ // ============================================================================
/// Send a single text payload /// Send a single text payload
pub async fn send_text( pub fn send_text(
subject: &str, subject: &str,
text: &str, text: &str,
options: &SmartsendOptions, options: &smartpackOptions,
) -> Result<(MsgEnvelopeV1, String), msghandlerError> { ) -> Result<(MsgEnvelopeV1, String), MsgHandlerError> {
smartsend( smartpack(
subject, subject,
&[( &[(
"text".to_string(), "text".to_string(),
@@ -1027,16 +1009,15 @@ pub async fn send_text(
)], )],
options, options,
) )
.await
} }
/// Send a single dictionary payload /// Send a single dictionary payload
pub async fn send_dictionary( pub fn send_dictionary(
subject: &str, subject: &str,
data: &JsonValue, data: &JsonValue,
options: &SmartsendOptions, options: &smartpackOptions,
) -> Result<(MsgEnvelopeV1, String), msghandlerError> { ) -> Result<(MsgEnvelopeV1, String), MsgHandlerError> {
smartsend( smartpack(
subject, subject,
&[( &[(
"dictionary".to_string(), "dictionary".to_string(),
@@ -1045,16 +1026,15 @@ pub async fn send_dictionary(
)], )],
options, options,
) )
.await
} }
/// Send a single binary payload /// Send a single binary payload
pub async fn send_binary( pub fn send_binary(
subject: &str, subject: &str,
data: &[u8], data: &[u8],
options: &SmartsendOptions, options: &smartpackOptions,
) -> Result<(MsgEnvelopeV1, String), msghandlerError> { ) -> Result<(MsgEnvelopeV1, String), MsgHandlerError> {
smartsend( smartpack(
subject, subject,
&[( &[(
"binary".to_string(), "binary".to_string(),
@@ -1063,7 +1043,6 @@ pub async fn send_binary(
)], )],
options, options,
) )
.await
} }
// ============================================================================ // ============================================================================
@@ -1080,25 +1059,22 @@ pub async fn send_binary(
/// - `filepath`: Full path to the local file to upload /// - `filepath`: Full path to the local file to upload
/// ///
/// # Returns /// # Returns
/// - `Result<UploadResult, msghandlerError>` with uploadid, fileid, and download URL /// - `Result<UploadResult, MsgHandlerError>` with uploadid, fileid, and download URL
/// ///
/// # Example /// # Example
/// ```no_run /// ```no_run
/// use msghandler::plik_upload_file; /// use msghandler::plik_upload_file;
/// ///
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> { /// let result = plik_upload_file("http://localhost:8080", "./large_file.zip").unwrap();
/// let result = plik_upload_file("http://localhost:8080", "./large_file.zip").await?;
/// println!("Uploaded to: {}", result.url); /// println!("Uploaded to: {}", result.url);
/// # Ok(())
/// # }
/// ``` /// ```
pub async fn plik_upload_file( pub fn plik_upload_file(
file_server_url: &str, file_server_url: &str,
filepath: &str, filepath: &str,
) -> Result<UploadResult, msghandlerError> { ) -> Result<UploadResult, MsgHandlerError> {
// Read the file from disk // Read the file from disk
let data = tokio::fs::read(filepath).await let data = std::fs::read(filepath)
.map_err(|e| msghandlerError::IoError(format!( .map_err(|e| MsgHandlerError::IoError(format!(
"Failed to read file '{}': {}", filepath, e "Failed to read file '{}': {}", filepath, e
)))?; )))?;
@@ -1109,7 +1085,7 @@ pub async fn plik_upload_file(
.unwrap_or_default(); .unwrap_or_default();
// Upload using the Plik one-shot handler // Upload using the Plik one-shot handler
PlikOneshotUploadHandler.upload(file_server_url, &dataname, &data).await PlikOneshotUploadHandler.upload(file_server_url, &dataname, &data)
} }
// ============================================================================ // ============================================================================
@@ -1118,13 +1094,13 @@ pub async fn plik_upload_file(
// All public types are already exported via `pub` on their definitions. // All public types are already exported via `pub` on their definitions.
// Key types: // Key types:
// - `smartsend`, `smartreceive` - main API functions // - `smartpack`, `smartunpack` - main API functions
// - `Payload` - type-safe payload enum // - `Payload` - type-safe payload enum
// - `MsgEnvelopeV1`, `MsgPayloadV1` - wire format structs // - `MsgEnvelopeV1`, `MsgPayloadV1` - wire format structs
// - `SmartsendOptions`, `SmartreceiveOptions` - configuration // - `smartpackOptions`, `smartunpackOptions` - configuration
// - `FileUploadHandler`, `FileDownloadHandler` - trait abstractions // - `FileUploadHandler`, `FileDownloadHandler` - trait abstractions
// - `PlikOneshotUploadHandler`, `BackoffDownloadHandler` - default implementations // - `PlikOneshotUploadHandler`, `BackoffDownloadHandler` - default implementations
// - `msghandlerError` - error type // - `MsgHandlerError` - error type
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
@@ -1205,10 +1181,10 @@ mod tests {
#[test] #[test]
fn test_error_display() { fn test_error_display() {
let err = msghandlerError::UnknownPayloadType("custom_type".to_string()); let err = MsgHandlerError::UnknownPayloadType("custom_type".to_string());
assert!(format!("{}", err).contains("custom_type")); assert!(format!("{}", err).contains("custom_type"));
let err = msghandlerError::DownloadFailed { let err = MsgHandlerError::DownloadFailed {
url: "http://example.com/file".to_string(), url: "http://example.com/file".to_string(),
retries: 5, retries: 5,
}; };
@@ -1217,12 +1193,12 @@ mod tests {
#[test] #[test]
fn test_default_options() { fn test_default_options() {
let opts = SmartsendOptions::default(); let opts = smartpackOptions::default();
assert_eq!(opts.size_threshold, DEFAULT_SIZE_THRESHOLD); assert_eq!(opts.size_threshold, DEFAULT_SIZE_THRESHOLD);
assert_eq!(opts.broker_url, DEFAULT_BROKER_URL); assert_eq!(opts.broker_url, DEFAULT_BROKER_URL);
assert_eq!(opts.fileserver_url, DEFAULT_FILESERVER_URL); assert_eq!(opts.fileserver_url, DEFAULT_FILESERVER_URL);
let opts = SmartreceiveOptions::default(); let opts = smartunpackOptions::default();
assert_eq!(opts.max_retries, DEFAULT_MAX_RETRIES); assert_eq!(opts.max_retries, DEFAULT_MAX_RETRIES);
assert_eq!(opts.base_delay, DEFAULT_BASE_DELAY); assert_eq!(opts.base_delay, DEFAULT_BASE_DELAY);
assert_eq!(opts.max_delay, DEFAULT_MAX_DELAY); assert_eq!(opts.max_delay, DEFAULT_MAX_DELAY);

View File

@@ -3,8 +3,7 @@ msghandler - Cross-Platform Bi-Directional Data Bridge
MicroPython Implementation MicroPython Implementation
This module provides functionality for sending and receiving data across network boundaries This module provides functionality for sending and receiving data across network boundaries
using NATS as the message bus, with support for both direct payload transport and with support for both direct payload transport and URL-based transport for larger payloads.
URL-based transport for larger payloads.
Note: MicroPython has significant constraints compared to desktop implementations: Note: MicroPython has significant constraints compared to desktop implementations:
- Limited memory (~256KB - 1MB) - Limited memory (~256KB - 1MB)
@@ -29,9 +28,9 @@ Default size threshold for switching from direct to link transport (100KB for Mi
DEFAULT_SIZE_THRESHOLD = 100000 DEFAULT_SIZE_THRESHOLD = 100000
""" """
Default NATS server URL Default broker URL
""" """
DEFAULT_BROKER_URL = "nats://localhost:4222" DEFAULT_BROKER_URL = "localhost:4222"
""" """
Default HTTP file server URL for link transport Default HTTP file server URL for link transport
@@ -190,64 +189,6 @@ def _sync_fileserver_download(url, max_retries, base_delay, max_delay, correlati
"Use direct transport only for memory-constrained devices.") "Use direct transport only for memory-constrained devices.")
# ---------------------------------------------- NATS Client ---------------------------------------------- #
class NATSClient:
"""
NATS client wrapper for MicroPython.
Note:
This is a simplified implementation for MicroPython.
Full NATS client implementation would require additional network stack support.
"""
def __init__(self, url=DEFAULT_BROKER_URL):
"""
Initialize NATS client.
Args:
url: NATS server URL
"""
self.url = url
self._connected = False
def connect(self):
"""
Connect to NATS server.
Note:
This is a placeholder implementation.
Actual NATS client would require network stack support.
Returns:
True if connected, False otherwise
"""
# Placeholder - actual implementation would connect to NATS server
self._connected = True
return self._connected
def publish(self, subject, message):
"""
Publish message to NATS subject.
Note:
This is a placeholder implementation.
Actual NATS client would require network stack support.
Args:
subject: NATS subject to publish to
message: Message to publish
"""
if not self._connected:
raise RuntimeError("Not connected to NATS server")
# Placeholder - actual implementation would publish to NATS
print(f"[NATS] Publish to {subject}: {message[:50]}...")
def close(self):
"""Close the NATS connection."""
self._connected = False
# ---------------------------------------------- Core Functions ---------------------------------------------- # # ---------------------------------------------- Core Functions ---------------------------------------------- #
def _build_envelope(subject, payloads, options): def _build_envelope(subject, payloads, options):
@@ -255,7 +196,7 @@ def _build_envelope(subject, payloads, options):
Build message envelope from payloads and metadata. Build message envelope from payloads and metadata.
Args: Args:
subject: NATS subject subject: Subject/topic
payloads: Array of payload objects payloads: Array of payload objects
options: Envelope metadata options options: Envelope metadata options
@@ -308,44 +249,43 @@ def _build_payload(dataname, payload_type, payload_bytes, transport, data):
def _publish(subject, message, correlation_id): def _publish(subject, message, correlation_id):
""" """
Publish message to NATS. Publish message via transport.
Note: Note:
This is a simplified implementation for MicroPython. This is a simplified implementation for MicroPython.
Args: Args:
subject: NATS subject to publish to subject: Subject to publish to
message: JSON message to publish message: JSON message to publish
correlation_id: Correlation ID for logging correlation_id: Correlation ID for logging
""" """
log_trace(correlation_id, f"Publishing to {subject}") log_trace(correlation_id, f"Publishing to {subject}")
# Placeholder - actual implementation would use NATSClient # Placeholder - actual implementation would publish via preferred transport
# client = NATSClient()
# client.connect()
# client.publish(subject, message)
# client.close()
def smartsend(subject, data, **kwargs): def smartpack(subject, data, **kwargs):
""" """
Send data via NATS with automatic transport selection. Send data with automatic transport selection.
This function intelligently routes data delivery based on payload size. This function intelligently routes data delivery based on payload size.
If the serialized payload is smaller than size_threshold, it encodes the data as Base64 If the serialized payload is smaller than size_threshold, it encodes the data as Base64
and publishes directly over NATS. Otherwise, it uploads the data to a fileserver into a "direct" payload. Otherwise, it uploads the data to a fileserver
and publishes only the download URL over NATS. and creates a "link" payload with the URL.
Transport publishing is the caller's responsibility. This function returns the
envelope and its JSON string representation.
Note: Note:
MicroPython has memory constraints, so the default size_threshold is lower (100KB). MicroPython has memory constraints, so the default size_threshold is lower (100KB).
Table type is not supported due to memory constraints. Table type is not supported due to memory constraints.
Args: Args:
subject: NATS subject to publish the message to subject: Subject/topic to send the message to
data: List of (dataname, data, type) tuples to send data: List of (dataname, data, type) tuples to send
- dataname: Name of the payload - dataname: Name of the payload
- data: The actual data to send - data: The actual data to send
- type: Payload type: "text", "dictionary", "image", "audio", "video", "binary" - type: Payload type: "text", "dictionary", "image", "audio", "video", "binary"
broker_url: NATS server URL (default: DEFAULT_BROKER_URL) broker_url: Broker URL (for envelope metadata, default: DEFAULT_BROKER_URL)
fileserver_url: HTTP file server URL (default: DEFAULT_FILESERVER_URL) fileserver_url: HTTP file server URL (default: DEFAULT_FILESERVER_URL)
fileserver_upload_handler: Function to handle fileserver uploads (default: _sync_fileserver_upload) fileserver_upload_handler: Function to handle fileserver uploads (default: _sync_fileserver_upload)
size_threshold: Threshold in bytes separating direct vs link transport (default: 100000) size_threshold: Threshold in bytes separating direct vs link transport (default: 100000)
@@ -356,36 +296,23 @@ def smartsend(subject, data, **kwargs):
receiver_id: UUID of the receiver (empty means broadcast) receiver_id: UUID of the receiver (empty means broadcast)
reply_to: Topic to reply to (empty if no reply expected) reply_to: Topic to reply to (empty if no reply expected)
reply_to_msg_id: Message ID this message is replying to reply_to_msg_id: Message ID this message is replying to
is_publish: Whether to automatically publish the message (default: True)
msg_id: Message ID (auto-generated if not provided) msg_id: Message ID (auto-generated if not provided)
sender_id: Sender ID (auto-generated if not provided) sender_id: Sender ID (auto-generated if not provided)
Returns: Returns:
Tuple of (env, env_json_str) where: Tuple of (env, env_json_str) where:
- env: Dict containing all metadata and payloads - env: Dict containing all metadata and payloads
- env_json_str: JSON string for publishing to NATS - env_json_str: JSON string for transport
Example: Example:
>>> # Send text payload >>> # Send text payload
>>> env, env_json_str = msghandler.smartsend( >>> env, env_json_str = smartpack(
... "/chat", ... "/chat",
... [("message", "Hello!", "text")], ... [("message", "Hello!", "text")]
... broker_url="nats://localhost:4222"
... ) ... )
>>> >>>
>>> # Send dictionary payload >>> # Publish via your transport
>>> env, env_json_str = msghandler.smartsend( >>> # my_transport.publish("/chat", env_json_str)
... "/config",
... [("config", {"key": "value"}, "dictionary")],
... broker_url="nats://localhost:4222"
... )
>>>
>>> # Send binary payload (image, audio, video)
>>> env, env_json_str = msghandler.smartsend(
... "/media",
... [("image", image_bytes, "image")],
... broker_url="nats://localhost:4222"
... )
""" """
# Extract options with defaults # Extract options with defaults
correlation_id = kwargs.get('correlation_id', _generate_uuid()) correlation_id = kwargs.get('correlation_id', _generate_uuid())
@@ -403,7 +330,7 @@ def smartsend(subject, data, **kwargs):
is_publish = kwargs.get('is_publish', True) is_publish = kwargs.get('is_publish', True)
fileserver_upload_handler = kwargs.get('fileserver_upload_handler', _sync_fileserver_upload) fileserver_upload_handler = kwargs.get('fileserver_upload_handler', _sync_fileserver_upload)
log_trace(correlation_id, f"Starting smartsend for subject: {subject}") log_trace(correlation_id, f"Starting smartpack for subject: {subject}")
# Process payloads # Process payloads
payloads = [] payloads = []
@@ -463,11 +390,11 @@ def smartsend(subject, data, **kwargs):
return env, env_json_str return env, env_json_str
def smartreceive(msg, **kwargs): def smartunpack(msg, **kwargs):
""" """
Receive and process NATS message. Receive and process messages.
This function processes incoming NATS messages, handling both direct transport This function processes incoming messages, handling both direct transport
(base64 decoded payloads) and link transport (URL-based payloads). (base64 decoded payloads) and link transport (URL-based payloads).
It deserializes the data based on the transport type and returns the result. It deserializes the data based on the transport type and returns the result.
@@ -476,7 +403,7 @@ def smartreceive(msg, **kwargs):
Table type is not supported due to memory constraints. Table type is not supported due to memory constraints.
Args: Args:
msg: NATS message to process (can be string, dict, or object with 'payload' attribute) msg: Message to process (can be JSON string, dict, or object with 'payload'/'data' attribute)
fileserver_download_handler: Function to handle downloading data from file server URLs fileserver_download_handler: Function to handle downloading data from file server URLs
max_retries: Maximum retry attempts (default: 3) max_retries: Maximum retry attempts (default: 3)
base_delay: Initial delay in ms (default: 100) base_delay: Initial delay in ms (default: 100)
@@ -486,8 +413,11 @@ def smartreceive(msg, **kwargs):
Dict with envelope metadata and payloads field containing List[Tuple[str, Any, str]] Dict with envelope metadata and payloads field containing List[Tuple[str, Any, str]]
Example: Example:
>>> # Receive and process message >>> # Receive from JSON string
>>> env = msghandler.smartreceive(msg, fileserver_download_handler=_sync_fileserver_download) >>> env = smartunpack(json_string)
>>>
>>> # Receive from transport message object
>>> env = smartunpack(transport_msg, fileserver_download_handler=_sync_fileserver_download)
>>> # env is a Dict with "payloads" key containing List[Tuple[str, Any, str]] >>> # env is a Dict with "payloads" key containing List[Tuple[str, Any, str]]
>>> for dataname, data, type_ in env["payloads"]: >>> for dataname, data, type_ in env["payloads"]:
... print(f"{dataname}: {data} (type: {type_})") ... print(f"{dataname}: {data} (type: {type_})")
@@ -496,13 +426,19 @@ def smartreceive(msg, **kwargs):
if isinstance(msg, dict): if isinstance(msg, dict):
# Already parsed # Already parsed
env_json_obj = msg env_json_obj = msg
elif isinstance(msg, str):
# Raw JSON string
env_json_obj = json.loads(msg)
elif hasattr(msg, 'payload'): elif hasattr(msg, 'payload'):
# Object with payload attribute # Object with payload attribute
payload = msg.payload if isinstance(msg.payload, str) else msg.payload.decode('utf-8') payload = msg.payload if isinstance(msg.payload, str) else msg.payload.decode('utf-8')
env_json_obj = json.loads(payload) env_json_obj = json.loads(payload)
elif hasattr(msg, 'data'):
# Object with data attribute
payload = msg.data if isinstance(msg.data, str) else msg.data.decode('utf-8')
env_json_obj = json.loads(payload)
else: else:
# Assume it's already a JSON string or dict raise ValueError('Invalid message format: expected JSON string or message object')
env_json_obj = json.loads(msg) if isinstance(msg, str) else msg
correlation_id = env_json_obj['correlation_id'] correlation_id = env_json_obj['correlation_id']
log_trace(correlation_id, "Processing received message") log_trace(correlation_id, "Processing received message")
@@ -565,7 +501,7 @@ def smartreceive(msg, **kwargs):
class msghandler: class msghandler:
""" """
MicroPython NATS bridge implementation. MicroPython message bridge implementation.
This class provides a convenient interface for msghandler functionality, This class provides a convenient interface for msghandler functionality,
encapsulating the main functions and providing a class-based API. encapsulating the main functions and providing a class-based API.
@@ -588,40 +524,40 @@ class msghandler:
Initialize msghandler. Initialize msghandler.
Args: Args:
broker_url: NATS server URL (defaults to DEFAULT_BROKER_URL) broker_url: Broker URL (defaults to DEFAULT_BROKER_URL)
fileserver_url: HTTP file server URL (defaults to DEFAULT_FILESERVER_URL) fileserver_url: HTTP file server URL (defaults to DEFAULT_FILESERVER_URL)
""" """
self.broker_url = broker_url or self.DEFAULT_BROKER_URL self.broker_url = broker_url or self.DEFAULT_BROKER_URL
self.fileserver_url = fileserver_url or self.DEFAULT_FILESERVER_URL self.fileserver_url = fileserver_url or self.DEFAULT_FILESERVER_URL
def smartsend(self, subject, data, **kwargs): def smartpack(self, subject, data, **kwargs):
""" """
Send data via NATS. Send data.
Args: Args:
subject: NATS subject to publish to subject: Subject/topic to send to
data: List of (dataname, data, type) tuples data: List of (dataname, data, type) tuples
**kwargs: Additional options passed to smartsend **kwargs: Additional options passed to smartpack
Returns: Returns:
Tuple of (env, env_json_str) Tuple of (env, env_json_str)
""" """
kwargs['broker_url'] = kwargs.get('broker_url', self.broker_url) kwargs['broker_url'] = kwargs.get('broker_url', self.broker_url)
kwargs['fileserver_url'] = kwargs.get('fileserver_url', self.fileserver_url) kwargs['fileserver_url'] = kwargs.get('fileserver_url', self.fileserver_url)
return smartsend(subject, data, **kwargs) return smartpack(subject, data, **kwargs)
def smartreceive(self, msg, **kwargs): def smartunpack(self, msg, **kwargs):
""" """
Receive and process NATS message. Receive and process message.
Args: Args:
msg: NATS message to process msg: Message to process
**kwargs: Additional options passed to smartreceive **kwargs: Additional options passed to smartunpack
Returns: Returns:
Dict with envelope metadata and payloads Dict with envelope metadata and payloads
""" """
return smartreceive(msg, **kwargs) return smartunpack(msg, **kwargs)
# Convenience functions for module-level usage # Convenience functions for module-level usage
@@ -630,14 +566,14 @@ def send(subject, data, **kwargs):
Convenience function for sending data. Convenience function for sending data.
Args: Args:
subject: NATS subject to publish to subject: Subject/topic to send to
data: List of (dataname, data, type) tuples data: List of (dataname, data, type) tuples
**kwargs: Additional options **kwargs: Additional options
Returns: Returns:
Tuple of (env, env_json_str) Tuple of (env, env_json_str)
""" """
return smartsend(subject, data, **kwargs) return smartpack(subject, data, **kwargs)
def receive(msg, **kwargs): def receive(msg, **kwargs):
@@ -645,18 +581,18 @@ def receive(msg, **kwargs):
Convenience function for receiving messages. Convenience function for receiving messages.
Args: Args:
msg: NATS message to process msg: Message to process
**kwargs: Additional options **kwargs: Additional options
Returns: Returns:
Dict with envelope metadata and payloads Dict with envelope metadata and payloads
""" """
return smartreceive(msg, **kwargs) return smartunpack(msg, **kwargs)
__all__ = [ __all__ = [
'smartsend', 'smartpack',
'smartreceive', 'smartunpack',
'msghandler', 'msghandler',
'send', 'send',
'receive', 'receive',
@@ -664,10 +600,9 @@ __all__ = [
'DEFAULT_BROKER_URL', 'DEFAULT_BROKER_URL',
'DEFAULT_FILESERVER_URL', 'DEFAULT_FILESERVER_URL',
'MAX_PAYLOAD_SIZE', 'MAX_PAYLOAD_SIZE',
'NATSClient',
'_serialize_data', '_serialize_data',
'_deserialize_data', '_deserialize_data',
'log_trace', 'log_trace',
'_sync_fileserver_upload', '_sync_fileserver_upload',
'_sync_fileserver_download' '_sync_fileserver_download'
] ]

View File

@@ -1,12 +1,12 @@
/** /**
* JavaScript Mix Payloads Receiver Test * JavaScript Mix Payloads Receiver Test
* Tests the smartreceive function with mixed payload types * Tests the smartunpack function with mixed payload types
* *
* This test mirrors test_julia_mix_payloads_receiver.jl and demonstrates that * This test mirrors test_julia_mix_payloads_receiver.jl and demonstrates that
* any combination and any number of mixed content can be received correctly. * any combination and any number of mixed content can be received correctly.
*/ */
const msghandler = require('../src/msghandler.js'); const msghandler = require('../src/msghandler-csr.js');
const nats = require('nats'); const nats = require('nats');
const crypto = require('crypto'); const crypto = require('crypto');
@@ -50,8 +50,8 @@ async function runTest() {
console.log(`Received message on ${msg.subject}`); console.log(`Received message on ${msg.subject}`);
try { try {
// Process the message using smartreceive // Process the message using smartunpack
const envelope = await msghandler.smartreceive(msg, { const envelope = await msghandler.smartunpack(msg, {
fileserver_download_handler: msghandler.fetchWithBackoff, fileserver_download_handler: msghandler.fetchWithBackoff,
max_retries: 5, max_retries: 5,
base_delay: 100, base_delay: 100,

View File

@@ -1,12 +1,12 @@
/** /**
* JavaScript Mix Payloads Sender Test * JavaScript Mix Payloads Sender Test
* Tests the smartsend function with mixed payload types * Tests the smartpack function with mixed payload types
* *
* This test mirrors test_julia_mix_payloads_sender.jl and demonstrates that * This test mirrors test_julia_mix_payloads_sender.jl and demonstrates that
* any combination and any number of mixed content can be sent correctly. * any combination and any number of mixed content can be sent correctly.
*/ */
const msghandler = require('../src/msghandler.js'); const msghandler = require('../src/msghandler-csr.js').default;
const crypto = require('crypto'); const crypto = require('crypto');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
@@ -169,7 +169,7 @@ async function runTest() {
try { try {
// Send the message // Send the message
console.log('Sending mixed payloads...\n'); console.log('Sending mixed payloads...\n');
const [env, envJsonStr] = await msghandler.smartsend( const [env, envJsonStr] = await msghandler.smartpack(
TEST_SUBJECT, TEST_SUBJECT,
payloads, payloads,
{ {

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env julia #!/usr/bin/env julia
# Test script for mixed-content message testing # Test script for mixed-content message testing
# Tests receiving a mix of text, json, table, image, audio, video, and binary data # Tests receiving a mix of text, json, table, image, audio, video, and binary data
# from Julia serviceA to Julia serviceB using msghandler.jl smartreceive # from Julia serviceA to Julia serviceB using msghandler.jl smartunpack
# #
# This test demonstrates that any combination and any number of mixed content # This test demonstrates that any combination and any number of mixed content
# can be sent and received correctly. # can be sent and received correctly.
@@ -9,7 +9,7 @@
using NATS, JSON, UUIDs, Dates, PrettyPrinting, DataFrames, Arrow, HTTP, Base64 using NATS, JSON, UUIDs, Dates, PrettyPrinting, DataFrames, Arrow, HTTP, Base64
# Include the bridge module # Include the bridge module
include("../src/msghandler.jl") include("/home/ton/docker-apps/sommpanion/msghandler/src/msghandler.jl")
using .msghandler using .msghandler
# Configuration # Configuration
@@ -36,10 +36,10 @@ function test_mix_receive()
NATS.subscribe(conn, SUBJECT) do msg NATS.subscribe(conn, SUBJECT) do msg
log_trace("Received message on $(msg.subject)") log_trace("Received message on $(msg.subject)")
# Use msghandler.smartreceive to handle the data # Use msghandler.smartunpack to handle the data
# API: smartreceive(msg, download_handler; max_retries, base_delay, max_delay) # API: smartunpack(msg, download_handler; max_retries, base_delay, max_delay)
result = msghandler.smartreceive( result = msghandler.smartunpack(
msg; env_json_str;
max_retries = 5, max_retries = 5,
base_delay = 100, base_delay = 100,
max_delay = 5000 max_delay = 5000
@@ -245,7 +245,7 @@ println("Note: This receiver will wait for messages from the sender.")
println("Run test_julia_to_julia_mix_sender.jl first to send test data.") println("Run test_julia_to_julia_mix_sender.jl first to send test data.")
# Run receiver # Run receiver
println("\ntesting smartreceive for mixed content") println("\ntesting smartunpack for mixed content")
test_mix_receive() test_mix_receive()
println("\nTest completed.") println("\nTest completed.")

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env julia #!/usr/bin/env julia
# Test script for mixed-content message testing # Test script for mixed-content message testing
# Tests sending a mix of text, dictionary, arrowtable, jsontable, image, audio, video, and binary data # Tests sending a mix of text, dictionary, arrowtable, jsontable, image, audio, video, and binary data
# from Julia serviceA to Julia serviceB using msghandler.jl smartsend # from Julia serviceA to Julia serviceB using msghandler.jl smartpack
# #
# This test demonstrates that any combination and any number of mixed content # This test demonstrates that any combination and any number of mixed content
# can be sent and received correctly. # can be sent and received correctly.
@@ -14,13 +14,14 @@
using NATS, JSON, UUIDs, Dates, PrettyPrinting, DataFrames, Arrow, HTTP, Base64 using NATS, JSON, UUIDs, Dates, PrettyPrinting, DataFrames, Arrow, HTTP, Base64
# Include the bridge module # Include the bridge module
include("../src/msghandler.jl") include("/home/ton/docker-apps/sommpanion/msghandler/src/msghandler.jl")
using .msghandler using .msghandler
# Configuration # Configuration
const SUBJECT = "/msghandler" const SUBJECT = "/msghandler"
const NATS_URL = "nats.yiem.cc" const NATS_URL = "nats.yiem.cc"
const FILESERVER_URL = "http://192.168.88.104:8080" # const FILESERVER_URL = "http://192.168.88.104:8080"
const FILESERVER_URL = "https://fileserver.yiem.cc"
# Create correlation ID for tracing # Create correlation ID for tracing
correlation_id = string(uuid4()) correlation_id = string(uuid4())
@@ -166,7 +167,7 @@ function create_sample_data()
end end
# Sender: Send mixed content via smartsend # Sender: Send mixed content via smartpack
function test_mix_send() function test_mix_send()
# Create sample data # Create sample data
(text_data, dict_data, arrow_table_small, arrow_table_large, json_table_small, json_table_large, audio_data, large_audio_data, video_data, large_video_data, binary_data, large_binary_data) = create_sample_data() (text_data, dict_data, arrow_table_small, arrow_table_large, json_table_small, json_table_large, audio_data, large_audio_data, video_data, large_video_data, binary_data, large_binary_data) = create_sample_data()
@@ -203,8 +204,8 @@ function test_mix_send()
("binary_file_large", large_binary_data, "binary") ("binary_file_large", large_binary_data, "binary")
] ]
# Use smartsend with mixed content # Use smartpack with mixed content
sendinfo = msghandler.smartsend( env, env_json_str = msghandler.smartpack(
SUBJECT, SUBJECT,
payloads; # List of (dataname, data, type) tuples payloads; # List of (dataname, data, type) tuples
broker_url = NATS_URL, broker_url = NATS_URL,
@@ -218,10 +219,8 @@ function test_mix_send()
receiver_id = "", receiver_id = "",
reply_to = "", reply_to = "",
reply_to_msg_id = "", reply_to_msg_id = "",
is_publish = true # Publish the message to NATS
) )
env, env_json_str = sendinfo
log_trace("Sent message with $(length(env.payloads)) payloads") log_trace("Sent message with $(length(env.payloads)) payloads")
# Log transport type for each payload # Log transport type for each payload
@@ -243,6 +242,8 @@ function test_mix_send()
link_count = count(p -> p.transport == "link", env.payloads) link_count = count(p -> p.transport == "link", env.payloads)
log_trace("Direct transport: $direct_count payloads") log_trace("Direct transport: $direct_count payloads")
log_trace("Link transport: $link_count payloads") log_trace("Link transport: $link_count payloads")
return env_json_str
end end
@@ -251,8 +252,8 @@ println("Starting mixed-content transport test...")
println("Correlation ID: $correlation_id") println("Correlation ID: $correlation_id")
# Run sender # Run sender
println("start smartsend for mixed content") println("start smartpack for mixed content")
test_mix_send() env_json_str = test_mix_send()
println("\nTest completed.") println("\nTest completed.")
println("Note: Run test_julia_to_julia_mix_receiver.jl to receive the messages.") println("Note: Run test_julia_to_julia_mix_receiver.jl to receive the messages.")

View File

@@ -1,6 +1,6 @@
""" """
Python Mix Payloads Sender Test Python Mix Payloads Sender Test
Tests the smartsend function with mixed payload types Tests the smartpack function with mixed payload types
""" """
import asyncio import asyncio
@@ -11,7 +11,7 @@ import base64
# Add parent directory to path # Add parent directory to path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from msghandler import smartsend, DEFAULT_BROKER_URL, DEFAULT_FILESERVER_URL from msghandler import smartpack, DEFAULT_BROKER_URL, DEFAULT_FILESERVER_URL
TEST_SUBJECT = '/test/mix' TEST_SUBJECT = '/test/mix'
TEST_BROKER_URL = os.environ.get('NATS_URL', 'nats://localhost:4222') TEST_BROKER_URL = os.environ.get('NATS_URL', 'nats://localhost:4222')
@@ -56,7 +56,7 @@ async def run_test():
try: try:
# Send the message # Send the message
print('Sending mixed payloads...') print('Sending mixed payloads...')
env, env_json_str = await smartsend( env, env_json_str = await smartpack(
TEST_SUBJECT, TEST_SUBJECT,
test_data, test_data,
broker_url=TEST_BROKER_URL, broker_url=TEST_BROKER_URL,
@@ -164,7 +164,7 @@ async def run_test():
('audio', bytes([0x46, 0x4C, 0x41, 0x43]), 'audio') ('audio', bytes([0x46, 0x4C, 0x41, 0x43]), 'audio')
] ]
chat_env, _ = await smartsend( chat_env, _ = await smartpack(
TEST_SUBJECT, TEST_SUBJECT,
chat_data, chat_data,
broker_url=TEST_BROKER_URL, broker_url=TEST_BROKER_URL,