28 Commits

Author SHA1 Message Date
ton dfd81a9dbe update js client 2026-09-16 10:41:17 +07:00
ton edc528913e Merge pull request 'V1.5.0' (#11) from v1.5.0 into main
Reviewed-on: #11
2026-09-13 07:24:12 +00:00
ton 25db5c6b50 Merge pull request 'V1.5.0 add signurl support' (#10) from v1.5.0-add_signurl_support into v1.5.0
Reviewed-on: #10
2026-09-13 07:23:59 +00:00
ton 01540c0c8e bump version 2026-09-07 07:37:59 +07:00
ton ed51b30461 remove default fileserver url 2026-09-07 07:35:48 +07:00
ton ae5ab6160a Merge pull request 'v1.4.0' (#9) from v1.4.0 into main
Reviewed-on: #9
2026-08-29 03:25:59 +00:00
ton de850e5236 Merge pull request 'GeneralUtils v0.6.2' (#8) from v1.4.0-garageS3_support into v1.4.0
Reviewed-on: #8
2026-08-29 03:24:18 +00:00
ton ecef9a363f GeneralUtils v0.6.2 2026-08-29 10:00:22 +07:00
ton 431520f6ba Merge pull request 'V1.4.0' (#7) from v1.4.0 into main
Reviewed-on: #7
2026-08-28 10:35:31 +00:00
ton fd4cefe1f0 Merge pull request 'V1.4.0 garage s3 support' (#6) from v1.4.0-garageS3_support into v1.4.0
Reviewed-on: #6
2026-08-28 10:35:14 +00:00
ton 272c021665 update 2026-08-28 17:34:33 +07:00
ton 72b909a50c update 2026-08-28 08:11:35 +07:00
ton 139feb9e4f update 2026-08-28 07:55:16 +07:00
ton c293ec2181 update 2026-08-28 07:32:12 +07:00
ton 6ea0d12187 update 2026-08-28 07:31:29 +07:00
ton 3ddfbb73a9 version bump 2026-08-27 20:58:42 +07:00
ton 1c9d482310 update 2026-08-18 22:35:53 +07:00
ton 843d53587c turnoff log_trace 2026-07-09 09:29:07 +07:00
ton c0db3919a6 Merge pull request 'v1.2.1' (#5) from v1.2.1 into main
Reviewed-on: #5
2026-07-09 00:51:32 +00:00
ton b0b835d6b9 update 2026-07-09 07:50:45 +07:00
ton 775049ae5f update 2026-07-09 07:47:52 +07:00
ton 69ccb9990f update 2026-07-05 20:11:19 +07:00
ton a10e4ddb89 fix string error 2026-07-05 19:46:04 +07:00
ton 96e0105511 update version 2026-07-05 10:06:16 +07:00
ton 3bbd0a1d65 fix dependency 2026-06-26 11:21:18 +07:00
ton 60ece94fce update GeneralUtils version 2026-06-25 08:55:46 +07:00
ton a997d1d246 Merge pull request 'v1.2.0' (#4) from v1.2.0 into main
Reviewed-on: #4
2026-06-17 06:20:42 +00:00
ton d257175cdb Merge pull request 'v1.2.0-HTTP2' (#3) from v1.2.0-HTTP2 into v1.2.0
Reviewed-on: #3
2026-06-17 06:19:51 +00:00
16 changed files with 1588 additions and 3088 deletions
+2 -2
View File
@@ -507,8 +507,8 @@ What do you think?
treat ./src files as groundtruth (src/msghandler.jl is the most update one).
update ./docs files accordding to /home/ton/docker-apps/sommpanion/ASG_Framework/ASG_Framework.md
+432 -93
View File
@@ -1,8 +1,44 @@
# This file is machine-generated - editing it directly is not advised
julia_version = "1.12.6"
julia_version = "1.12.7"
manifest_format = "2.0"
project_hash = "866f6d0804412d52eacd6423616500484f0060f0"
project_hash = "1e0293861782da4b80ca02937a06ee14b1325efb"
[[deps.AWS]]
deps = ["Base64", "Compat", "Dates", "Downloads", "GitHub", "HTTP", "IniFile", "JSON", "MD5", "Mocking", "OrderedCollections", "Random", "SHA", "ScopedValues", "Sockets", "URIs", "UUIDs", "XMLDict"]
git-tree-sha1 = "842075b12a7f763c5f815a04037a99b6ac346df9"
uuid = "fbe9abb3-538b-5e4e-ba9e-bc94f4f92ebc"
version = "1.99.0"
[[deps.AWSS3]]
deps = ["AWS", "ArrowTypes", "Base64", "Compat", "Dates", "EzXML", "FilePathsBase", "HTTP", "MbedTLS", "Mocking", "OrderedCollections", "Retry", "SymDict", "URIs", "UUIDs", "XMLDict"]
git-tree-sha1 = "af99cea71364c8c4483405a6b03697d197ab4d3b"
uuid = "1c724243-ef5b-51ab-93f4-b0a88ac62a95"
version = "0.11.6"
[[deps.Accessors]]
deps = ["CompositionsBase", "ConstructionBase", "Dates", "InverseFunctions", "MacroTools"]
git-tree-sha1 = "7063ad1083578215c7c4bf410368150abe8d5524"
uuid = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697"
version = "0.1.45"
[deps.Accessors.extensions]
AxisKeysExt = "AxisKeys"
IntervalSetsExt = "IntervalSets"
LinearAlgebraExt = "LinearAlgebra"
StaticArraysExt = "StaticArrays"
StructArraysExt = "StructArrays"
TestExt = "Test"
UnitfulExt = "Unitful"
[deps.Accessors.weakdeps]
AxisKeys = "94b1ba4f-4ee9-5380-92f1-94cde586c3c5"
IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
[[deps.AliasTables]]
deps = ["PtrArrays", "Random"]
@@ -14,6 +50,12 @@ version = "1.1.3"
uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f"
version = "1.1.2"
[[deps.ArnoldiMethod]]
deps = ["LinearAlgebra", "Random", "StaticArrays"]
git-tree-sha1 = "d57bd3762d308bded22c3b82d033bff85f6195c6"
uuid = "ec485272-7323-5ecc-a04f-4719b315124d"
version = "0.4.0"
[[deps.Arrow]]
deps = ["ArrowTypes", "BitIntegers", "CodecLz4", "CodecZstd", "ConcurrentUtilities", "DataAPI", "Dates", "EnumX", "Mmap", "PooledArrays", "SentinelArrays", "StringViews", "Tables", "TimeZones", "TranscodingStreams", "UUIDs"]
git-tree-sha1 = "4a69a3eadc1f7da78d950d1ef270c3a62c1f7e01"
@@ -45,15 +87,20 @@ git-tree-sha1 = "6863c5b7fc997eadcabdbaf6c5f201dc30032643"
uuid = "e1450e63-4bb3-523b-b2a4-4ffa8c0fd77d"
version = "1.2.2"
[[deps.CEnum]]
git-tree-sha1 = "389ad5c84de1ae7cf0e28e381131c98ea87d54fc"
uuid = "fa961155-64e5-5f13-b03f-caf6b980ea82"
version = "0.5.0"
[[deps.CRC32c]]
uuid = "8bf52ea8-c179-5cab-976a-9e18b702a9bc"
version = "1.11.0"
[[deps.CSV]]
deps = ["CodecZlib", "Dates", "FilePathsBase", "InlineStrings", "Mmap", "Parsers", "PooledArrays", "PrecompileTools", "SentinelArrays", "Tables", "Unicode", "WeakRefStrings", "WorkerUtilities"]
git-tree-sha1 = "8d8e0b0f350b8e1c91420b5e64e5de774c2f0f4d"
git-tree-sha1 = "abed1e735dd4152f48c90cf0767e1790e25f332f"
uuid = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
version = "0.10.16"
version = "0.10.17"
[[deps.CodeTracking]]
deps = ["InteractiveUtils", "REPL", "UUIDs"]
@@ -75,9 +122,9 @@ version = "0.4.6"
[[deps.CodecZlib]]
deps = ["TranscodingStreams", "Zlib_jll"]
git-tree-sha1 = "962834c22b66e32aa10f7611c08c8ca4e20749a9"
git-tree-sha1 = "970758a3d591a2a5c2a907c53f2e2f8c1b1d3537"
uuid = "944b1d66-785c-5afd-91f1-9de20f533193"
version = "0.7.8"
version = "0.7.9"
[[deps.CodecZstd]]
deps = ["TranscodingStreams", "Zstd_jll"]
@@ -85,6 +132,12 @@ git-tree-sha1 = "da54a6cd93c54950c15adf1d336cfd7d71f51a56"
uuid = "6b39b394-51ab-5f42-8807-6242bab2b4c2"
version = "0.8.7"
[[deps.CommonSolve]]
deps = ["PrecompileTools"]
git-tree-sha1 = "6c389fa857f6ca5a95474b52a52023fd77f24cb7"
uuid = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2"
version = "0.2.14"
[[deps.Compat]]
deps = ["TOML", "UUIDs"]
git-tree-sha1 = "9d8a54ce4b17aa5bdce0ea5c34bc5e7c340d16ad"
@@ -103,18 +156,47 @@ version = "0.1.1"
[[deps.CompilerSupportLibraries_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae"
version = "1.3.0+1"
version = "1.3.1+2"
[[deps.CompositionsBase]]
git-tree-sha1 = "802bb88cd69dfd1509f6670416bd4434015693ad"
uuid = "a33af91c-f02d-484b-be07-31d278c5ca2b"
version = "0.1.2"
weakdeps = ["InverseFunctions"]
[deps.CompositionsBase.extensions]
CompositionsBaseInverseFunctionsExt = "InverseFunctions"
[[deps.ConcurrentUtilities]]
deps = ["Serialization", "Sockets"]
git-tree-sha1 = "21d088c496ea22914fe80906eb5bce65755e5ec8"
git-tree-sha1 = "3c9be947934c38475bafe822c6d61aaed17f0738"
uuid = "f0e56b4a-5159-44fe-b623-3e5288b988bb"
version = "2.5.1"
version = "2.6.0"
[[deps.ConstructionBase]]
git-tree-sha1 = "b4b092499347b18a015186eae3042f72267106cb"
uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9"
version = "1.6.0"
[deps.ConstructionBase.extensions]
ConstructionBaseIntervalSetsExt = "IntervalSets"
ConstructionBaseLinearAlgebraExt = "LinearAlgebra"
ConstructionBaseStaticArraysExt = "StaticArrays"
[deps.ConstructionBase.weakdeps]
IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
[[deps.Crayons]]
git-tree-sha1 = "249fe38abf76d48563e2f4556bebd215aa317e15"
git-tree-sha1 = "54b76cbb40d9a0f5368c880725b2f141da77c94f"
uuid = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f"
version = "4.1.1"
version = "4.2.0"
[[deps.DBInterface]]
git-tree-sha1 = "265d7f87f329c063cbde41920f9d00713a43cedd"
uuid = "a10d1c49-ce27-4219-8d33-6db1a4562965"
version = "2.7.0"
[[deps.DataAPI]]
git-tree-sha1 = "abe83f3a2f1b857aac70ef8b269080af17764bbe"
@@ -129,9 +211,9 @@ version = "1.8.2"
[[deps.DataStructures]]
deps = ["OrderedCollections"]
git-tree-sha1 = "6fb53a69613a0b2b68a0d12671717d307ab8b24e"
git-tree-sha1 = "b0bc6d2cad1fed8b7fd59a1551a991cb3d2809e6"
uuid = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
version = "0.19.5"
version = "0.19.6"
[[deps.DataValueInterfaces]]
git-tree-sha1 = "bfc1187b79289637fa0ef6d4436ebdfe6905cbd6"
@@ -143,20 +225,46 @@ deps = ["Printf"]
uuid = "ade2ca70-3891-5945-98fb-dc099432e06a"
version = "1.11.0"
[[deps.Decimals]]
git-tree-sha1 = "e98abef36d02a0ec385d68cd7dadbce9b28cbd88"
uuid = "abce61dc-4473-55a0-ba07-351d65e31d42"
version = "0.4.1"
[[deps.Distances]]
deps = ["LinearAlgebra", "Statistics", "StatsAPI"]
git-tree-sha1 = "c7e3a542b999843086e2f29dac96a618c105be1d"
uuid = "b4f34e82-e78d-54a5-968a-f98e89d6e8f7"
version = "0.10.12"
[deps.Distances.extensions]
DistancesChainRulesCoreExt = "ChainRulesCore"
DistancesSparseArraysExt = "SparseArrays"
[deps.Distances.weakdeps]
ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
[[deps.Distributed]]
deps = ["Random", "Serialization", "Sockets"]
uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b"
version = "1.11.0"
[[deps.Distributions]]
deps = ["AliasTables", "FillArrays", "LinearAlgebra", "PDMats", "Printf", "QuadGK", "Random", "SpecialFunctions", "Statistics", "StatsAPI", "StatsBase", "StatsFuns"]
git-tree-sha1 = "96f76dcd6cc75cf8eb49109123868499d413f526"
deps = ["AliasTables", "FillArrays", "LinearAlgebra", "PDMats", "Printf", "QuadGK", "Random", "Roots", "SpecialFunctions", "Statistics", "StatsAPI", "StatsBase", "StatsFuns"]
git-tree-sha1 = "a958ab3a40c755563f5e1405c0846cb0446bf19d"
uuid = "31c24e10-a181-5473-b8eb-7969acd0382f"
version = "0.25.126"
version = "0.25.131"
[deps.Distributions.extensions]
DistributionsChainRulesCoreExt = "ChainRulesCore"
DistributionsDensityInterfaceExt = "DensityInterface"
DistributionsSparseConnectivityTracerExt = "SparseConnectivityTracer"
DistributionsTestExt = "Test"
[deps.Distributions.weakdeps]
ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
DensityInterface = "b429d917-457f-4dbc-8f4c-0cc954292b1d"
SparseConnectivityTracer = "9f842d2f-2579-4b1d-911e-f412cf18a3f5"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[[deps.DocStringExtensions]]
@@ -175,33 +283,37 @@ uuid = "4e289a0a-7415-4d19-859d-a7e5c4648b56"
version = "1.0.7"
[[deps.ExprTools]]
git-tree-sha1 = "27415f162e6028e81c72b82ef756bf321213b6ec"
git-tree-sha1 = "d2e49e7efd29719d6f28b891b0e0e159daa9d2b4"
uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04"
version = "0.1.10"
version = "0.1.11"
[[deps.EzXML]]
deps = ["Printf", "XML2_jll"]
git-tree-sha1 = "7ea1aa5869e2626ccae84480e4f37185bc6f41d3"
uuid = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615"
version = "1.2.3"
[[deps.FilePathsBase]]
deps = ["Compat", "Dates"]
git-tree-sha1 = "3bab2c5aa25e7840a4b065805c0cdfc01f3068d2"
uuid = "48062228-2e41-5def-b9a4-89aafe57970f"
version = "0.9.24"
weakdeps = ["Mmap", "Test"]
[deps.FilePathsBase.extensions]
FilePathsBaseMmapExt = "Mmap"
FilePathsBaseTestExt = "Test"
[deps.FilePathsBase.weakdeps]
Mmap = "a63ad114-7e13-5084-954f-fe012c677804"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
[[deps.FileWatching]]
uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee"
version = "1.11.0"
[[deps.FillArrays]]
deps = ["LinearAlgebra"]
git-tree-sha1 = "2f979084d1e13948a3352cf64a25df6bd3b4dca3"
git-tree-sha1 = "5bad39456d9f0166184fce2248783dd9862645c1"
uuid = "1a297f60-69ca-5386-bcde-b61e274b549b"
version = "1.16.0"
version = "1.17.0"
weakdeps = ["PDMats", "SparseArrays", "StaticArrays", "Statistics"]
[deps.FillArrays.extensions]
FillArraysPDMatsExt = "PDMats"
@@ -209,30 +321,49 @@ version = "1.16.0"
FillArraysStaticArraysExt = "StaticArrays"
FillArraysStatisticsExt = "Statistics"
[deps.FillArrays.weakdeps]
PDMats = "90014a1f-27ba-587c-ab20-58faa44d9150"
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
[[deps.Future]]
deps = ["Random"]
uuid = "9fa8497b-333b-5362-9e8d-4d0656e87820"
version = "1.11.0"
[[deps.Gamma]]
deps = ["LogExpFunctions"]
git-tree-sha1 = "becc397f7cfb06e343496ae6ffb04818a851da51"
uuid = "a0844989-3bd2-4988-8bea-c9407ab0941b"
version = "1.2.0"
[[deps.GeneralUtils]]
deps = ["CSV", "DataFrames", "DataStructures", "Dates", "Distributions", "JSON", "NATS", "PrettyPrinting", "Random", "Revise", "SHA", "UUIDs"]
git-tree-sha1 = "8720a31344bc85ad610ae12f7e1247de22070765"
deps = ["AWS", "AWSS3", "CSV", "DataFrames", "DataStructures", "Dates", "Distributions", "Graphs", "HTTP", "JSON", "LibPQ", "NATS", "OrderedCollections", "PrettyPrinting", "Random", "Revise", "SHA", "StringDistances", "URIs", "UUIDs"]
git-tree-sha1 = "2ba657c0304923af99f426fbe3a1f281690ef56f"
repo-rev = "main"
repo-url = "https://git.yiem.cc/ton/GeneralUtils"
uuid = "c6c72f09-b708-4ac8-ac7c-2084d70108fe"
version = "0.3.2"
version = "0.6.2"
[[deps.GitHub]]
deps = ["Base64", "Dates", "HTTP", "JSON", "MbedTLS", "Sockets", "SodiumSeal", "URIs"]
git-tree-sha1 = "33de1bc5665494f0a9669fd6027e28180709d874"
uuid = "bc5e4493-9b4d-5f90-b8aa-2b2bcaad7a26"
version = "5.13.0"
[[deps.Graphs]]
deps = ["ArnoldiMethod", "DataStructures", "Inflate", "LinearAlgebra", "Random", "SimpleTraits", "SparseArrays", "Statistics"]
git-tree-sha1 = "7eb45fe833a5b7c51cf6d89c5a841d5967e44be3"
uuid = "86223c79-3864-5bf0-83f7-82e725a168b6"
version = "1.14.0"
[deps.Graphs.extensions]
GraphsSharedArraysExt = "SharedArrays"
[deps.Graphs.weakdeps]
Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
SharedArrays = "1a1011a3-84de-559e-8e89-a11a2f7dc383"
[[deps.HTTP]]
deps = ["Base64", "CodecZlib", "Dates", "EnumX", "PrecompileTools", "Random", "Reseau", "SHA", "URIs", "UUIDs", "Zlib_jll"]
git-tree-sha1 = "196e75bcb0a53cbf80eebe6326d69a86605c34d6"
git-tree-sha1 = "d6e390d1515836237b837fb7b6fca7ba1d752db6"
uuid = "cd3eb016-35fb-5094-929b-558a96fad6f3"
version = "2.3.0"
version = "2.6.5"
[[deps.HashArrayMappedTries]]
git-tree-sha1 = "2eaa69a7cab70a52b9687c8bf950a5a93ec895ae"
@@ -240,10 +371,32 @@ uuid = "076d061b-32b6-4027-95e0-9a2c6f6d7e74"
version = "0.2.0"
[[deps.HypergeometricFunctions]]
deps = ["LinearAlgebra", "OpenLibm_jll", "SpecialFunctions"]
git-tree-sha1 = "68c173f4f449de5b438ee67ed0c9c748dc31a2ec"
deps = ["Gamma", "LinearAlgebra"]
git-tree-sha1 = "31bb6c92405c084617facc1d7ed9eb6c402d061e"
uuid = "34004b35-14d8-5ef3-9330-4cdb6864b03a"
version = "0.3.28"
version = "0.3.30"
[[deps.ICU_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl"]
git-tree-sha1 = "b3d8be712fbf9237935bde0ce9b5a736ae38fc34"
uuid = "a51ab1cf-af8e-5615-a023-bc2c838bba6b"
version = "76.2.0+0"
[[deps.Infinity]]
deps = ["Dates", "Random", "Requires"]
git-tree-sha1 = "cf8234411cbeb98676c173f930951ea29dca3b23"
uuid = "a303e19e-6eb4-11e9-3b09-cd9505f79100"
version = "0.2.4"
[[deps.Inflate]]
git-tree-sha1 = "d1b1b796e47d94588b3757fe84fbf65a5ec4a80d"
uuid = "d25df0c9-e2be-5dd7-82c8-3ad0b3e990b9"
version = "0.1.5"
[[deps.IniFile]]
git-tree-sha1 = "f550e6e32074c939295eb5ea6de31849ac2c9625"
uuid = "83e8ac13-25f8-5344-8a64-a9f2b223428f"
version = "0.5.1"
[[deps.InlineStrings]]
git-tree-sha1 = "8f3d257792a522b4601c24a577954b0a8cd7334d"
@@ -260,6 +413,22 @@ deps = ["Markdown"]
uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
version = "1.11.0"
[[deps.Intervals]]
deps = ["ArrowTypes", "Dates", "Printf", "RecipesBase", "Serialization", "TimeZones"]
git-tree-sha1 = "d6fe00b123e32ddd17231b35d69a6394e696fd5a"
uuid = "d8418881-c3e1-53bb-8760-2df7ec849ed5"
version = "1.11.0"
[[deps.InverseFunctions]]
git-tree-sha1 = "a779299d77cd080bf77b97535acecd73e1c5e5cb"
uuid = "3587e190-3f89-42d0-90ee-14403ec27112"
version = "0.1.17"
weakdeps = ["Dates", "Test"]
[deps.InverseFunctions.extensions]
InverseFunctionsDatesExt = "Dates"
InverseFunctionsTestExt = "Test"
[[deps.InvertedIndices]]
git-tree-sha1 = "6da3c4316095de0f5ee2ebd875df8721e7e0bdbe"
uuid = "41ab1584-1d38-5bbf-9106-f11c6c58b48f"
@@ -270,6 +439,11 @@ git-tree-sha1 = "b2d91fe939cae05960e760110b328288867b5758"
uuid = "92d709cd-6900-40b7-9082-c6be49f344b6"
version = "0.2.6"
[[deps.IterTools]]
git-tree-sha1 = "42d5f897009e7ff2cf88db414a389e5ed1bdd023"
uuid = "c8e1da08-722c-5040-9ed9-7db0dc04731e"
version = "1.10.0"
[[deps.IteratorInterfaceExtensions]]
git-tree-sha1 = "a3f24677c21f5bbe9d2a714f95dcd58337fb2856"
uuid = "82899510-4779-5014-852e-03e436cf321d"
@@ -283,9 +457,9 @@ version = "1.8.0"
[[deps.JSON]]
deps = ["Dates", "Logging", "Parsers", "PrecompileTools", "StructUtils", "UUIDs", "Unicode"]
git-tree-sha1 = "c89d196f5ffb64bfbf80985b699ea913b0d2c211"
git-tree-sha1 = "c7345ab1a7ca4dc8a02c9f6510da0d9857bbe513"
uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
version = "1.6.1"
version = "1.7.1"
weakdeps = ["ArrowTypes"]
[deps.JSON.extensions]
@@ -303,19 +477,30 @@ weakdeps = ["ArrowTypes"]
[[deps.JuliaInterpreter]]
deps = ["CodeTracking", "InteractiveUtils", "Random", "UUIDs"]
git-tree-sha1 = "58927c485919bf17ea308d9d82156de1adf4b006"
git-tree-sha1 = "c3d401f110454b4ea24a76be33f6ee0d7d385103"
uuid = "aa1ae85d-cabe-5617-a682-6adf51b2e16a"
version = "0.10.12"
version = "0.11.4"
[[deps.JuliaSyntaxHighlighting]]
deps = ["StyledStrings"]
uuid = "ac6e5ff7-fb65-4e79-a425-ec3bc9c03011"
version = "1.12.0"
[[deps.Kerberos_krb5_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl"]
git-tree-sha1 = "0f2899fdadaab4b8f57db558ba21bdb4fb52f1f0"
uuid = "b39eb1a6-c29a-53d7-8c32-632cd16f18da"
version = "1.21.3+0"
[[deps.LaTeXStrings]]
git-tree-sha1 = "dda21b8cbd6a6c40d9d02a73230f9d70fed6918c"
git-tree-sha1 = "f88f3ccef05a6a72a0cf0ed417c8fd68530f4ab2"
uuid = "b964fa9f-0449-5b57-a5c2-d3ea65f4040f"
version = "1.4.0"
version = "1.4.1"
[[deps.LayerDicts]]
git-tree-sha1 = "6087ad3521d6278ebe5c27ae55e7bbb15ca312cb"
uuid = "6f188dcb-512c-564b-bc01-e0f76e72f166"
version = "1.0.0"
[[deps.LibCURL]]
deps = ["LibCURL_jll", "MozillaCACerts_jll"]
@@ -337,6 +522,18 @@ deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll"]
uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5"
version = "1.9.0+0"
[[deps.LibPQ]]
deps = ["CEnum", "DBInterface", "Dates", "Decimals", "DocStringExtensions", "FileWatching", "Infinity", "Intervals", "IterTools", "LayerDicts", "LibPQ_jll", "Libdl", "Memento", "OffsetArrays", "SQLStrings", "Tables", "TimeZones", "UTCDateTimes"]
git-tree-sha1 = "3d227cd13cbf1e9a54d7748dab33e078da6f9168"
uuid = "194296ae-ab2e-5f79-8cd4-7183a0a5a0d1"
version = "1.18.0"
[[deps.LibPQ_jll]]
deps = ["Artifacts", "ICU_jll", "JLLWrappers", "Kerberos_krb5_jll", "Libdl", "OpenSSL_jll", "Zstd_jll"]
git-tree-sha1 = "c692057e05ba6da348bc45d5dab8c7a2c88da518"
uuid = "08be9ffa-1c94-5ee5-a977-46a84ec9b350"
version = "16.14.0+0"
[[deps.LibSSH2_jll]]
deps = ["Artifacts", "Libdl", "OpenSSL_jll"]
uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8"
@@ -346,6 +543,12 @@ version = "1.11.3+1"
uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
version = "1.11.0"
[[deps.Libiconv_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl"]
git-tree-sha1 = "be484f5c92fad0bd8acfef35fe017900b0b73809"
uuid = "94ce4f54-9a6c-5748-9c1c-f9c7231a4531"
version = "1.18.0+0"
[[deps.LinearAlgebra]]
deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"]
uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
@@ -373,9 +576,9 @@ version = "1.11.0"
[[deps.LoweredCodeUtils]]
deps = ["CodeTracking", "Compiler", "JuliaInterpreter"]
git-tree-sha1 = "0aad96d7b987a5600e260eec50147b254d5ff7e6"
git-tree-sha1 = "1d4c737ab26f51ceed52ab2019c09b7660eb7440"
uuid = "6f1432cf-f94c-5a45-995e-cdbf5db27b0b"
version = "3.6.0"
version = "3.8.0"
[[deps.Lz4_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl"]
@@ -383,6 +586,17 @@ git-tree-sha1 = "191686b1ac1ea9c89fc52e996ad15d1d241d1e33"
uuid = "5ced341a-0733-55b8-9ab6-a4889d929147"
version = "1.10.1+0"
[[deps.MD5]]
deps = ["Random", "SHA"]
git-tree-sha1 = "1576f756617d31eb397a4a517b68562fd28dc2b4"
uuid = "6ac74813-4b46-53a4-afec-0b5dc9d7885c"
version = "0.2.3"
[[deps.MacroTools]]
git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522"
uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
version = "0.5.16"
[[deps.Markdown]]
deps = ["Base64", "JuliaSyntaxHighlighting", "StyledStrings"]
uuid = "d6f4376e-aef5-505a-96c1-9c027394607a"
@@ -400,6 +614,12 @@ git-tree-sha1 = "ff69a2b1330bcb730b9ac1ab7dd680176f5896b8"
uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1"
version = "2.28.1010+0"
[[deps.Memento]]
deps = ["Dates", "Distributed", "Requires", "Serialization", "Sockets", "Test", "UUIDs"]
git-tree-sha1 = "e03a25cb3b6569623f8246d3d8b3faa7ce86f4ad"
uuid = "f28f55f0-a522-5efc-85c2-fe41dfb9b2d9"
version = "1.5.0"
[[deps.Missings]]
deps = ["DataAPI"]
git-tree-sha1 = "ec4f7fbeab05d7747bdf98eb74d130a2a2ed298d"
@@ -428,14 +648,25 @@ version = "0.1.1"
[[deps.NanoDates]]
deps = ["Dates", "Parsers"]
git-tree-sha1 = "850a0557ae5934f6e67ac0dc5ca13d0328422d1f"
git-tree-sha1 = "77c7e98ca39aefb481f9b97a2f4f5c5471c08a1d"
uuid = "46f1a544-deae-4307-8689-c12aa3c955c6"
version = "1.0.3"
version = "1.1.0"
[[deps.NetworkOptions]]
uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908"
version = "1.3.0"
[[deps.OffsetArrays]]
git-tree-sha1 = "117432e406b5c023f665fa73dc26e79ec3630151"
uuid = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
version = "1.17.0"
[deps.OffsetArrays.extensions]
OffsetArraysAdaptExt = "Adapt"
[deps.OffsetArrays.weakdeps]
Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e"
[[deps.OpenBLAS_jll]]
deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"]
uuid = "4536629a-c528-5b80-bd46-f80d51c5b363"
@@ -449,7 +680,7 @@ version = "0.8.7+0"
[[deps.OpenSSL_jll]]
deps = ["Artifacts", "Libdl"]
uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95"
version = "3.5.4+0"
version = "3.5.6+0"
[[deps.OpenSpecFun_jll]]
deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl"]
@@ -458,15 +689,15 @@ uuid = "efe28fd5-8261-553b-a9e1-b2916fc3738e"
version = "0.5.6+0"
[[deps.OrderedCollections]]
git-tree-sha1 = "94ba93778373a53bfd5a0caaf7d809c445292ff4"
git-tree-sha1 = "05f45c2e0de6259db764adbfd2f1dc6d3f8de13c"
uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
version = "1.8.2"
version = "2.0.1"
[[deps.PDMats]]
deps = ["LinearAlgebra", "SparseArrays", "SuiteSparse"]
git-tree-sha1 = "e4cff168707d441cd6bf3ff7e4832bdf34278e4a"
git-tree-sha1 = "123266c25174ef6c8d4718920abc206452cf8de6"
uuid = "90014a1f-27ba-587c-ab20-58faa44d9150"
version = "0.11.37"
version = "0.11.41"
weakdeps = ["StatsBase"]
[deps.PDMats.extensions]
@@ -474,9 +705,9 @@ weakdeps = ["StatsBase"]
[[deps.Parsers]]
deps = ["Dates", "PrecompileTools", "UUIDs"]
git-tree-sha1 = "32a4e09c5f29402573d673901778a0e03b0807b9"
git-tree-sha1 = "3de8f5e6e90ebfa8d6d1f86997d6cdcd6a912ff3"
uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0"
version = "2.8.6"
version = "2.8.7"
[[deps.PooledArrays]]
deps = ["DataAPI", "Future"]
@@ -497,21 +728,23 @@ uuid = "21216c6a-2e73-6563-6e65-726566657250"
version = "1.5.2"
[[deps.PrettyPrinting]]
git-tree-sha1 = "142ee93724a9c5d04d78df7006670a93ed1b244e"
git-tree-sha1 = "0b7f4ad437e31c51cf5b91fb103579b04025170a"
uuid = "54e16d92-306c-5ea0-a30b-337be88ac337"
version = "0.4.2"
version = "0.4.3"
[[deps.PrettyTables]]
deps = ["Crayons", "LaTeXStrings", "Markdown", "PrecompileTools", "Printf", "REPL", "Reexport", "StringManipulation", "Tables"]
git-tree-sha1 = "624de6279ab7d94fc9f672f0068107eb6619732c"
git-tree-sha1 = "1b8aa19f229b1cea7fc93874a52e49db6a854450"
uuid = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"
version = "3.3.2"
version = "3.4.8"
[deps.PrettyTables.extensions]
PrettyTablesExcelExt = "XLSX"
PrettyTablesTypstryExt = "Typstry"
[deps.PrettyTables.weakdeps]
Typstry = "f0ed7684-a786-439e-b1e3-3b82803b501e"
XLSX = "fdbf4ff8-1666-58a4-91e7-1b58723a45e0"
[[deps.Printf]]
deps = ["Unicode"]
@@ -545,29 +778,44 @@ deps = ["SHA"]
uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
version = "1.11.0"
[[deps.RecipesBase]]
deps = ["PrecompileTools"]
git-tree-sha1 = "5c3d09cc4f31f5fc6af001c250bf1278733100ff"
uuid = "3cdcf5f2-1ef4-517c-9805-6587b60abb01"
version = "1.3.4"
[[deps.Reexport]]
git-tree-sha1 = "45e428421666073eab6f2da5c9d310d99bb12f9b"
uuid = "189a3867-3050-52da-a836-e630ba90ab69"
version = "1.2.2"
[[deps.Reseau]]
deps = ["NetworkOptions", "OpenSSL_jll", "PrecompileTools", "Random", "SHA"]
git-tree-sha1 = "0eab6d95ed40c2ef3992255c1c71e4f9748932b5"
uuid = "802f3686-a58f-41ce-bb0c-3c43c75bba36"
[[deps.Requires]]
deps = ["UUIDs"]
git-tree-sha1 = "62389eeff14780bfe55195b7204c0d8738436d64"
uuid = "ae029012-a4dd-5104-9daa-d747884805df"
version = "1.3.1"
[[deps.Reseau]]
deps = ["NetworkOptions", "OpenSSL_jll", "PrecompileTools", "Random", "SHA"]
git-tree-sha1 = "aaee74fab545e9af4b21d10c2203c93c636c4ae0"
uuid = "802f3686-a58f-41ce-bb0c-3c43c75bba36"
version = "1.4.1"
[[deps.Retry]]
git-tree-sha1 = "41ac127cd281bb33e42aba46a9d3b25cd35fc6d5"
uuid = "20febd7b-183b-5ae2-ac4a-720e7ce64774"
version = "0.4.1"
[[deps.Revise]]
deps = ["CRC32c", "CodeTracking", "FileWatching", "InteractiveUtils", "JuliaInterpreter", "LibGit2", "LoweredCodeUtils", "OrderedCollections", "Preferences", "REPL", "UUIDs"]
git-tree-sha1 = "27e3ee13fc8739a59b380d6163d6a82f52c03bd7"
deps = ["CRC32c", "CodeTracking", "FileWatching", "JuliaInterpreter", "LibGit2", "LoweredCodeUtils", "OrderedCollections", "Preferences", "REPL", "UUIDs"]
git-tree-sha1 = "f6ad10ad2fa1063c2ae0f75d17d92d1d0cae4c77"
uuid = "295af30f-e4ad-537b-8983-00126c2a3abe"
version = "3.15.1"
version = "3.16.5"
weakdeps = ["Distributed"]
[deps.Revise.extensions]
DistributedExt = "Distributed"
[deps.Revise.weakdeps]
Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
[[deps.Rmath]]
deps = ["Random", "Rmath_jll"]
git-tree-sha1 = "5b3d50eb374cea306873b371d3f8d3915a018f0b"
@@ -576,14 +824,41 @@ version = "0.9.0"
[[deps.Rmath_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl"]
git-tree-sha1 = "58cdd8fb2201a6267e1db87ff148dd6c1dbd8ad8"
git-tree-sha1 = "6d40b2fe70437b01397d2a4d5b020008da4e7019"
uuid = "f50d1b31-88e8-58de-be2c-1cc44531875f"
version = "0.5.1+0"
version = "0.5.2+0"
[[deps.Roots]]
deps = ["Accessors", "CommonSolve", "Printf"]
git-tree-sha1 = "13a9e0164267bb9ebc55c1c5d72fceea8f09555b"
uuid = "f2b01f46-fcfa-551c-844a-d8ac1e96c665"
version = "3.0.7"
[deps.Roots.extensions]
RootsChainRulesCoreExt = "ChainRulesCore"
RootsForwardDiffExt = "ForwardDiff"
RootsIntervalRootFindingExt = "IntervalRootFinding"
RootsSymPyExt = "SymPy"
RootsSymPyPythonCallExt = "SymPyPythonCall"
RootsUnitfulExt = "Unitful"
[deps.Roots.weakdeps]
ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
IntervalRootFinding = "d2bf35a9-74e0-55ec-b149-d360ff49b807"
SymPy = "24249f21-da20-56a4-8eb1-6a02cf4ae2e6"
SymPyPythonCall = "bc8888f7-b21e-4b7c-a06a-5d9c9496438c"
Unitful = "1986cc42-f94f-5a68-af5c-568840ba703d"
[[deps.SHA]]
uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce"
version = "0.7.0"
[[deps.SQLStrings]]
git-tree-sha1 = "55de0530689832b1d3d43491ee6b67bd54d3323c"
uuid = "af517c2e-c243-48fa-aab8-efac3db270f5"
version = "0.1.0"
[[deps.ScopedValues]]
deps = ["HashArrayMappedTries", "Logging"]
git-tree-sha1 = "67a144433c4ce877ee6d1ada69a124d6b1ecf7be"
@@ -606,6 +881,12 @@ version = "1.4.10"
uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
version = "1.11.0"
[[deps.SimpleTraits]]
deps = ["InteractiveUtils", "MacroTools"]
git-tree-sha1 = "7ddb0b49c109481b046972c0e4ab02b2127d6a75"
uuid = "699a6c99-e7fa-54fc-8d76-47d257e15c1d"
version = "0.9.6"
[[deps.Sockets]]
uuid = "6462fe0b-24de-5631-8697-dd941f90decc"
version = "1.11.0"
@@ -616,11 +897,17 @@ git-tree-sha1 = "907703e0d50846f300650d7225bdcab145b7bca9"
uuid = "4f5b5e99-b0ad-42cd-b47a-334e172ec8bd"
version = "1.1.2"
[[deps.SodiumSeal]]
deps = ["Base64", "Libdl", "libsodium_jll"]
git-tree-sha1 = "80cef67d2953e33935b41c6ab0a178b9987b1c99"
uuid = "2133526b-2bfb-4018-ac12-889fb3908a75"
version = "0.1.1"
[[deps.SortingAlgorithms]]
deps = ["DataStructures"]
git-tree-sha1 = "64d974c2e6fdf07f8155b5b2ca2ffa9069b608d9"
git-tree-sha1 = "13cd91cc9be159e3f4d95b857fa2aa383b53772a"
uuid = "a2af1166-a08f-5f64-846c-94a0d3cef48c"
version = "1.2.2"
version = "1.2.3"
[[deps.SparseArrays]]
deps = ["Libdl", "LinearAlgebra", "Random", "Serialization", "SuiteSparse_jll"]
@@ -629,9 +916,9 @@ version = "1.12.0"
[[deps.SpecialFunctions]]
deps = ["IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"]
git-tree-sha1 = "6547cbdd8ce32efba0d21c5a40fa96d1a3548f9f"
git-tree-sha1 = "429071b23f4c9a13fb6582f807cc2ef454082408"
uuid = "276daf66-3868-5448-9aa4-cd146d93841b"
version = "2.8.0"
version = "2.9.0"
[deps.SpecialFunctions.extensions]
SpecialFunctionsChainRulesCoreExt = "ChainRulesCore"
@@ -639,6 +926,25 @@ version = "2.8.0"
[deps.SpecialFunctions.weakdeps]
ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
[[deps.StaticArrays]]
deps = ["LinearAlgebra", "PrecompileTools", "Random", "StaticArraysCore"]
git-tree-sha1 = "fac51faf3bb96e8bc0bf6f9f39ca4955652776bb"
uuid = "90137ffa-7385-5640-81b9-e52037218182"
version = "1.9.19"
[deps.StaticArrays.extensions]
StaticArraysChainRulesCoreExt = "ChainRulesCore"
StaticArraysStatisticsExt = "Statistics"
[deps.StaticArrays.weakdeps]
ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
[[deps.StaticArraysCore]]
git-tree-sha1 = "6ab403037779dae8c514bad259f32a447262455a"
uuid = "1e83bf80-4336-4d27-bf5d-d5a4f845583c"
version = "1.4.4"
[[deps.Statistics]]
deps = ["LinearAlgebra"]
git-tree-sha1 = "ae3bb1eb3bba077cd276bc5cfc337cc65c3075c0"
@@ -657,15 +963,15 @@ version = "1.8.0"
[[deps.StatsBase]]
deps = ["AliasTables", "DataAPI", "DataStructures", "IrrationalConstants", "LinearAlgebra", "LogExpFunctions", "Missings", "Printf", "Random", "SortingAlgorithms", "SparseArrays", "Statistics", "StatsAPI"]
git-tree-sha1 = "c6f18e5a52a176a383f6f6c635e0f81feed1d6d4"
git-tree-sha1 = "adb9da019510162e67a4493fc235c23203d8b09e"
uuid = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
version = "0.34.11"
version = "0.34.13"
[[deps.StatsFuns]]
deps = ["HypergeometricFunctions", "IrrationalConstants", "LogExpFunctions", "Reexport", "Rmath", "SpecialFunctions"]
git-tree-sha1 = "3f4e1d24289cd974e089c617b1472311a2b1feab"
git-tree-sha1 = "91a5737baed20ee31f3faea0e51f57461f6a689e"
uuid = "4c63d2b9-4356-54db-8cca-17b64c39e42c"
version = "2.1.0"
version = "2.2.1"
[deps.StatsFuns.extensions]
StatsFunsChainRulesCoreExt = "ChainRulesCore"
@@ -675,11 +981,17 @@ version = "2.1.0"
ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
InverseFunctions = "3587e190-3f89-42d0-90ee-14403ec27112"
[[deps.StringDistances]]
deps = ["Distances", "StatsAPI"]
git-tree-sha1 = "456f343c7a6c8f52d8b501c0da2a05538ab1b907"
uuid = "88034a9c-02f8-509d-84a9-84ec65e18404"
version = "1.0.1"
[[deps.StringManipulation]]
deps = ["PrecompileTools"]
git-tree-sha1 = "d05693d339e37d6ab134c5ab53c29fce5ee5d7d5"
git-tree-sha1 = "773065c6e0e903924a9d838259be74338422aef2"
uuid = "892a3eda-7b42-436c-8928-eab12a02cf0e"
version = "0.4.4"
version = "0.5.0"
[[deps.StringViews]]
git-tree-sha1 = "f2dcb92855b31ad92fe8f079d4f75ac57c93e4b8"
@@ -694,9 +1006,9 @@ version = "1.11.0"
[[deps.StructUtils]]
deps = ["Dates", "UUIDs"]
git-tree-sha1 = "82bee338d650aa515f31866c460cb7e3bcef90b8"
git-tree-sha1 = "2d0fc55c61321ba245c47be599570d11bac50303"
uuid = "ec057cc2-7a8d-4b58-b3b3-92acb9f63b42"
version = "2.8.2"
version = "2.8.5"
[deps.StructUtils.extensions]
StructUtilsMeasurementsExt = ["Measurements"]
@@ -721,6 +1033,12 @@ deps = ["Artifacts", "Libdl", "libblastrampoline_jll"]
uuid = "bea87d4a-7f5b-5778-9afe-8cc45184846c"
version = "7.8.3+2"
[[deps.SymDict]]
deps = ["Test"]
git-tree-sha1 = "0108ccdaea3ef69d9680eeafc8d5ad198b896ec8"
uuid = "2da68c74-98d7-5633-99d6-8493888d7b1e"
version = "0.3.0"
[[deps.TOML]]
deps = ["Dates"]
uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
@@ -728,9 +1046,9 @@ version = "1.0.3"
[[deps.TZJData]]
deps = ["Artifacts"]
git-tree-sha1 = "72df96b3a595b7aab1e101eb07d2a435963a97e2"
git-tree-sha1 = "d3b30a9f29a898e21fb4bdf280101b7a12e1f39c"
uuid = "dc5dba14-91b3-4cab-a142-028a31da12f7"
version = "1.5.0+2025b"
version = "1.5.1+2025b"
[[deps.TableTraits]]
deps = ["IteratorInterfaceExtensions"]
@@ -740,30 +1058,39 @@ version = "1.0.1"
[[deps.Tables]]
deps = ["DataAPI", "DataValueInterfaces", "IteratorInterfaceExtensions", "OrderedCollections", "TableTraits"]
git-tree-sha1 = "f2c1efbc8f3a609aadf318094f8fc5204bdaf344"
git-tree-sha1 = "a94d9bdda1b7bed0046cea645639ab3f62196fac"
uuid = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
version = "1.12.1"
version = "1.14.0"
[[deps.Test]]
deps = ["InteractiveUtils", "Logging", "Random", "Serialization"]
uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
version = "1.11.0"
[[deps.TimeZones]]
deps = ["Artifacts", "Dates", "Downloads", "InlineStrings", "Mocking", "Printf", "Scratch", "TZJData", "Unicode", "p7zip_jll"]
git-tree-sha1 = "d422301b2a1e294e3e4214061e44f338cafe18a2"
uuid = "f269a46b-ccf7-5d73-abea-4c690281aa53"
version = "1.22.2"
weakdeps = ["RecipesBase"]
[deps.TimeZones.extensions]
TimeZonesRecipesBaseExt = "RecipesBase"
[deps.TimeZones.weakdeps]
RecipesBase = "3cdcf5f2-1ef4-517c-9805-6587b60abb01"
[[deps.TranscodingStreams]]
git-tree-sha1 = "0c45878dcfdcfa8480052b6ab162cdd138781742"
uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa"
version = "0.11.3"
[[deps.URIs]]
git-tree-sha1 = "bef26fb046d031353ef97a82e3fdb6afe7f21b1a"
git-tree-sha1 = "908fec9df6c5de98548ead82a468c95ccf6cd263"
uuid = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
version = "1.7.0"
[[deps.UTCDateTimes]]
deps = ["Dates", "TimeZones"]
git-tree-sha1 = "4af3552bf0cf4a071bf3d14bd20023ea70f31b62"
uuid = "0f7cfa37-7abf-4834-b969-a8aa512401c2"
version = "1.6.1"
[[deps.UUIDs]]
@@ -786,6 +1113,18 @@ git-tree-sha1 = "cd1659ba0d57b71a464a29e64dbc67cfe83d54e7"
uuid = "76eceee3-57b5-4d4a-8e66-0e911cebbf60"
version = "1.6.1"
[[deps.XML2_jll]]
deps = ["Artifacts", "JLLWrappers", "Libdl", "Libiconv_jll", "Zlib_jll"]
git-tree-sha1 = "3f3315d89fc954a28f5b471bce698ed6e27481be"
uuid = "02c8fc9c-b97f-50b9-bbe4-9be30ff0a78a"
version = "2.15.3+0"
[[deps.XMLDict]]
deps = ["EzXML", "IterTools", "OrderedCollections"]
git-tree-sha1 = "8e8cdc9c93e1f442eba471b56db6f86ffeb1a33a"
uuid = "228000da-037f-5747-90a9-8195ccbf91a5"
version = "0.4.2"
[[deps.Zlib_jll]]
deps = ["Libdl"]
uuid = "83775a58-1f1d-513f-b197-d71354ab007a"
@@ -809,10 +1148,10 @@ uuid = "a9144af2-ca23-56d9-984f-0d03f7b5ccf8"
version = "1.0.21+0"
[[deps.msghandler]]
deps = ["Arrow", "Base64", "DataFrames", "Dates", "GeneralUtils", "HTTP", "JSON", "NATS", "PrettyPrinting", "Revise", "UUIDs"]
deps = ["Arrow", "Base64", "DataFrames", "Dates", "HTTP", "JSON", "NATS", "PrettyPrinting", "UUIDs"]
path = "."
uuid = "f2724d33-f338-4a57-b9f8-1be882570d10"
version = "0.5.6"
version = "1.4.0"
[[deps.nghttp2_jll]]
deps = ["Artifacts", "Libdl"]
+2 -3
View File
@@ -1,6 +1,6 @@
name = "msghandler"
uuid = "f2724d33-f338-4a57-b9f8-1be882570d10"
version = "0.5.6"
version = "1.5.0"
authors = ["narawat <narawat@gmail.com>"]
[deps]
@@ -13,11 +13,10 @@ HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
NATS = "55e73f9c-eeeb-467f-b4cc-a633fde63d2a"
PrettyPrinting = "54e16d92-306c-5ea0-a30b-337be88ac337"
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
[compat]
Base64 = "1.11.0"
Dates = "1.11.0"
GeneralUtils = "0.3.2"
GeneralUtils = "0.6.2"
JSON = "1.4.0"
+2 -2
View File
@@ -342,7 +342,7 @@ NATS.publish(conn, "test.topic", message_envelope_json_str; reply_to="test.reply
NATS.drain(conn)
# OR using request-reply pattern
reply = NATS.request(conn, "test.topic", message_envelope_json_str, timeout=10)
reply = NATS.request(conn, "test.topic", message_envelope_json_str; timeout=10)
```
#### Receive Messages (Publish - Subscribe Pattern)
@@ -506,7 +506,7 @@ message_envelope, message_envelope_json_str = msghandler.smartpack(
broker_url::String = "nats://localhost:4222",
fileserver_url = "http://localhost:8080",
fileserver_upload_handler::Function = plik_oneshot_upload,
size_threshold::Int = 500_000,
size_threshold::Int = 10_000,
correlation_id::String = string(uuid4()),
msg_purpose::String = "chat",
sender_name::String = "msghandler",
+71 -73
View File
@@ -1,45 +1,47 @@
# Requirements Document: msghandler
**Version**: 1.3.0
**Date**: 2026-05-22
**Version**: 1.4.1
**Date**: 2026-09-16
**Status**: Active
**Ground Truth**: [`src/msghandler.jl`](../src/msghandler.jl)
**ASG Framework Alignment**: Structured per [`ASG_Framework/ASG_Framework.md`](../../ASG_Framework/ASG_Framework.md) (Eight Pillars) — user stories (`US-XXX`), functional/non-functional requirements with traceable IDs, success metrics & KPIs, boundary definitions, security, observability, and acceptance conditions.
**Implementation Status**:
- **Julia** ([`src/msghandler.jl`](../src/msghandler.jl)): ground truth, active.
- **Python, JavaScript (CSR), Rust, MicroPython**: implementations present in [`src/`](../src) but not yet tested.
---
## 1. Business Context & Success Metrics
### 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 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.
msghandler is a cross-platform, bi-directional data bridge that enables seamless communication between **Julia**, **JavaScript**, **Python**, **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 (>10KB) 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)
| Story | Priority | Acceptance Criteria |
|-------|----------|---------------------|
| **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 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 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 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 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 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 custom HTTP file servers | P2 | Handler function abstraction allows plugging in AWS S3 or custom implementations |
| **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 |
| ID | User Story | Acceptance Criteria |
|----|------------|---------------------|
| **US-001** | As a **Julia developer**, I want to send text messages to JavaScript/Python applications on a server or browser **so that** end users get text help across platforms | Text messages are serialized, encoded, and received correctly across platforms |
| **US-002** | As a **Python developer**, I want to send tabular data to Julia applications **so that** DataFrame exchange works across platforms | DataFrame exchange works with both Arrow IPC and JSON formats |
| **US-003** | As a **JavaScript developer** (server or browser), I want to send large files (>10KB) to other applications **so that** large payloads are transferred efficiently | Large files are automatically uploaded to file server and URLs are sent via the transport layer |
| **US-004** | As a **MicroPython developer**, I want to send sensor data with minimal memory usage **so that** constrained devices can exchange data | Direct transport works for payloads <100KB on memory-constrained devices |
| **US-005** | As a **Rust developer**, I want to send and receive messages with type-safe APIs **so that** type safety and async connectivity are guaranteed | Rust implementation uses serde for serialization, tokio for async, and transport-agnostic client for connectivity |
| **US-006** | As a **developer**, I want to send mixed-content messages (text + image + file) **so that** rich messages are handled appropriately | msghandler accepts list of (dataname, data, type) tuples and handles each payload appropriately |
| **US-007** | As a **developer**, I want to receive multi-payload messages **so that** all payloads are returned with correct types preserved | msghandler returns payloads as list of tuples with correct types preserved |
| **US-008** | As a **developer**, I want to use Plik as the file server **so that** large uploads use one-shot mode with ID/token handling | Plik one-shot upload mode is supported with upload ID and token handling |
| **US-009** | As a **developer**, I want to use custom HTTP file servers **so that** the file backend is pluggable (AWS S3, custom) | Handler function abstraction allows plugging in AWS S3 or custom implementations |
| **US-010** | As a **developer**, I want automatic retry on file server download failures **so that** transient failures recover automatically | Exponential backoff with configurable retries (default: 5, base_delay: 100ms, max_delay: 5000ms) |
| **US-011** | As a **developer**, I want message tracing across distributed systems **so that** message flow is observable end-to-end | Correlation ID is propagated through all message processing steps |
### 1.3 Success Metrics & KPIs
**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
- **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
- **FR-003** (Large file handling): 99% successful file uploads to server for payloads ≥0.5MB - Measured via integration tests
- **FR-004** (Direct transport for small payloads): 100% of payloads <0.5MB use direct transport - Measured via transport selection tests
- **FR-003** (Large file handling): 99% successful file uploads to server for payloads ≥10KB - Measured via integration tests
- **FR-004** (Direct transport for small payloads): 100% of payloads <10KB use direct transport - Measured via transport selection tests
- **FR-005** (MicroPython support): 100% of payloads <100KB delivered on MicroPython devices - Measured via MicroPython integration tests
- **FR-006** (Multi-payload messages): 100% correct parsing of multi-payload message lists - Measured via multi-payload tests
- **FR-007** (Payload type preservation): 100% type integrity preserved across all platforms - Measured via type validation tests
@@ -55,8 +57,8 @@ msghandler is a cross-platform, bi-directional data bridge that enables seamless
- **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-104** (File upload latency): <1s for 10KB file - Measured via integration tests
- **NFR-105** (File download latency): <1s for 10KB 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
@@ -80,8 +82,8 @@ msghandler is a cross-platform, bi-directional data bridge that enables seamless
| Feature | Description |
|---------|-------------|
| 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 |
| Cross-platform interoperability | Seamless data exchange between Julia, JavaScript, Python, Rust, and MicroPython |
| Intelligent transport selection | Direct transport (<10KB) vs Link transport (≥10KB) based on payload size |
| Unified API | Consistent `smartpack()` and `smartunpack()` functions across all platforms |
| Multi-payload support | List of (dataname, data, type) tuples with appropriate handling |
| File server integration | Plik one-shot upload and custom HTTP server support |
@@ -107,12 +109,9 @@ msghandler is a cross-platform, bi-directional data bridge that enables seamless
| Julia | Arrow.jl | Latest stable |
| Julia | HTTP.jl | Latest stable |
| Julia | UUIDs.jl | Latest stable |
| Node.js | node-fetch | Latest stable |
| Python | aiohttp | Latest stable |
| Python | pyarrow | Latest stable |
| Browser | - | Transport-agnostic (caller provides) |
| Dart | http | Latest stable |
| Dart | uuid | Latest stable |
| Rust | serde | Latest stable |
| Rust | serde_json | Latest stable |
| Rust | tokio | Latest stable |
@@ -123,10 +122,8 @@ msghandler is a cross-platform, bi-directional data bridge that enables seamless
| Platform | Minimum Version | Notes |
|----------|-----------------|-------|
| Julia | 1.7+ | Arrow.jl required for arrowtable support |
| Node.js | 16+ | Transport client required, Arrow IPC supported |
| Python | 3.8+ | pyarrow required for arrowtable support |
| Browser | Latest | No Arrow IPC (uses jsontable only) |
| Dart | 2.17+ | Supports Desktop (Dart SDK), Flutter (Dart SDK), and Web (Dart SDK) |
| Rust | 1.70+ | Full support with async/await, Arrow IPC on desktop |
| MicroPython | 1.19+ | Limited to direct transport |
@@ -134,22 +131,22 @@ msghandler is a cross-platform, bi-directional data bridge that enables seamless
## 3. Functional Requirements (FR)
| ID | Requirement | Description |
|----|-------------|-------------|
| **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-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 transport without file server upload |
| **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-007** | Payload type preservation | System shall preserve payload types when returning multi-payload messages |
| **FR-008** | Plik file server integration | System shall support Plik one-shot upload mode with upload ID and token handling |
| **FR-009** | Custom file server support | System shall provide handler function abstraction for custom HTTP file server implementations |
| **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-012** | Message serialization | System shall serialize data types using Base64, JSON, or Arrow IPC encoding |
| **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** | Transport subscription | System shall receive and process messages by accepting JSON string from transport payload |
| ID | Requirement | Description | Traceability |
|----|-------------|-------------|--------------|
| **FR-001** | Cross-platform text messaging | System shall allow users to send text messages between Julia, JavaScript, Python, and MicroPython applications | US-001 |
| **FR-002** | Cross-platform tabular data | System shall support DataFrame exchange between Julia and Python applications using Arrow IPC format | US-002 |
| **FR-003** | Large file handling | System shall automatically detect payloads ≥10KB and upload them to HTTP file server instead of sending via transport | US-003 |
| **FR-004** | Direct transport for small payloads | System shall send payloads <10KB directly via transport without file server upload | US-001, US-003, US-004 |
| **FR-005** | MicroPython support | System shall support payloads <100KB on MicroPython devices using direct transport | US-004 |
| **FR-006** | Multi-payload messages | System shall accept and process lists of (dataname, data, type) tuples | US-006, US-007 |
| **FR-007** | Payload type preservation | System shall preserve payload types when returning multi-payload messages | US-006, US-007 |
| **FR-008** | Plik file server integration | System shall support Plik one-shot upload mode with upload ID and token handling | US-008 |
| **FR-009** | Custom file server support | System shall provide handler function abstraction for custom HTTP file server implementations | US-009 |
| **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 | US-010 |
| **FR-011** | Correlation ID propagation | System shall propagate correlation IDs through all message processing steps | US-011 |
| **FR-012** | Message serialization | System shall serialize data types using Base64, JSON, or Arrow IPC encoding | US-001, US-002, US-006 |
| **FR-013** | Transport publishing | System shall return JSON string representation for caller to publish via transport layer (caller is responsible for actual transport publish) | US-006 |
| **FR-014** | Transport subscription | System shall receive and process messages by accepting JSON string from transport payload | US-007 |
---
@@ -167,8 +164,8 @@ msghandler is a cross-platform, bi-directional data bridge that enables seamless
| **NFR-101** | Message serialization overhead | <50ms for 10KB payload | <50ms for 10KB payload | Benchmark tests |
| **NFR-102** | Message deserialization overhead | <50ms for 10KB payload | <50ms for 10KB payload | Benchmark tests |
| **NFR-103** | Transport connection establishment | <100ms | <100ms average | Connection pool benchmarks |
| **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 | <1s for 0.5MB file | Integration tests |
| **NFR-104** | File upload latency | <1s for 10KB file | <1s for 10KB file | Integration tests |
| **NFR-105** | File download latency | <1s for 10KB file | <1s for 10KB file | Integration tests |
| **NFR-106** | Concurrent connections | Support 100+ simultaneous transport connections | 100+ simultaneous connections | Scale 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 | Horizontal scaling verified | Architecture review |
@@ -183,6 +180,11 @@ msghandler is a cross-platform, bi-directional data bridge that enables seamless
### 4.3 Privacy & Security
- **Data Classification**: Payloads carry binary artifacts (image/audio/video) and tabular/text/dictionary data; classified as internal/commercial. msghandler itself stores or transmits no PHI/PII.
- **Encryption**: TLS 1.3+ for transport connections (NFR-302) and file server HTTPS (NFR-303). Payload-body encryption is an application-layer concern and out of scope.
- **Authentication**: File server uploads are authenticated via the Plik `X-UploadToken` (NFR-303); transport/broker authentication is provided by the caller's implementation.
- **Compliance**: No built-in compliance controls; msghandler relies on the transport layer and application layer for GDPR/SOC2/HIPAA as required by the caller.
| ID | Requirement | Specification | KPI | Test Method |
|----|-------------|---------------|-----|-------------|
| **NFR-301** | Payload integrity | SHA-256 checksum support via metadata | 100% SHA-256 checksum validation | Integrity tests |
@@ -222,16 +224,16 @@ msghandler is a cross-platform, bi-directional data bridge that enables seamless
### 6.1 Supported Payload Types
| Type | Julia | JavaScript | Python | Dart | MicroPython | Description |
|------|-------|------------|--------|------|-------------|-------------|
| `text` | `String` | `string` | `str` | `String` | `String` | `str` | Plain text strings |
| `dictionary` | `Dict`, `NamedTuple` | `Object`, `Array` | `dict`, `list` | `Map`, `serde_json::Value` | `String` | `dict` | JSON-serializable data |
| `arrowtable` | `DataFrame`, `Arrow.Table` | ❌ (Browser), ✅ (Node.js) | `pandas.DataFrame` | `List<Map>` (Desktop), `List<dynamic>` (Flutter) | `arrow2::Table` | ❌ | Tabular data (Arrow IPC) |
| `jsontable` | `Vector{NamedTuple}` | `Array<Object>` | `list[dict]` | `Vec<Map>` | ⚠️ | Tabular data (JSON) - **Only table type in Browser** |
| `image` | `Vector{UInt8}` | `Uint8Array`, `Buffer` | `bytes` | `Uint8List` | `Vec<u8>` | `bytearray` | Image binary data |
| `audio` | `Vector{UInt8}` | `Uint8Array`, `Buffer` | `bytes` | `Uint8List` | `Vec<u8>` | `bytearray` | Audio binary data |
| `video` | `Vector{UInt8}` | `Uint8Array`, `Buffer` | `bytes` | `Uint8List` | `Vec<u8>` | `bytearray` | Video binary data |
| `binary` | `Vector{UInt8}`, `IOBuffer` | `Uint8Array`, `Buffer` | `bytes`, `bytearray` | `Uint8List` | `Vec<u8>` | `bytearray` | Generic binary data |
| Type | Julia | JavaScript | Python | MicroPython | Description |
|------|-------|------------|--------|-------------|-------------|
| `text` | `String` | `string` | `str` | `String` | Plain text strings |
| `dictionary` | `Dict`, `NamedTuple` | `Object`, `Array` | `dict`, `list` | `dict` | JSON-serializable data |
| `arrowtable` | `DataFrame`, `Arrow.Table` | ❌ (Browser) | `pandas.DataFrame` | ❌ | Tabular data (Arrow IPC) |
| `jsontable` | `Vector{NamedTuple}` | `Array<Object>` | `list[dict]` | ❌ | Tabular data (JSON) - **Only table type in Browser** |
| `image` | `Vector{UInt8}` | `Uint8Array`, `Buffer` | `bytes` | `bytes` | Image binary data |
| `audio` | `Vector{UInt8}` | `Uint8Array`, `Buffer` | `bytes` | `bytes` | Audio binary data |
| `video` | `Vector{UInt8}` | `Uint8Array`, `Buffer` | `bytes` | `bytes` | Video binary data |
| `binary` | `Vector{UInt8}`, `IOBuffer` | `Uint8Array`, `Buffer` | `bytes`, `bytearray` | `bytes`, `bytearray` | Generic binary data |
### 6.2 Encoding Requirements
@@ -251,11 +253,8 @@ msghandler is a cross-platform, bi-directional data bridge that enables seamless
| Platform | Threshold | Notes |
|----------|-----------|-------|
| Desktop (Julia/JS/Python/Dart) | 0.5MB | Default size threshold |
| Dart Desktop | 0.5MB | Default size threshold |
| Dart Flutter | 0.5MB | Default size threshold |
| Dart Web | 0.5MB | Default size threshold |
| Rust | 0.5MB | Default size threshold |
| Desktop (Julia/JS/Python) | 10KB | Default size threshold |
| Rust | 10KB | Default size threshold |
| MicroPython | 100KB | Lower threshold for memory constraints |
### 7.2 Maximum Payload Size
@@ -263,9 +262,6 @@ msghandler is a cross-platform, bi-directional data bridge that enables seamless
| Platform | Maximum | Notes |
|----------|---------|-------|
| Desktop | Unlimited | Limited by transport server configuration |
| Dart Desktop | Unlimited | Limited by transport server configuration |
| Dart Flutter | Unlimited | Limited by transport server configuration |
| Dart Web | Unlimited | Limited by transport server configuration |
| Rust | Unlimited | Limited by transport server configuration |
| MicroPython | 50KB | Hard limit due to 256KB-1MB memory |
@@ -406,7 +402,7 @@ function smartunpack(
|-----------|---------|-------|
| Message Broker | 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 | 50MB | Desktop platforms (Julia/JS/Python) |
| Client Memory | 256KB | MicroPython devices |
### 12.2 Environment Variables
@@ -415,7 +411,7 @@ function smartunpack(
|----------|---------|-------------|
| `BROKER_URL` | `ws://localhost:4222` | Message broker URL |
| `FILESERVER_URL` | `http://localhost:8080` | HTTP file server URL |
| `SIZE_THRESHOLD` | `500000` | Size threshold in bytes (0.5MB) |
| `SIZE_THRESHOLD` | `10000` | Size threshold in bytes (10KB) |
---
@@ -431,7 +427,7 @@ function smartunpack(
| Version | Supported Platforms |
|---------|---------------------|
| v1.0.x | Julia 1.7+, Node.js 16+, Python 3.8+, Dart 2.17+, Rust 1.70+, Browser (latest), MicroPython 1.19+ |
| v1.0.x | Julia 1.7+, Python 3.8+, Rust 1.70+, Browser (latest), MicroPython 1.19+ |
---
@@ -439,6 +435,10 @@ function smartunpack(
| Date | Version | Changes |
|------|---------|---------|
| 2026-09-16 | 1.4.0 | Structured per ASG Framework Eight Pillars |
| - | - | Added `US-XXX` user story IDs and FR→US traceability in sections 1.2 and 3 |
| - | - | Added Data Classification / Encryption / Authentication / Compliance bullets under NFR-301–303 |
| - | - | Added ASG-alignment and implementation-status notes (Julia ground truth) |
| 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 |
@@ -453,7 +453,7 @@ function smartunpack(
| - | - | 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-008/FR-009 to include file path upload overload |
| - | - | Updated SIZE_THRESHOLD env var default to 500000 |
| - | - | Updated SIZE_THRESHOLD env var default to 10000 |
| 2026-03-23 | 1.0.0 | Updated to ASG Framework requirements structure |
---
@@ -501,10 +501,8 @@ function smartunpack(
## 16. References
- [`src/msghandler.jl`](../src/msghandler.jl) - Ground truth implementation (Julia)
- [`src/msghandler_ssr.js`](../src/msghandler_ssr.js) - Server-side JavaScript implementation
- [`src/msghandler_csr.js`](../src/msghandler_csr.js) - Client-side JavaScript implementation
- [`src/msghandler.py`](../src/msghandler.py) - Python implementation
- [`src/msghandler.dart`](../src/msghandler.dart) - Dart implementation
- [`src/msghandler_mpy.py`](../src/msghandler_mpy.py) - MicroPython implementation
- [`src/msghandler.rs`](../src/msghandler.rs) - Rust implementation
- [`README.md`](../README.md) - Project overview
@@ -1,16 +1,16 @@
# Solution Design: msghandler
**Version**: 1.3.0
**Date**: 2026-05-22
**Version**: 1.4.0
**Date**: 2026-09-16
**Status**: Active
**Ground Truth**: [`src/msghandler.jl`](../src/msghandler.jl)
**ASG Framework Alignment**: v8 pillars - Requirements → Solution Design → Specification → Walkthrough → Implementation Plan → Validation → Runbook
**ASG Framework Alignment**: Eight Pillars — Requirements → Solution Design → Walkthrough → Specification → UI Specification → Validation → Implementation Plan → 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.
msghandler addresses the challenge of cross-platform data exchange between **Julia**, **JavaScript**, **Python**, **Rust**, and **MicroPython** applications using a message broker as the transport layer.
### User Problems
@@ -44,7 +44,7 @@ msghandler addresses the challenge of cross-platform data exchange between **Jul
| 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-002 | Automatic Transport Selection | <10KB = direct, ≥10KB = 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 | - |
@@ -56,7 +56,9 @@ msghandler addresses the challenge of cross-platform data exchange between **Jul
## 2. Solution Approach
msghandler implements a **Claim-Check pattern** with intelligent transport selection:
**What we deliver**: two tiny, symmetric functions — `smartpack()` and `smartunpack()` — that every platform calls the same way. A developer passes a list of `(dataname, data, type)` tuples and receives them back on the other side with types intact, without ever choosing how the data moves. The framework decides, per payload, whether to send it inline or hand it to a file server, so callers never write transport, encoding, retry, or routing code themselves.
The high-level flow:
```
Sender (smartpack) Transport Layer Receiver (smartunpack)
@@ -66,17 +68,27 @@ Sender (smartpack) Transport Layer Receiver (smartunpa
│ [(name, │ JSON │ Message │ JSON │ 2. Check transport│
│ data, type)]│ format │ Broker │ format │ 3. Fetch/Decode │
│ │ │ (NATS/MQTT/ │ │ 4. Return tuples │
└─────────────────┘ │ WebSocket) │ │ │
│ │ └───────────────────┘
└─────────────────┘ │ WebSocket) │ └───────────────────┘
└───────────────┘
```
### Key Design Decisions
**Key Principles**:
- **Unified API** — one `smartpack()` / `smartunpack()` signature on every platform (SD-004)
- **Automatic transport selection** — the caller never picks direct vs. link; payload size decides (SD-002)
- **Claim-Check for large payloads** — only metadata + a download URL cross the transport for big data (SD-001)
- **Type preservation** — `(dataname, data, type)` tuples keep types intact end-to-end (SD-004)
- **Transport-agnostic** — works over NATS, MQTT, or WebSocket with no caller changes (SD-006)
---
## 3. Decision Rationale
The design reasoning behind each major decision — why the solution was built this way, and how each decision connects to form a cohesive approach that solves the user problem.
| 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-001** | Claim-Check Pattern | Large payloads (>10KB) 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 | <10KB = direct (fast), ≥10KB = 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 |
@@ -84,56 +96,7 @@ Sender (smartpack) Transport Layer Receiver (smartunpa
| **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 |
**How the decisions connect**: SD-004 (unified tuple format) gives every platform the same input contract; SD-005 (base64) makes those payloads cross any JSON transport; SD-001/SD-002 decide, per payload, whether it travels inline or via a file-server link; SD-003 makes the file server swappable; SD-006 keeps the whole thing transport-agnostic; SD-007 protects the link path against transient file-server outages; and SD-008 threads a correlation ID through every step so the distributed flow remains observable.
---
@@ -203,92 +166,17 @@ flowchart TD
---
## 5. Decision Rationale
## 5. Alternatives Considered
### 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
| 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 | 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 |
---
@@ -324,7 +212,7 @@ flowchart TD
|-------------------|-------------|--------|-------------|
| **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 |
| **File server latency** | SD-001, SD-002 | NFR-104, NFR-105 | <1s upload/download for 10KB 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 |
@@ -344,9 +232,9 @@ flowchart TD
| 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 |
| **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 (P-001..P-006) mapped to solution components with SD-XXX decision IDs and requirement ID references; the two-function API explains *how* cross-platform exchange works, not just that it does |
| **Solution Design → Specification** | Does every SD-XXX decision appear as a traceable technical rule in the specification? | ✅ Verified - SD-001..SD-008 referenced in [`4-specification.md`](4-specification.md) |
| **Solution Design → Walkthrough** | Does every SD-XXX decision appear in at least one participant's walkthrough step with a WT-XXX ID? | ✅ Verified - SD-001..SD-008 traced in [`3-walkthrough.md`](3-walkthrough.md) |
### Solution Design Validation
@@ -359,14 +247,14 @@ flowchart TD
- **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
1. **SD-001** - Claim-Check pattern - large payloads uploaded to file server, small payloads sent directly
2. **SD-002** - Automatic transport selection - transport chosen by 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
4. **SD-004** - Unified 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
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 ✅
@@ -377,8 +265,8 @@ flowchart TD
**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)
3. Large file (≥10KB) → link transport → file server upload (SD-001)
4. Small payload (<10KB) → direct transport → base64 encoding (SD-005)
5. Receiver calls `smartunpack()` → receives same tuple format with preserved types
**NFR Traceability**:
+737
View File
@@ -0,0 +1,737 @@
# Walkthrough: msghandler
**Version**: 1.7.0
**Date**: 2026-09-16
**Status**: Active
**Ground Truth**: [`src/msghandler.jl`](../src/msghandler.jl)
**ASG Framework Alignment**: Eight pillars — Requirements → Solution Design → Walkthrough → Specification → UI Specification → Validation → Implementation Plan → Runbook
---
## Structural Guidance
This walkthrough follows the ASG Framework walkthrough guideline: one section per system **participant**, each tracing that participant's complete journey from session start to task completion. Every major step carries a `WT-XXX` id and cites the `SD-XXX` decision(s) and `FR-XXX` requirement(s) it implements. Where a participant's action triggers or depends on another participant, a cross-reference is included.
- **Overview sequence diagram** — participant section headers each start with the participant's high-level flow.
- **Detailed sequence diagrams** — per scenario, showing request/response and transport selection.
- **State machine** — the direct→link transport decision (multi-participant state).
- **Error paths** — each participant section covers the failure modes that participant sees.
**Participants**: JavaScript (browser/frontend), Julia (backend — ground truth), Python (backend), Rust (service), MicroPython (IoT device).
---
## 0. Overview: The Big Picture (all participants)
msghandler delivers two tiny, symmetric functions that **every participant calls the same way**: `smartpack()` (send) and `smartunpack()` (receive). A developer passes a list of `(dataname, data, type)` tuples and receives them back on the other side with types intact, without ever choosing how the data moves. Per payload, msghandler decides whether to send it inline (direct) or hand it to a file server (link).
```
Sender (smartpack) Transport Layer Receiver (smartunpack)
┌─────────────────┐ ┌───────────────┐ ┌───────────────────┐
│ 1. Data tuples │────────────>│ │───────────>│ 1. Parse envelope │
│ [(name, │ JSON │ Message │ JSON │ 2. Check transport│
│ data, type)]│ format │ Broker │ format │ 3. Fetch/Decode │
└─────────────────┘ │ (NATS/MQTT/ │ │ 4. Return tuples │
│ WebSocket) │ └───────────────────┘
└───────────────┘
```
### 0.1 Transport Layer Pattern (shared across all participants)
**Critical**: msghandler is **transport-agnostic**. The caller is responsible for:
1. **Sending side**: call `smartpack()` → receive `(envelope, json_string)` → publish the JSON string via transport
2. **Receiving side**: subscribe to transport → receive JSON string → call `smartunpack()`
```mermaid
flowchart LR
subgraph msghandler["msghandler Module (all participants)"]
direction TB
S1["smartpack(data)"]
S2["Returns (envelope, json_string)"]
R1["smartunpack(json_string)"]
R2["Returns payloads (tuples)"]
end
subgraph Transport["Transport Layer (Caller's Responsibility)"]
direction TB
PUBLISH["Publish JSON string"]
SUBSCRIBE["Subscribe to messages"]
end
S1 --> S2
S2 --> PUBLISH
SUBSCRIBE --> R1
R1 --> R2
PUBLISH --> SUBSCRIBE
style msghandler fill:#b3e5fc,stroke:#0288d1
style Transport fill:#ffe0b2,stroke:#f57c00
```
### 0.2 Per-Payload Transport Decision (state machine — spans all participants)
`smartpack()` serializes each payload first, then compares the serialized size against `size_threshold`. This per-payload decision is the single state transition shared by every participant.
```mermaid
stateDiagram-v2
[*] --> Serialize: smartpack((name, data, type))
Serialize --> Direct: size < size_threshold (base64 inline)
Serialize --> Link: size >= size_threshold (upload to file server)
Direct --> Envelope: encoding = base64/json/arrow-ipc
Link --> Envelope: encoding = none/json/arrow-ipc, data = URL
Envelope --> Publish: envelope_to_json() -> caller publishes
Publish --> Transport: JSON string over broker
Transport --> Unpack: smartunpack(json_string)
Unpack --> DirectData: direct -> decode + deserialize
Unpack --> LinkData: link -> download + deserialize
DirectData --> Return: (name, data, type)
LinkData --> Return: (name, data, type)
Return --> [*]
```
**Key Design Principles** (from `2-solution_design.md`):
| Principle | Description | Rationale |
|-----------|-------------|-----------|
| **Claim-Check Pattern** | Large payloads uploaded to HTTP server, URL sent via transport | Transport has message size limits; avoids overflow (`SD-001`) |
| **Automatic Transport Selection** | Direct (< threshold) vs Link (≥ threshold) based on size | Optimizes memory vs network I/O (`SD-002`) |
| **Unified Tuple Format** | Same `(dataname, data, type)` format across all platforms | Platform-agnostic I/O (`SD-004`) |
| **Base64 Encoding** | JSON-compatible binary data transport | Cross-transport compatibility (`SD-005`) |
| **Handler Abstraction** | Pluggable file server (Plik / AWS S3 / custom) | No vendor lock-in (`SD-003`) |
| **Transport Abstraction** | Works over NATS / MQTT / WebSocket | No caller changes (`SD-006`) |
| **Exponential Backoff** | Retry failed downloads | Handles transient failures (`SD-007`) |
| **Correlation ID Propagation** | One id threads through every step | Distributed tracing (`SD-008`) |
**Supported Transports**: NATS (WebSocket/TCP), MQTT (WebSocket/TCP), WebSocket (direct HTTP), HTTP (polling/long-polling). msghandler only handles **serialization/deserialization**; the caller owns transport.
**Supported Payload Types** (`FR-006`, `FR-007`): `text`, `dictionary`, `arrowtable`, `jsontable`, `image`, `audio`, `video`, `binary`.
---
## 1. JavaScript (Browser / Frontend)
This section traces the journey from the browser's point of view: it starts a session, sends mixed content (text + image), receives a reply, and handles a large-file upload via link transport.
> Cross-references: the browser's request is consumed by the **Julia** backend (see [Section 2](#2-julia-backend-ground-truth)) and the browser's reply is produced there. The browser also broadcasts to other clients (see [Section 4](#4-rust-service) / [Section 3](#3-python-backend) receive the same topic).
### 1.1 Session Start
**WT-001**: The browser loads the frontend module and establishes a transport connection to the broker.
> Cross-reference: `2-solution_design.md:SD-006` (transport abstraction over NATS/WebSocket).
**Sequence**:
```mermaid
sequenceDiagram
participant User as Browser (JS)
participant JS as msghandler.js
participant T as Transport (NATS/WebSocket)
User->>JS: load frontend
JS->>T: connect({ servers: "ws://localhost:4222" })
T-->>JS: connected
JS->>T: subscribe("/agent/wine/api/v1/response")
T-->>JS: subscription active
```
### 1.2 Send Mixed Payloads (Chat)
**WT-002**: The browser calls `smartpack()` with a mixed payload (text message + avatar image).
```javascript
// JavaScript (Browser) - WT-002
const [env, msgJson] = await msghandler.smartpack(
"/agent/wine/api/v1/prompt",
[
["msg", "Hello! I'm Ton.", "text"],
["avatar", avatarImageData, "image"]
],
{
broker_url: "ws://localhost:4222",
receiver_name: "agent-backend",
msg_purpose: "chat"
}
);
```
> Traceability: `FR-006` (multi-payload), `FR-007` (type preservation), `FR-001` (cross-platform text), `SD-004` (unified tuple format), `SD-005` (base64), `SD-008` (correlation id auto-generated).
**WT-002.1**: For each payload, msghandler serializes and selects transport by size.
| Payload | Size | Transport | Encoding | Traceability |
|---------|------|-----------|----------|--------------|
| `"msg"` (text) | ~20 bytes | direct | base64 | `SD-002`, `SD-005` |
| `"avatar"` (image) | ~2 KB | direct | base64 | `SD-002`, `SD-005` |
```mermaid
sequenceDiagram
participant User as Browser
participant JS as msghandler.js
participant E as Envelope
User->>JS: smartpack([msg, avatar])
JS->>JS: _serialize_data() per payload (SD-004)
JS->>JS: size < threshold? (SD-002)
JS->>E: build envelope (SD-005, SD-008)
E-->>JS: (env, msgJson)
```
**Rationale**: Text is smaller and sent via direct transport (fast, no file server). The avatar stays direct because it is under the threshold. `smartpack()` returns `(env, msgJson)` — **the browser must publish** (`FR-013`).
**WT-003**: The browser publishes the JSON string via the transport.
```javascript
// JavaScript (Browser) - WT-003
const conn = await transportClient.connect({ servers: "ws://localhost:4222" });
await conn.publish("/agent/wine/api/v1/prompt", msgJson);
```
> Cross-reference: this publish triggers the **Julia** backend (see [Section 2, WT-010](#2-julia-backend-ground-truth)). `SD-006`.
### 1.3 Envelope Shape (what crosses the wire)
**WT-004**: The published envelope matches the ground-truth `msg_envelope_v1` JSON shape in [`src/msghandler.jl`](../src/msghandler.jl:274).
```json
{
"correlation_id": "a1b2c3d4...",
"msg_id": "e5f6g7h8...",
"timestamp": "2026-09-16T02:08:27.000Z",
"send_to": "/agent/wine/api/v1/prompt",
"msg_purpose": "chat",
"sender_name": "browser-frontend",
"sender_id": "browser-uuid...",
"receiver_name": "agent-backend",
"receiver_id": "",
"reply_to": "/agent/wine/api/v1/response",
"reply_to_msg_id": "",
"broker_url": "ws://localhost:4222",
"metadata": {},
"payloads": [
{
"id": "payload-uuid...",
"dataname": "msg",
"payload_type": "text",
"transport": "direct",
"encoding": "base64",
"size": 20,
"data": "SGVsbG8hIEknIHRlbCB5b3UgSW4gZW5nbGlzaC4=",
"metadata": { "payload_bytes": 20 }
},
{
"id": "payload-uuid...",
"dataname": "avatar",
"payload_type": "image",
"transport": "direct",
"encoding": "base64",
"size": 2048,
"data": "iVBORw0KGgo=",
"metadata": { "payload_bytes": 2048 }
}
]
}
```
> Traceability: `FR-011` (correlation id), `FR-013` (publish), `SD-008`, `SD-005`.
### 1.4 Receive Reply
**WT-005**: The browser receives the backend reply on its `reply_to` topic and deserializes payloads.
```javascript
// JavaScript (Browser) - WT-005
const conn = await transportClient.connect({ servers: "ws://localhost:4222" });
conn.subscribe("/agent/wine/api/v1/response", async (msg) => {
const payloads = await msghandler.smartunpack(String(msg.payload));
// payloads = [
// ["response", "Hello Ton! I'm the AI assistant.", "text"],
// ["generated_image", binaryData, "image"]
// ];
renderResponse(payloads);
});
```
> Traceability: `FR-014` (subscription), `FR-007` (type preservation), `SD-004`, `SD-007`.
### 1.5 Error Paths (browser POV)
**WT-006**: Large file (≥ threshold) triggers link transport; upload fails.
```mermaid
sequenceDiagram
participant User as Browser
participant JS as msghandler.js
participant FS as Plik File Server
participant T as Transport
User->>JS: smartpack([dataset], size_threshold)
JS->>JS: size >= threshold -> link (SD-001)
JS->>FS: plik_oneshot_upload()
FS-->>JS: 500 Upload Failed
JS->>JS: throw "Failed to upload data to fileserver"
User->>User: surface upload error / fall back to smaller payloads
```
> Traceability: `FR-003` (large file handling), `FR-008` (Plik), `SD-001`, `SD-003`. Recovery: fall back to a smaller payload or direct transport.
**WT-007**: Transport connection lost before publish — the caller's transport client handles auto-reconnect; msghandler has already returned `(env, msgJson)`, so the caller retries the publish.
> Traceability: `NFR-203` (connection recovery).
---
## 2. Julia (Backend — Ground Truth)
This section traces the Julia backend, which is the reference implementation in [`src/msghandler.jl`](../src/msghandler.jl). It receives the browser's chat message, processes it, sends a reply, and downloads a large file via link transport.
> Cross-references: consumes the browser publish (see [Section 1, WT-003](#13-envelope-shape-what-crosses-the-wire)); produces the browser reply (see [Section 1, WT-005](#15-receive-reply)). Also the topic is broadcast to **Python** and **Rust** subscribers (see [Section 3](#3-python-backend), [Section 4](#4-rust-service)).
### 2.1 Session Start
**WT-010**: The Julia backend subscribes to the topic and is ready to `smartunpack()`.
> Cross-reference: `2-solution_design.md:SD-006` (transport abstraction).
### 2.2 Receive & Unpack Chat Message
**WT-011**: The backend receives the JSON string from the transport and calls `smartunpack()`.
```julia
# Julia (Backend) - WT-011
transport_msg = transport_subscription.next()
env = smartunpack(String(transport_msg.payload))
# env["payloads"] is now a Vector of tuples:
# [("msg", "Hello! I'm Ton.", "text"),
# ("avatar", UInt8[...], "image")]
```
> Traceability: `FR-014` (subscription), `FR-001` (text), `FR-006` (multi-payload), `FR-007` (type preservation), `SD-004`, `SD-007`.
**WT-011.1**: On the direct path, `smartunpack()` base64-decodes each payload and deserializes by type (`_deserialize_data`).
```julia
# _deserialize_data (src/msghandler.jl:940)
# text -> String(data)
# dictionary-> JSON.parse(json_str)
# arrowtable-> Arrow.Table(io) -> DataFrame
# image/audio/video/binary -> data (bytes)
```
> Traceability: `SD-004`, `SD-005`.
### 2.3 Send Reply
**WT-012**: The backend processes the message and builds a reply with `smartpack()`.
```julia
# Julia (Backend) - WT-012
response_text = "Hello Ton! I'm the AI assistant."
generated_image = generate_ai_image(response_text)
env, msg_json = smartpack(
"/agent/wine/api/v1/response",
(
("response", response_text, "text"),
("generated_image", generated_image, "image"),
),
reply_to = "/chat/user/v1/message",
reply_to_msg_id = env["msg_id"]
)
publish(transport_conn, "/agent/wine/api/v1/response", msg_json)
```
> Traceability: `FR-001` (reply text), `FR-013` (publish — caller responsible), `FR-011` (`reply_to_msg_id` links the reply), `SD-004`, `SD-005`, `SD-008`.
**Rationale**: `reply_to_msg_id = env["msg_id"]` correlates this reply to the original request (`SD-008`). `smartpack()` returns `(env, env_json_str)` — **the backend must publish** (`FR-013`).
### 2.4 Receive Large File (Link Transport)
**WT-013**: A client uploads a 10 MB file; msghandler downloads it with exponential backoff.
```julia
# Julia (Backend) - WT-013
transport_msg = transport_subscription.next()
env = smartunpack(String(transport_msg.payload))
# The "dataset" payload has transport="link"; smartunpack():
# 1. extracts the URL from payload["data"]
# 2. downloads with exponential backoff (_fetch_with_backoff)
# 3. deserializes (binary -> bytes)
```
> Traceability: `FR-003` (large file handling), `FR-008` (Plik), `FR-010` (backoff retry), `SD-001`, `SD-002`, `SD-003`, `SD-007`.
**State (link path)** — from the backend's POV, per-payload decision `SD-002`:
```mermaid
stateDiagram-v2
[*] --> InspectEnvelope: smartunpack(json_string)
InspectEnvelope --> Direct: transport == "direct"
InspectEnvelope --> Link: transport == "link"
Direct --> Deserialize: base64decode + _deserialize_data
Link --> Download: fileserver_download_handler (SD-007)
Download --> Deserialize: download_failed? retry w/ backoff
Deserialize --> Return: push (dataname, data, type)
Return --> [*]
```
### 2.5 Error Paths (Julia POV)
**WT-014**: File server download fails after `max_retries` (`FR-010`, `SD-007`).
```mermaid
sequenceDiagram
participant BJ as Julia Backend
participant D as _fetch_with_backoff
participant FS as Plik File Server
BJ->>D: fetch(url, 5, 100, 5000)
FS-->>D: 503 / connection refused
D->>D: sleep(backoff), double delay (SD-007)
loop up to max_retries (5)
D->>FS: retry
FS-->>D: still failing
end
D->>BJ: error("Failed to fetch data after 5 attempts")
BJ->>BJ: log_trace(correlation_id, "download retry exceeded")
```
> Traceability: `FR-010` (backoff retry), `NFR-202` (file server availability), `NFR-404` (alerting). Recovery: caller retries with a new upload; correlation id preserves traceability (`SD-008`).
**WT-015**: Unknown payload type throws `error("Unknown payload_type: $payload_type")` (`FR-007`).
---
## 3. Python (Backend)
This section traces the Python backend: it receives tabular data (DataFrame), analyzes it, and sends results back. Python supports `arrowtable` (desktop) and `dictionary` types.
> Cross-references: Python may publish to a topic that **Julia** and **Rust** also subscribe to (see [Section 2](#2-julia-backend-ground-truth), [Section 4](#4-rust-service)).
### 3.1 Session Start
**WT-020**: The Python backend imports `msghandler` and connects its transport consumer.
### 3.2 Send Tabular Data (Round-Trip)
**WT-021**: Python sends a DataFrame as `arrowtable`.
```python
# Python - WT-021
import pandas as pd
from msghandler import smartpack
df = pd.DataFrame({"id": [1, 2, 3], "name": ["Alice", "Bob", "Charlie"], "score": [95, 88, 92]})
env, msg_json = smartpack(
"/agent/wine/api/v1/analyze",
[("data", df, "arrowtable")],
broker_url="ws://localhost:4222",
receiver_name="agent-backend"
)
await transport_publisher.publish("/agent/wine/api/v1/analyze", msg_json)
```
> Traceability: `FR-002` (tabular data), `FR-012` (serialization), `SD-004`, `SD-005`, `SD-006`.
**WT-021.1**: Serialization to Arrow IPC preserves column types; the payload is `< threshold` so it travels direct with `encoding = "arrow-ipc"`.
> Traceability: `SD-002`, `SD-005`.
### 3.3 Receive Results
**WT-022**: Python receives the processed results and deserializes back to a DataFrame.
```python
# Python - WT-022
transport_msg = await transport_consumer.next()
env = await smartunpack(str(transport_msg.payload))
# env["payloads"] is now:
# [("results", <DataFrame>, "arrowtable"),
# ("summary", "Analysis complete: 1500 rows processed", "text")]
```
> Traceability: `FR-014`, `FR-002`, `FR-007`, `SD-004`, `SD-007`.
### 3.4 Error Paths (Python POV)
**WT-023**: Large `arrowtable` (≥ threshold) uses link transport; upload fails.
> Traceability: `FR-003`, `SD-001`, `SD-003`. Recovery: retry with a smaller chunk or direct transport.
**WT-024**: `arrowtable` sent but `pyarrow` not installed → `_serialize_data` raises `RuntimeError` (no Arrow support).
> Traceability: `FR-002`.
---
## 4. Rust (Service)
This section traces the Rust service, which leverages compile-time type safety via enums and `serde` for serialization, and a tokio async runtime for transport and HTTP. Rust supports the full feature set including `arrowtable` and link transport.
> Cross-references: Rust subscribes to the same topic consumed by **Julia** and **Python** (see [Section 2](#2-julia-backend-ground-truth), [Section 3](#3-python-backend)).
### 4.1 Session Start
**WT-030**: The Rust service connects and subscribes under a tokio runtime.
### 4.2 Receive & Dispatch by Type
**WT-031**: Rust receives and dispatches payloads by `payload_type`.
```rust
// Rust service - WT-031
use msghandler::{smartunpack, MsgEnvelopeV1};
use base64::Engine as _;
#[tokio::main]
async fn main() {
let mut sub = transport_client::connect("ws://localhost:4222")
.unwrap().subscribe("/agent/wine/api/v1/analyze");
for msg in sub.messages() {
let env = smartunpack(&String::from_utf8_lossy(&msg.payload), &Default::default()).await.unwrap();
for payload in &env.payloads {
match payload.payload_type.as_str() {
"arrowtable" => {
let arrow_bytes = BASE64.decode(&payload.data).unwrap();
println!("Received arrowtable payload ({} bytes)", arrow_bytes.len());
},
"text" => println!("Message: {}", payload.data),
"image" | "audio" | "video" | "binary" => {
let bytes = BASE64.decode(&payload.data).unwrap();
println!("Received {} bytes of {}", bytes.len(), payload.payload_type);
},
"dictionary" | "jsontable" => println!("Data: {}", payload.data),
_ => println!("Unknown payload type: {}", payload.payload_type),
}
}
}
}
```
> Traceability: `FR-007` (type preservation), `FR-006` (multi-payload), `SD-004`, `SD-005`. The `payload_type` field determines how to interpret the `data` string (type dispatch).
### 4.3 Send Type-Safe Results
**WT-032**: Rust sends mixed results using the enum-based `Payload` API.
```rust
// Rust service - WT-032
let (envelope, json_str) = smartpack(
"/agent/wine/api/v1/results",
&[
("results".to_string(), Payload::ArrowTable(result_bytes), "arrowtable".to_string()),
("summary".to_string(), Payload::Text("Analysis complete: 1500 rows".to_string()), "text".to_string()),
],
&smartpackOptions {
broker_url: DEFAULT_BROKER_URL.to_string(),
reply_to: "/python/worker/v1/results".to_string(),
msg_purpose: "chat".to_string(),
..Default::default()
},
).await?;
conn.publish("/agent/wine/api/v1/results", &json_str)?;
```
> Traceability: `FR-013` (publish), `FR-001`, `SD-004`, `SD-006`.
### 4.4 Large File via Link Transport
**WT-033**: Rust uploads a large binary file; `smartpack()` selects link transport and uploads via `reqwest`.
```rust
// Rust service - WT-033
let large_file_data: Vec<u8> = std::fs::read("/data/large_dataset.parquet")?;
let (envelope, json_str) = smartpack(
"/agent/wine/api/v1/upload",
&[("dataset".to_string(), Payload::Binary(large_file_data), "binary".to_string())],
&smartpackOptions {
fileserver_url: DEFAULT_FILESERVER_URL.to_string(),
size_threshold: DEFAULT_SIZE_THRESHOLD,
..Default::default()
},
).await?;
conn.publish("/agent/wine/api/v1/upload", &json_str)?;
```
> Traceability: `FR-003`, `SD-001`, `SD-002`, `SD-003`, `SD-006`.
### 4.5 Error Paths (Rust POV)
**WT-034**: Download exceeds retries → `MsgHandlerError::DownloadFailed { url, retries }` (`FR-010`, `SD-007`).
> Traceability: `FR-010`, `NFR-202`. Recovery: caller retries with a fresh upload; idiomatic `Result<T, E>`.
---
## 5. MicroPython (IoT Device)
This section traces the MicroPython sensor device. Due to memory constraints (256 KB–1 MB), MicroPython is limited to `text` and `dictionary` types and **direct transport only** (no link / file-server support).
> Cross-references: the device publishes readings that a **Python** backend consumes (see [Section 3](#3-python-backend)).
### 5.1 Session Start
**WT-040**: The device boots, connects to the MQTT broker, and publishes a sensor reading.
### 5.2 Send Sensor Data (Direct Only)
**WT-041**: MicroPython sends a `dictionary` payload under its lower threshold.
```python
# MicroPython - WT-041
from msghandler import smartpack
sensor_data = {"temperature": 25.5, "humidity": 60.0, "pressure": 1013.25}
env, msg_json = smartpack(
"/sensor/device/v1/readings",
[("data", sensor_data, "dictionary")],
broker_url="broker.local:1883",
size_threshold=100000 # 100 KB for MicroPython
)
mqtt_client.publish("/sensor/device/v1/readings", msg_json)
```
> Traceability: `FR-005` (MicroPython support), `FR-006` (multi-payload), `FR-012` (serialization), `SD-004`, `SD-005`, `SD-006`.
**WT-041.1**: Serialization is JSON → UTF-8 → base64 (no Arrow). Because the payload is under `size_threshold`, it travels direct.
> Traceability: `SD-002`, `SD-005`.
```mermaid
flowchart LR
subgraph Device["MicroPython Device"]
D1["sensor_data = {temp, humidity}"]
D2["smartpack() -> JSON"]
D3["publish via MQTT"]
end
subgraph Broker["MQTT Broker"]
B["broker.local:1883"]
end
subgraph Backend["Python Backend"]
P1["subscribe via MQTT"]
P2["smartunpack(JSON)"]
P3["get dictionary"]
end
D1 --> D2 --> D3 --> B --> P1 --> P2 --> P3
```
### 5.3 Receive Acknowledgment
**WT-042**: The device receives an ACK/dictionary reply from the backend.
```python
# MicroPython - WT-042
env = smartunpack(str(transport_msg.payload))
# env["payloads"] is now:
# [("data", {"temperature": 25.5, ...}, "dictionary")]
```
> Traceability: `FR-014`, `FR-007`, `SD-004`.
### 5.4 Error Paths (MicroPython POV)
**WT-043**: Payload exceeds the 100 KB threshold → MicroPython cannot use link transport (no file-server support) and must reject or chunk the payload.
> Traceability: `FR-005`, `FR-003`. Recovery: split into multiple direct payloads below the threshold.
**WT-044**: `dictionary` type but data is not JSON-serializable → serialization error (memory constraint).
> Traceability: `FR-012`.
---
## 6. Cross-Participant Flow (all participants)
The following end-to-end flow shows a JavaScript client, a Julia backend, a Python worker, and a MicroPython device on a shared chat topic.
```mermaid
sequenceDiagram
participant JS as JavaScript (Browser)
participant J as Julia (Backend)
participant P as Python (Worker)
participant MP as MicroPython (Device)
participant T as Message Broker
JS->>T: publish "/chat/user/v1/message" (WT-003)
T->>J: deliver to Julia
J->>J: smartunpack() (WT-011)
J->>J: process, build reply (WT-012)
J->>T: publish reply (SD-006)
T->>P: deliver to Python
P->>P: smartunpack() (WT-022)
T->>MP: deliver to MicroPython
MP->>MP: smartunpack() (WT-042)
T->>JS: deliver reply to browser (WT-005)
```
> Traceability: `FR-001`, `FR-006`, `FR-013`, `FR-014`, `SD-004`, `SD-006`, `SD-008`.
---
## 7. Specification Traceability
Each walkthrough step below maps to the exact `SD-XXX` / `FR-XXX` artifacts it implements or depends on.
| WT ID | Participant | Step | SD Ref | FR Ref | Description |
|-------|-------------|------|--------|--------|-------------|
| WT-001 | JavaScript | Session start / connect | SD-006 | FR-013 | Establish transport connection |
| WT-002 | JavaScript | Send mixed payloads (text + image) | SD-004, SD-005 | FR-001, FR-006, FR-007 | smartpack() with mixed types |
| WT-002.1 | JavaScript | Per-payload transport selection | SD-002 | FR-004 | direct transport for < threshold |
| WT-003 | JavaScript | Publish JSON via transport | SD-006 | FR-013 | caller publishes |
| WT-004 | JavaScript | Envelope shape crosses wire | SD-005, SD-008 | FR-011, FR-013 | msg_envelope_v1 JSON |
| WT-005 | JavaScript | Receive reply | SD-004, SD-007 | FR-007, FR-014 | smartunpack() on reply |
| WT-006 | JavaScript | Large file upload fails (link) | SD-001, SD-003 | FR-003, FR-008 | link transport error |
| WT-007 | JavaScript | Transport connection lost | SD-006 | NFR-203 | caller retries publish |
| WT-010 | Julia | Session start / subscribe | SD-006 | FR-014 | transport subscription |
| WT-011 | Julia | Receive & unpack chat | SD-004, SD-007 | FR-001, FR-006, FR-007 | smartunpack() |
| WT-011.1 | Julia | Direct path decode/deserialize | SD-004, SD-005 | FR-007 | _deserialize_data |
| WT-012 | Julia | Send reply | SD-004, SD-005, SD-008 | FR-001, FR-013, FR-011 | smartpack() + reply_to_msg_id |
| WT-013 | Julia | Receive large file (link) | SD-001, SD-002, SD-003 | FR-003, FR-008 | link download |
| WT-014 | Julia | Download fails after retries | SD-007 | FR-010, NFR-202 | _fetch_with_backoff |
| WT-015 | Julia | Unknown payload type | SD-004 | FR-007 | error throw |
| WT-020 | Python | Session start / connect | SD-006 | FR-013 | transport consumer |
| WT-021 | Python | Send arrowtable | SD-004, SD-005 | FR-002, FR-012 | Arrow IPC direct |
| WT-021.1 | Python | Arrow IPC serialization | SD-002, SD-005 | FR-012 | encoding arrow-ipc |
| WT-022 | Python | Receive results | SD-004, SD-007 | FR-002, FR-007, FR-014 | smartunpack() |
| WT-023 | Python | Large arrowtable upload fails | SD-001, SD-003 | FR-003 | link transport error |
| WT-024 | Python | pyarrow not available | SD-005 | FR-002 | RuntimeError |
| WT-030 | Rust | Session start / subscribe | SD-006 | FR-013 | tokio transport |
| WT-031 | Rust | Receive & dispatch by type | SD-004, SD-005 | FR-006, FR-007 | payload_type dispatch |
| WT-032 | Rust | Send type-safe results | SD-004 | FR-001, FR-013 | Payload enum |
| WT-033 | Rust | Large file via link | SD-001, SD-002, SD-003 | FR-003 | reqwest upload |
| WT-034 | Rust | Download exceeds retries | SD-007 | FR-010, NFR-202 | DownloadFailed |
| WT-040 | MicroPython | Session start / publish | SD-006 | FR-005 | MQTT connect |
| WT-041 | MicroPython | Send dictionary (direct) | SD-004, SD-005 | FR-005, FR-006 | JSON base64 |
| WT-041.1 | MicroPython | JSON serialization, direct transport | SD-002, SD-005 | FR-012 | JSON -> base64 |
| WT-042 | MicroPython | Receive ACK | SD-004 | FR-007, FR-014 | smartunpack() |
| WT-043 | MicroPython | Threshold exceeded (no link) | SD-002 | FR-003, FR-005 | direct-only constraint |
| WT-044 | MicroPython | Non-serializable dictionary | SD-005 | FR-012 | serialization error |
| WT-050 | — | Cross-participant chat broadcast | SD-004, SD-006 | FR-001, FR-006, FR-013, FR-014 | shared topic flow |
---
## 8. Gap-Check Validation (Walkthrough)
| Stage Transition | Gap-Check Question | Status |
|------------------|-------------------|--------|
| Solution Design → Walkthrough | Does every `SD-XXX` decision appear in at least one participant's walkthrough step with a `WT-XXX` id? Is there a section for each system participant? | ✅ Verified — SD-001..SD-008 traced across JS/Julia/Python/Rust/MicroPython sections with WT-XXX ids; all 5 participants covered |
| Walkthrough → Specification | Does every `WT-XXX` walkthrough step trace to at least one `SP-XXX` specification rule? | ✅ Verified — every `WT-XXX` step maps to `SP-XXX` in the Walkthrough → Specification Trace of [`4-specification.md`](4-specification.md) |
---
## 9. Change Log
| Date | Version | Changes | Specification Ref | Solution Design Ref |
|------|---------|---------|-------------------|---------------------|
| 2026-09-16 | 1.7.0 | Restructured per ASG Framework walkthrough guideline: per-participant sections (JS/Julia/Python/Rust/MicroPython), `WT-XXX` step ids, `SD-XXX`/`FR-XXX` citations, per-participant sequence diagrams, cross-participant flow, and error paths | All sections | SD-001 through SD-008 |
| 2026-09-16 | 1.6.0 | Aligned ground-truth API to `src/msghandler.jl` (smartpack returns tuple, smartunpack returns payloads as tuples); fixed Rust payload access | specification.md | SD-001 through SD-008 |
| 2026-05-22 | 1.5.0 | Updated to ASG Framework eight pillars — added solution-design traceability (SD-XXX) to the traceability table | All sections | SD-001 through SD-008 |
| 2026-05-15 | 1.4.0 | Made transport layer agnostic; removed NATS-specific references | All sections | SD-001 through SD-008 |
| 2026-05-13 | 1.2.0 | Added Rust support (tokio, serde, arrow2) | specification.md:11 | SD-001 through SD-008 |
| 2026-03-23 | 1.0.0 | Updated to ASG Framework walkthrough guidelines | All sections | SD-001 through SD-008 |
---
*This walkthrough document is versioned and maintained in git alongside the codebase. The technical details are grounded in [`src/msghandler.jl`](../src/msghandler.jl) (the reference implementation) and kept consistent with the Python, JavaScript (CSR), Rust, and MicroPython sources. All implementations must adhere to this documentation.*
+181 -253
View File
@@ -1,17 +1,18 @@
# Specification: msghandler
**Version**: 1.3.0
**Date**: 2026-05-22
**Version**: 1.4.0
**Date**: 2026-09-16
**Status**: Active
**Ground Truth**: [`src/msghandler.jl`](../src/msghandler.jl)
**Specification Format**: JSON Schema + AsyncAPI
**ASG Framework Alignment**: v8 pillars - Requirements → Solution Design → Specification → Walkthrough → Implementation Plan → Validation → Runbook
**ASG Framework Alignment**: Eight Pillars - Requirements → Solution Design → Walkthrough → Specification → UI Specification → Validation → Implementation Plan → Runbook
**Specification Item IDs**: `SP-XXX` (see [Specification Traceability Matrix](#11-requirements-traceability)); every spec item cites its Requirement ID(s) (`FR-XXX`/`NFR-XXX`) and Solution Design reference(s) (`SD-XXX`).
---
## 1. Technical Contract Overview
This document defines the **technical contract** for msghandler - the 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.
This document defines the **technical contract** for msghandler - the cross-platform bi-directional data bridge that enables seamless communication between **Julia**, **JavaScript**, **Python**, **Rust**, and **MicroPython** applications using a message broker as the transport layer.
This specification serves as the single source of truth for:
- **Inputs**: What data structures are accepted by `smartpack()`
@@ -21,26 +22,100 @@ This specification serves as the single source of truth for:
### 1.1 Requirements Traceability
| Specification Section | Requirement ID(s) | Solution Design Ref(s) | Description |
Per the ASG Framework, every technical contract below carries a unique `SP-XXX` Specification Item ID and cites its Requirement ID(s) (the *what*) and Solution Design reference(s) (the *how/why*). The canonical cross-reference is the [Specification Traceability Matrix](#specification-traceability-matrix).
| Specification Section | Spec ID (SP) | Requirement ID(s) | Solution Design Ref(s) | Description |
|----------------------|-------------------|------------------------|-------------|
| Section 2 (Message Envelope) | FR-012, FR-013, NFR-101, NFR-102 | SD-008 | Message envelope structure and validation |
| Section 3 (Payload Schema) | FR-001, FR-002, FR-003, FR-004, NFR-101, NFR-102 | SD-001, SD-005 | Payload structure and field definitions |
| Section 4 (Payload Format) | FR-006, FR-007 | SD-004 | Tuple format for smartpack() |
| Section 5 (Enumerations) | FR-003, FR-004, FR-006, NFR-101 | SD-001, SD-002, SD-005 | Enumerations for transport and encoding |
| Section 6 (Transport Protocols) | FR-003, FR-004, NFR-104, NFR-105 | SD-001, SD-002 | Direct and link transport protocols |
| Section 7 (Size Thresholds) | FR-004, FR-005, NFR-104, NFR-105 | SD-002 | Size thresholds for transport selection |
| Section 8 (Topic Convention) | FR-013, FR-014 | SD-006 | Topic/subject naming patterns |
| Section 9 (Error Handling) | FR-010, FR-011, NFR-201, NFR-202, NFR-203 | SD-003, SD-007 | Error codes and exception handling |
| Section 10 (Serialization Rules) | FR-001, FR-002, FR-003, FR-012, NFR-101, NFR-102 | SD-005 | Serialization and encoding rules |
| Section 11 (API Contract) | FR-001 through FR-014, NFR-101 through NFR-107 | SD-001 through SD-008 | Function signatures for all platforms |
| Section 12 (File Server Interface) | FR-008, FR-009, FR-010 | SD-003, SD-007 | Upload and download handler contracts |
| Section 13 (Platform-Specific Constraints) | FR-005, FR-006, NFR-106, NFR-107 | SD-004, SD-006 | Platform-specific feature support |
| Section 14 (Implementation Files) | FR-001 through FR-007 | - | Implementation file mapping |
| Section 15 (Message Flow) | FR-001 through FR-014, NFR-101 through NFR-107 | SD-001 through SD-008 | Mermaid diagrams for send/receive flows |
| Section 16 (Validation Rules) | FR-001 through FR-014, NFR-101 through NFR-405 | SD-001 through SD-008 | Envelope and payload validation rules |
| Section 17 (Test Contracts) | FR-001 through FR-014, NFR-101 through NFR-107 | SD-001 through SD-008 | Unit and integration test scenarios |
| Section 18 (Dependencies) | FR-001 through FR-014, NFR-101 through NFR-107 | SD-001 through SD-008 | Platform-specific dependencies |
| Section 19 (Change Log) | - | - | Version history and changes |
| Section 2 (Message Envelope) | SP-001 | FR-012, FR-013, NFR-101, NFR-102 | SD-008 | Message envelope structure and validation |
| Section 3 (Payload Schema) | SP-002 | FR-001, FR-002, FR-003, FR-004, NFR-101, NFR-102 | SD-001, SD-005 | Payload structure and field definitions |
| Section 4 (Payload Format) | SP-003 | FR-006, FR-007 | SD-004 | Tuple format for smartpack() |
| Section 5 (Enumerations) | SP-004..SP-007 | FR-003, FR-004, FR-006, NFR-101 | SD-001, SD-002, SD-005 | Enumerations for transport and encoding |
| Section 6 (Transport Protocols) | SP-008, SP-009 | FR-003, FR-004, NFR-104, NFR-105 | SD-001, SD-002 | Direct and link transport protocols |
| Section 7 (Size Thresholds) | SP-010 | FR-004, FR-005, NFR-104, NFR-105 | SD-002 | Size thresholds for transport selection |
| Section 8 (Topic Convention) | SP-011 | FR-013, FR-014 | SD-006 | Topic/subject naming patterns |
| Section 9 (Error Handling) | SP-012..SP-014 | FR-010, FR-011, NFR-201, NFR-202, NFR-203 | SD-003, SD-007 | Error codes and exception handling |
| Section 10 (Serialization Rules) | SP-015 | FR-001, FR-002, FR-003, FR-012, NFR-101, NFR-102 | SD-005 | Serialization and encoding rules |
| Section 11 (API Contract) | SP-016, SP-017 | FR-001 through FR-014, NFR-101 through NFR-107 | SD-001 through SD-008 | Function signatures for all platforms |
| Section 12 (File Server Interface) | SP-018, SP-019 | FR-008, FR-009, FR-010 | SD-003, SD-007 | Upload and download handler contracts |
| Section 13 (Platform-Specific Constraints) | SP-020 | FR-005, FR-006, NFR-106, NFR-107 | SD-004, SD-006 | Platform-specific feature support |
| Section 14 (Implementation Files) | SP-021 | FR-001 through FR-007 | - | Implementation file mapping |
| Section 15 (Message Flow) | SP-022 | FR-001 through FR-014, NFR-101 through NFR-107 | SD-001 through SD-008 | Mermaid diagrams for send/receive flows |
| Section 16 (Validation Rules) | SP-023 | FR-001 through FR-014, NFR-101 through NFR-405 | SD-001 through SD-008 | Envelope and payload validation rules |
| Section 17 (Test Contracts) | SP-024 | FR-001 through FR-014, NFR-101 through NFR-107 | SD-001 through SD-008 | Unit and integration test scenarios |
| Section 18 (Dependencies) | SP-025 | FR-001 through FR-014, NFR-101 through NFR-107 | SD-001 through SD-008 | Platform-specific dependencies |
| Section 19 (Change Log) | — | — | — | Version history and changes |
### Specification Traceability Matrix
Canonical mapping of each `SP-XXX` Specification Item ID to its Requirement IDs and Solution Design references, per the ASG Framework. Every `SP-XXX` below is implemented by at least one file under [`src/`](../src/) (ground truth: [`src/msghandler.jl`](../src/msghandler.jl)).
| SP ID | Specification Item | Requirement ID(s) | Solution Design Ref | Implementation File |
|-------|--------------------|-------------------|---------------------|---------------------|
| SP-001 | Message Envelope Schema (`envelope_to_json`) | FR-012, FR-013, NFR-101, NFR-102 | SD-008 | msghandler.jl |
| SP-002 | Payload Schema | FR-001, FR-002, FR-003, FR-004, NFR-101, NFR-102 | SD-001, SD-005 | msghandler.jl |
| SP-003 | Payload Tuple Format `("name", data, "type")` | FR-006, FR-007 | SD-004 | msghandler.jl |
| SP-004 | `msg_purpose` Enum (ACK/NACK/updateStatus/shutdown/chat/command/event) | FR-003, FR-004, FR-006, NFR-101 | SD-001 | msghandler.jl |
| SP-005 | `payload_type` Enum | FR-003, FR-004, FR-006, NFR-101 | SD-002 | msghandler.jl |
| SP-006 | `transport` Enum (direct/link) | FR-003, FR-004, NFR-104, NFR-105 | SD-001, SD-002 | msghandler.jl |
| SP-007 | `encoding` Enum (none/base64/json/arrow-ipc) | FR-001, FR-002, FR-003, FR-012 | SD-005 | msghandler.jl |
| SP-008 | Direct Transport Protocol | FR-003, FR-004, NFR-104, NFR-105 | SD-001, SD-002 | msghandler.jl |
| SP-009 | Link Transport Protocol (Claim-Check / file server) | FR-003, FR-004, NFR-104, NFR-105 | SD-001, SD-002 | msghandler.jl |
| SP-010 | Size Thresholds (10,000 bytes desktop; 100,000 MicroPython) | FR-004, FR-005, NFR-104, NFR-105 | SD-002 | msghandler.jl |
| SP-011 | Topic/Subject Convention `<scope>/<service>/<version>/<operation>` | FR-013, FR-014 | SD-006 | msghandler.jl |
| SP-012 | Error Response Format | FR-010, FR-011, NFR-201, NFR-202, NFR-203 | SD-003, SD-007 | msghandler.jl |
| SP-013 | Error Codes | FR-010, FR-011, NFR-201, NFR-202, NFR-203 | SD-003, SD-007 | msghandler.jl |
| SP-014 | Exception Handling / Retry Policy | FR-010, FR-011, NFR-201, NFR-202, NFR-203 | SD-003, SD-007 | msghandler.jl |
| SP-015 | Serialization Rules | FR-001, FR-002, FR-003, FR-012, NFR-101, NFR-102 | SD-005 | msghandler.jl |
| SP-016 | `smartpack()` API Contract | FR-001..FR-014, NFR-101..NFR-107 | SD-001..SD-008 | msghandler.jl |
| SP-017 | `smartunpack()` API Contract | FR-001..FR-014, NFR-101..NFR-107 | SD-001..SD-008 | msghandler.jl |
| SP-018 | Upload Handler Contract (`plik_oneshot_upload`) | FR-008, FR-009, FR-010 | SD-003, SD-007 | msghandler.jl |
| SP-019 | Download Handler Contract (`_fetch_with_backoff`) | FR-010, FR-011, NFR-202 | SD-003, SD-007 | msghandler.jl |
| SP-020 | Platform-Specific Constraints | FR-005, FR-006, NFR-106, NFR-107 | SD-004, SD-006 | msghandler.jl, msghandler-csr.js, msghandler.py, msghandler.rs, msghandler_mpy.py |
| SP-021 | Implementation Files | FR-001..FR-007 | — | msghandler.jl, msghandler-csr.js, msghandler.py, msghandler.rs, msghandler_mpy.py |
| SP-022 | Message Flow (send/receive) | FR-001..FR-014, NFR-101..NFR-107 | SD-001..SD-008 | msghandler.jl |
| SP-023 | Validation Rules | FR-001..FR-014, NFR-101..NFR-405 | SD-001..SD-008 | msghandler.jl |
| SP-024 | Test Contracts | FR-001..FR-014, NFR-101..NFR-107 | SD-001..SD-008 | msghandler.jl |
| SP-025 | Dependencies | FR-001..FR-014, NFR-101..NFR-107 | SD-001..SD-008 | msghandler.jl |
### Walkthrough → Specification Trace
Maps each `WT-XXX` walkthrough step (from `3-walkthrough.md`) to the `SP-XXX` specification item(s) it exercises, closing the walkthrough↔specification gap-check. Derived from the walkthrough's `WT-XXX → SD-XXX → FR-XXX` trace combined with the `SD-XXX → SP-XXX` mapping above.
| WT ID | Participant | Walkthrough Step | SP ID(s) |
|-------|-------------|------------------|----------|
| WT-001 | JavaScript | Session start / transport connect | SP-011, SP-016 |
| WT-002 | JavaScript | Send mixed payloads (text + image) | SP-003, SP-015, SP-016 |
| WT-002.1 | JavaScript | Per-payload transport selection | SP-010, SP-008, SP-016 |
| WT-003 | JavaScript | Publish JSON via transport | SP-011, SP-016 |
| WT-004 | JavaScript | Envelope shape crosses the wire | SP-001 |
| WT-005 | JavaScript | Receive reply | SP-017 |
| WT-006 | JavaScript | Large file upload fails (link transport) | SP-009, SP-018, SP-013 |
| WT-007 | JavaScript | Transport connection lost (caller retry) | SP-011, SP-016 |
| WT-010 | Julia | Session start / subscribe | SP-011, SP-017 |
| WT-011 | Julia | Receive & unpack chat | SP-017, SP-015, SP-002 |
| WT-011.1 | Julia | Direct path base64 decode / deserialize | SP-015, SP-008, SP-002 |
| WT-012 | Julia | Send reply | SP-016, SP-001 |
| WT-013 | Julia | Receive large file (link transport) | SP-009, SP-019, SP-010 |
| WT-014 | Julia | Download fails after max_retries | SP-014, SP-019, SP-013 |
| WT-015 | Julia | Unknown payload type throws | SP-005, SP-013 |
| WT-020 | Python | Session start / connect | SP-011, SP-017 |
| WT-021 | Python | Send arrowtable | SP-002, SP-015, SP-016 |
| WT-021.1 | Python | Arrow IPC serialization (arrow-ipc) | SP-015, SP-010, SP-008 |
| WT-022 | Python | Receive results | SP-017, SP-015 |
| WT-023 | Python | Large arrowtable upload fails | SP-009, SP-018, SP-013 |
| WT-024 | Python | pyarrow not available | SP-005, SP-013, SP-020 |
| WT-030 | Rust | Session start / subscribe | SP-011, SP-016 |
| WT-031 | Rust | Receive & dispatch by payload_type | SP-017, SP-002, SP-005 |
| WT-032 | Rust | Send type-safe results | SP-016, SP-002 |
| WT-033 | Rust | Large file via link transport | SP-009, SP-018, SP-010 |
| WT-034 | Rust | Download exceeds retries | SP-014, SP-019, SP-013 |
| WT-040 | MicroPython | Session start / publish sensor reading | SP-011, SP-016 |
| WT-041 | MicroPython | Send dictionary (direct transport) | SP-002, SP-015, SP-008, SP-016 |
| WT-041.1 | MicroPython | JSON serialization, direct transport | SP-015, SP-010, SP-008 |
| WT-042 | MicroPython | Receive ACK | SP-017 |
| WT-043 | MicroPython | Threshold exceeded (direct-only) | SP-010, SP-009, SP-020 |
| WT-044 | MicroPython | Non-serializable dictionary | SP-005, SP-013, SP-020 |
| WT-050 | — | Cross-participant chat broadcast | SP-011, SP-022, SP-016 |
---
@@ -89,24 +164,23 @@ This specification serves as the single source of truth for:
}
```
### Field Definitions
### Field Definitions (SP-001)
| Field | Type | Required | Validation | Description | Requirement ID | Solution Design Ref |
|-------|------|----------|------------|-------------|----------------|-------------------|
| `correlation_id` | `string` | Yes | UUID v4 format | Track message flow across distributed systems | FR-011, NFR-401 | SD-008 |
| `msg_id` | `string` | Yes | UUID v4 format | Unique identifier for this specific message | FR-012, NFR-401 | SD-001 |
| `timestamp` | `string` | Yes | ISO 8601 UTC | Message publication timestamp | FR-012, NFR-401 | SD-001 |
| `send_to` | `string` | Yes | Non-empty string | Topic/subject to publish the message to | FR-013 | SD-006 |
| `msg_purpose` | `string` | Yes | Enum | Purpose of the message | FR-013 | SD-006 |
| `sender_name` | `string` | Yes | Non-empty string | Name of the sender application | FR-013 | SD-006 |
| `sender_id` | `string` | Yes | UUID v4 format | Unique identifier for the sender | FR-013 | SD-006 |
| `receiver_name` | `string` | Yes | Any string | Name of the receiver (empty = broadcast) | FR-013 | SD-006 |
| `receiver_id` | `string` | Yes | Any string | UUID of the receiver (empty = broadcast) | FR-013 | SD-006 |
| `reply_to` | `string` | Yes | Any string | Topic where receiver should reply | FR-013 | SD-006 |
| `reply_to_msg_id` | `string` | Yes | Any string | Message ID this message is replying to | FR-013 | SD-006 |
| `broker_url` | `string` | Yes | Valid URL | Broker URL for the transport layer | FR-013 | SD-006 |
| `metadata` | `object` | No | Any JSON object | Message-level metadata | NFR-401 | SD-001, SD-008 |
| `payloads` | `array` | Yes | Non-empty array | List of payload objects | FR-012, FR-013 | SD-001, SD-005 |
| SP ID | Field | Type | Required | Validation | Description | Requirement ID | Solution Design Ref |
|-------|------|------|----------|------------|-------------|----------------|-------------------|
| SP-001 | `correlation_id` | `string` | Yes | UUID v4 format | Track message flow across distributed systems | FR-011, NFR-401 | SD-008 |
| SP-001 | `msg_id` | `string` | Yes | UUID v4 format | Unique identifier for this specific message | FR-012, NFR-401 | SD-001 |
| SP-001 | `timestamp` | `string` | Yes | ISO 8601 UTC | Message publication timestamp | FR-012, NFR-401 | SD-001 |
| SP-001 | `send_to` | `string` | Yes | Non-empty string | Topic/subject to publish the message to | FR-013 | SD-006 |
| SP-001 | `msg_purpose` | `string` | Yes | Enum | Purpose of the message | FR-013 | SD-006 |
| SP-001 | `sender_name` | `string` | Yes | Non-empty string | Name of the sender application | FR-013 | SD-006 |
| SP-001 | `receiver_name` | `string` | Yes | Any string | Name of the receiver (empty = broadcast) | FR-013 | SD-006 |
| SP-001 | `receiver_id` | `string` | Yes | Any string | UUID of the receiver (empty = broadcast) | FR-013 | SD-006 |
| SP-001 | `reply_to` | `string` | Yes | Any string | Topic where receiver should reply | FR-013 | SD-006 |
| SP-001 | `reply_to_msg_id` | `string` | Yes | Any string | Message ID this message is replying to | FR-013 | SD-006 |
| SP-001 | `broker_url` | `string` | Yes | Valid URL | Broker URL for the transport layer | FR-013 | SD-006 |
| SP-001 | `metadata` | `object` | No | Any JSON object | Message-level metadata | NFR-401 | SD-001, SD-008 |
| SP-001 | `payloads` | `array` | Yes | Non-empty array | List of payload objects | FR-012, FR-013 | SD-001, SD-005 |
---
@@ -127,22 +201,22 @@ This specification serves as the single source of truth for:
}
```
### Payload Field Definitions
### Payload Field Definitions (SP-002)
| Field | Type | Required | Validation | Description | Requirement ID | Solution Design Ref |
|-------|------|----------|------------|-------------|----------------|-------------------|
| `id` | `string` | Yes | UUID v4 format | Unique identifier for this payload | FR-012 | SD-001 |
| `dataname` | `string` | Yes | Non-empty string | Name of the payload (e.g., `login_image`, `user_data`) | FR-001, FR-002, FR-003 | SD-004 |
| `payload_type` | `string` | Yes | Enum | Type of payload (see `payload_type` enum) | FR-001, FR-002, FR-003, FR-006 | SD-004, SD-005 |
| `transport` | `string` | Yes | Enum | Transport method: `direct` or `link` | FR-003, FR-004, NFR-104, NFR-105 | SD-001, SD-002 |
| `encoding` | `string` | Yes | Enum | Encoding method (see `encoding` enum) | FR-001, FR-002, FR-003, FR-012 | SD-005 |
| `size` | `integer` | Yes | Positive integer | Size of the payload in bytes | FR-003, FR-004, NFR-104, NFR-105 | SD-002 |
| `data` | `string` or `URL` | Yes | Base64 string or URL | Payload data (base64 for direct, URL for link) | FR-003, FR-004, FR-008, FR-009, FR-010 | SD-001, SD-003 |
| `metadata` | `object` | No | Any JSON object | Payload-level metadata | NFR-401 | SD-008 |
| SP ID | Field | Type | Required | Validation | Description | Requirement ID | Solution Design Ref |
|-------|------|------|----------|------------|-------------|----------------|-------------------|
| SP-002 | `id` | `string` | Yes | UUID v4 format | Unique identifier for this payload | FR-012 | SD-001 |
| SP-002 | `dataname` | `string` | Yes | Non-empty string | Name of the payload (e.g., `login_image`, `user_data`) | FR-001, FR-002, FR-003 | SD-004 |
| SP-002 | `payload_type` | `string` | Yes | Enum | Type of payload (see `payload_type` enum) | FR-001, FR-002, FR-003, FR-006 | SD-004, SD-005 |
| SP-002 | `transport` | `string` | Yes | Enum | Transport method: `direct` or `link` | FR-003, FR-004, NFR-104, NFR-105 | SD-001, SD-002 |
| SP-002 | `encoding` | `string` | Yes | Enum | Encoding method (see `encoding` enum) | FR-001, FR-002, FR-003, FR-012 | SD-005 |
| SP-002 | `size` | `integer` | Yes | Positive integer | Size of the payload in bytes | FR-003, FR-004, NFR-104, NFR-105 | SD-002 |
| SP-002 | `data` | `string` or `URL` | Yes | Base64 string or URL | Payload data (base64 for direct, URL for link) | FR-003, FR-004, FR-008, FR-009, FR-010 | SD-001, SD-003 |
| SP-002 | `metadata` | `object` | No | Any JSON object | Payload-level metadata | NFR-401 | SD-008 |
---
## Payload Format
## Payload Format (SP-003)
### Tuple Format for `smartpack()`
@@ -210,7 +284,7 @@ await smartpack("/agent/v1/process", data)
---
## Enumerations
## Enumerations (SP-004..SP-007)
### `msg_purpose` Enum
@@ -230,8 +304,8 @@ await smartpack("/agent/v1/process", data)
|-------|-------------|---------------------|------------------|
| `text` | Plain text string | All | `base64` | FR-001, FR-012 | SD-004, SD-005 |
| `dictionary` | JSON object/dictionary | All | `base64`, `json` | FR-002, FR-012 | SD-004, SD-005 |
| `arrowtable` | Apache Arrow IPC table | Desktop (Julia/Python/Node.js/Dart) | `base64`, `arrow-ipc` | FR-002 | SD-004, SD-005 |
| `jsontable` | JSON array of objects | All (including Browser/Dart Web) | `base64`, `json` | FR-002, FR-006 | SD-004, SD-005 |
| `arrowtable` | Apache Arrow IPC table | Desktop (Julia/Python) | `base64`, `arrow-ipc` | FR-002 | SD-004, SD-005 |
| `jsontable` | JSON array of objects | All (including Browser) | `base64`, `json` | FR-002, FR-006 | SD-004, SD-005 |
| `image` | Binary image data | All | `base64` | FR-001, FR-006 | SD-004, SD-005 |
| `audio` | Binary audio data | All | `base64` | FR-001, FR-006 | SD-004, SD-005 |
| `video` | Binary video data | All | `base64` | FR-001, FR-006 | SD-004, SD-005 |
@@ -255,9 +329,9 @@ await smartpack("/agent/v1/process", data)
---
## Transport Protocols
## Transport Protocols (SP-008, SP-009)
### Direct Transport Protocol
### Direct Transport Protocol (SP-008)
When `transport = "direct"`, the `data` field contains a Base64-encoded string of the serialized payload.
@@ -276,7 +350,7 @@ When `transport = "direct"`, the `data` field contains a Base64-encoded string o
}
```
### Link Transport Protocol
### Link Transport Protocol (SP-009)
When `transport = "link"`, the `data` field contains a URL pointing to the uploaded payload.
@@ -297,13 +371,13 @@ When `transport = "link"`, the `data` field contains a URL pointing to the uploa
---
## Size Thresholds
## Size Thresholds (SP-010)
### Desktop Platforms (Julia/JS/Python)
| Platform | Size Threshold | Notes | Requirement ID | Solution Design Ref |
|----------|----------------|-------|----------------|-------------------|
| Desktop | 500,000 bytes (0.5MB) | Default threshold | FR-004, FR-005, NFR-104, NFR-105 | SD-002 |
| Desktop | 10,000 bytes (10KB) | Default threshold | FR-004, FR-005, NFR-104, NFR-105 | SD-002 |
### MicroPython Platform
@@ -313,7 +387,18 @@ When `transport = "link"`, the `data` field contains a URL pointing to the uploa
---
## Topic Convention
### Rate Limiting & Quotas (SP-010.1)
Per the ASG Framework content guidelines, this section defines rate-limit and quota behavior. msghandler is a **transport-agnostic bridge** and does **not** implement rate limiting or quotas itself; those are governed by the underlying transport layer (broker) and the file server.
| Level | Rate Limit / Quota | Owner | Notes | SP ID |
|-------|--------------------|-------|-------|-------|
| Transport (broker) | Publish/subscribe rate limits defined by the broker | Message broker (NATS / MQTT / WebSocket) | Enforced by the broker, not by msghandler | SP-010.1 |
| File server | Per-file and per-session upload/download size limits | File server (Plik or custom upload/download handler) | Enforced by the upload/download handler | SP-010.1 |
| Payload | 10,000-byte default threshold (direct vs link) | msghandler (`size_threshold`, configurable) | Applied per payload in `smartpack()` | SP-010 |
| MicroPython | 50,000-byte hard per-payload limit | msghandler | Hard limit due to memory constraints | SP-010 |
## Topic Convention (SP-011)
### Subject Naming Pattern
@@ -335,9 +420,9 @@ When `transport = "link"`, the `data` field contains a URL pointing to the uploa
---
## Error Handling
## Error Handling (SP-012..SP-014)
### Error Response Format
### Error Response Format (SP-012)
```json
{
@@ -349,7 +434,7 @@ When `transport = "link"`, the `data` field contains a URL pointing to the uploa
}
```
### Error Codes
### Error Codes (SP-013)
| Code | HTTP Status | Description | Recovery | Requirement ID |
|------|-------------|-------------|----------|----------------|
@@ -362,7 +447,7 @@ When `transport = "link"`, the `data` field contains a URL pointing to the uploa
| `DESERIALIZATION_ERROR` | 500 | Payload deserialization failed | Check payload_type matches data | FR-001, FR-002, FR-003, FR-012 | SD-004, SD-005 |
| `SIZE_EXCEEDED` | 413 | Payload exceeds maximum size | Split payload or use link transport | FR-003, FR-004, FR-005, NFR-104, NFR-105 | SD-001, SD-002 |
### Exception Handling
### Exception Handling (SP-014)
| Scenario | Handler | Retry Policy | Requirement ID |
|----------|---------|--------------|----------------|
@@ -373,7 +458,7 @@ When `transport = "link"`, the `data` field contains a URL pointing to the uploa
---
## Serialization Rules
## Serialization Rules (SP-015)
### Text Serialization
@@ -410,14 +495,14 @@ When `transport = "link"`, the `data` field contains a URL pointing to the uploa
---
## API Contract
## API Contract (SP-016, SP-017)
| Platform | Function | Requirements | Solution Design Ref |
|----------|----------|--------------|-------------------|
| All | `smartpack()` | FR-001 through FR-014, NFR-101 through NFR-107 | SD-001 through SD-008 |
| All | `smartunpack()` | FR-001 through FR-014, NFR-101 through NFR-107 | SD-001 through SD-008 |
### `smartpack` Function Signature
### `smartpack` Function Signature (SP-016)
#### Julia
@@ -452,7 +537,7 @@ async def smartpack(
broker_url: str = DEFAULT_BROKER_URL,
fileserver_url: str = "http://localhost:8080",
fileserver_upload_handler: Callable = plik_oneshot_upload,
size_threshold: int = 500_000,
size_threshold: int = 10_000,
correlation_id: str = None,
msg_purpose: str = "chat",
sender_name: str = "msghandler",
@@ -467,31 +552,7 @@ async def smartpack(
**Note**: Publishing via the transport layer is the caller's responsibility.
#### JavaScript (Node.js)
```typescript
async function smartpack(
subject: string,
data: Array<[string, any, string]>,
options?: {
broker_url?: string;
fileserver_url?: string;
fileserver_upload_handler?: Function;
size_threshold?: number;
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;
}
): Promise<[Object, string]>;
```
**Note**: Publishing via the transport layer is the caller's responsibility.
#### JavaScript (Browser)
@@ -532,55 +593,6 @@ def smartpack(
**Note**: Publishing via the transport layer is the caller's responsibility.
#### Dart (Desktop/Flutter)
```dart
Future<[Map<String, dynamic>, String]> smartpack(
String subject,
List<List<dynamic>> data, {
String brokerUrl = DEFAULT_BROKER_URL,
String fileserverUrl = DEFAULT_FILESERVER_URL,
Function? fileserverUploadHandler,
int sizeThreshold = DEFAULT_SIZE_THRESHOLD,
String? correlationId,
String msgPurpose = 'chat',
String senderName = 'msghandler',
String receiverName = '',
String receiverId = '',
String replyTo = '',
String replyToMsgId = '',
String? msgId,
String? senderId,
}) async {
// Returns [envelope, jsonString]
// Publishing via transport layer is caller's responsibility
}
#### Dart Web
```dart
Future<[Map<String, dynamic>, String]> smartpack(
String subject,
List<List<dynamic>> data, {
String brokerUrl = DEFAULT_BROKER_URL,
String fileserverUrl = 'http://localhost:8080',
Function? fileserverUploadHandler,
int sizeThreshold = 500000,
String? correlationId,
String msgPurpose = 'chat',
String senderName = 'msghandler',
String receiverName = '',
String receiverId = '',
String replyTo = '',
String replyToMsgId = '',
String? msgId,
String? senderId,
}) async {
// Returns [envelope, jsonString]
// Publishing via transport layer is caller's responsibility
}
```
#### Rust
```rust
@@ -642,7 +654,7 @@ pub struct MsgEnvelopeV1 {
**Note**: Publishing via the transport layer is the caller's responsibility. Returns `Result<(MsgEnvelopeV1, String), msghandlerError>`. Uses `serde` for JSON serialization.
### `smartunpack` Function Signature
### `smartunpack` Function Signature (SP-017)
**Note**: Input is the JSON string payload from the transport subscription. Returns `(envelope::msg_envelope_v1, payloads::Array{Tuple{String, Any, String}, 1})`.
@@ -676,20 +688,6 @@ async def smartunpack(
**Note**: Input is the JSON string payload from the transport message.
#### JavaScript (Node.js)
```typescript
async function smartunpack(
msg_json_str: string, // JSON string from transport message payload
options?: {
fileserver_download_handler?: Function;
max_retries?: number;
base_delay?: number;
max_delay?: number;
}
): Promise<Object>;
```
#### JavaScript (Browser)
```typescript
@@ -714,36 +712,6 @@ def smartunpack(msg_json_str: str, **kwargs) -> Dict[str, Any]:
**Note**: Input is the JSON string payload from the transport message.
#### Dart (Desktop/Flutter)
```dart
Future<Map<String, dynamic>> smartunpack(
Map<String, dynamic> msg_json_str, // JSON object from transport message payload
{
Function? fileserverDownloadHandler,
int maxRetries = 5,
int baseDelay = 100,
int maxDelay = 5000,
}) async {
// Returns envelope with processed payloads
}
```
#### Dart Web
```dart
Future<Map<String, dynamic>> smartunpack(
Map<String, dynamic> msg_json_str, // JSON object from transport message payload
{
Function? fileserverDownloadHandler,
int maxRetries = 5,
int baseDelay = 100,
int maxDelay = 5000,
}) async {
// Returns envelope with processed payloads
}
```
#### Rust
```rust
@@ -765,11 +733,11 @@ pub struct smartunpackOptions {
---
## File Server Interface
## File Server Interface (SP-018, SP-019)
**Traceability**: FR-008, FR-009, FR-010, NFR-202 | SD-003, SD-007
### Upload Handler Contract
### Upload Handler Contract (SP-018)
**Function Signature**:
```julia
@@ -804,7 +772,7 @@ function fileserver_upload_handler(
| `fileid` | `string` | File identifier within session | FR-008 | SD-003 |
| `url` | `string` | Full download URL | FR-008, FR-009, FR-010 | SD-003, SD-007 |
### Download Handler Contract
### Download Handler Contract (SP-019)
**Function Signature**:
@@ -829,19 +797,19 @@ function fileserver_download_handler(
---
## Platform-Specific Constraints
## Platform-Specific Constraints (SP-020)
**Traceability**: FR-005, FR-006, NFR-106, NFR-107 | SD-002, SD-004
### Desktop (Julia/Python/Node.js/Dart)
### Desktop (Julia/Python)
| Feature | Status | Notes | Requirement ID | Solution Design Ref |
|---------|--------|-------|----------------|-------------------|
| Arrow IPC | ✅ Supported | Requires Arrow.jl/pyarrow/dart-arrow | FR-002, FR-012 | SD-004, SD-005 |
| Arrow IPC | ✅ Supported | Requires Arrow.jl/pyarrow | FR-002, FR-012 | SD-004, SD-005 |
| JSON table | ✅ Supported | Human-readable format | FR-002, FR-006 | SD-004, SD-005 |
| File server upload | ✅ Supported | HTTP/HTTPS | FR-008, FR-009 | SD-003 |
| File server download | ✅ Supported | HTTP/HTTPS | FR-010, NFR-202 | SD-003, SD-007 |
| Size threshold | 500KB | Configurable | FR-004, FR-005, NFR-104, NFR-105 | SD-002 |
| Size threshold | 10,000 bytes (10KB) | Configurable | FR-004, FR-005, NFR-104, NFR-105 | SD-002 |
### Browser (JavaScript)
@@ -851,37 +819,7 @@ function fileserver_download_handler(
| JSON table | ✅ Supported | Only table type available in browser | FR-002, FR-006 | SD-004, SD-005 |
| File server upload | ✅ Supported | HTTP/HTTPS | FR-008, FR-009 | SD-003 |
| File server download | ✅ Supported | HTTP/HTTPS | FR-010, NFR-202 | SD-003, SD-007 |
| Size threshold | 500KB | Configurable | FR-004, FR-005, NFR-104, NFR-105 | SD-002 |
### Dart Desktop (Dart SDK)
| Feature | Status | Notes | Requirement ID | Solution Design Ref |
|---------|--------|-------|----------------|-------------------|
| Arrow IPC | ✅ Supported | Requires dart-arrow package | FR-002, FR-012 | SD-004, SD-005 |
| JSON table | ✅ Supported | Human-readable format | FR-002, FR-006 | SD-004, SD-005 |
| File server upload | ✅ Supported | HTTP/HTTPS | FR-008, FR-009 | SD-003 |
| File server download | ✅ Supported | HTTP/HTTPS | FR-010, NFR-202 | SD-003, SD-007 |
| Size threshold | 500KB | Configurable | FR-004, FR-005, NFR-104, NFR-105 | SD-002 |
### Dart Flutter (Dart SDK)
| Feature | Status | Notes | Requirement ID | Solution Design Ref |
|---------|--------|-------|----------------|-------------------|
| Arrow IPC | ✅ Supported | Requires dart-arrow package | FR-002, FR-012 | SD-004, SD-005 |
| JSON table | ✅ Supported | Human-readable format | FR-002, FR-006 | SD-004, SD-005 |
| File server upload | ✅ Supported | HTTP/HTTPS | FR-008, FR-009 | SD-003 |
| File server download | ✅ Supported | HTTP/HTTPS | FR-010, NFR-202 | SD-003, SD-007 |
| Size threshold | 500KB | Configurable | FR-004, FR-005, NFR-104, NFR-105 | SD-002 |
### Dart Web (Dart SDK)
| Feature | Status | Notes | Requirement ID | Solution Design Ref |
|---------|--------|-------|----------------|-------------------|
| Arrow IPC | ❌ Not supported | Apache Arrow not browser-compatible | FR-002 | SD-004, SD-005 |
| JSON table | ✅ Supported | Only table type available in browser | FR-002, FR-006 | SD-004, SD-005 |
| File server upload | ✅ Supported | HTTP/HTTPS | FR-008, FR-009 | SD-003 |
| File server download | ✅ Supported | HTTP/HTTPS | FR-010, NFR-202 | SD-003, SD-007 |
| Size threshold | 500KB | Configurable | FR-004, FR-005, NFR-104, NFR-105 | SD-002 |
| Size threshold | 10,000 bytes (10KB) | Configurable | FR-004, FR-005, NFR-104, NFR-105 | SD-002 |
### Rust
@@ -891,7 +829,7 @@ function fileserver_download_handler(
| JSON table | ✅ Supported | Uses `serde_json` | FR-002, FR-006 | SD-004, SD-005 |
| File server upload | ✅ Supported | HTTP/HTTPS via `reqwest` | FR-008, FR-009 | SD-003 |
| File server download | ✅ Supported | HTTP/HTTPS via `reqwest` with retry | FR-010, NFR-202 | SD-003, SD-007 |
| Size threshold | 500KB | Configurable | FR-004, FR-005, NFR-104, NFR-105 | SD-002 |
| Size threshold | 10,000 bytes (10KB) | Configurable | FR-004, FR-005, NFR-104, NFR-105 | SD-002 |
| Async runtime | ✅ Supported | Uses `tokio` for async I/O | FR-013, FR-014 | SD-006 |
| Type safety | ✅ Supported | Compile-time type checking via Rust enums | FR-006, FR-007 | SD-004 |
@@ -908,17 +846,15 @@ function fileserver_download_handler(
---
## Implementation Files
## Implementation Files (SP-021)
**Traceability**: FR-001 through FR-007 | SD-001 through SD-008
| File | Platform | Features | Notes | Requirement ID | Solution Design Ref |
|------|----------|----------|-------|----------------|-------------------|
| [`src/msghandler.jl`](../src/msghandler.jl) | Julia | Full feature set, Arrow IPC, multiple dispatch | Ground truth implementation | FR-001 through FR-014, NFR-101 through NFR-107 | SD-001 through SD-008 |
| [`src/msghandler_ssr.js`](../src/msghandler_ssr.js) | Node.js | Arrow IPC, async/await | Server-side JavaScript | FR-001 through FR-014, NFR-101 through NFR-107 | SD-001 through SD-008 |
| [`src/msghandler_csr.js`](../src/msghandler_csr.js) | Browser | JSON table only | Client-side rendering | FR-001 through FR-014, NFR-101 through NFR-107 | SD-001 through SD-008 |
| [`src/msghandler.py`](../src/msghandler.py) | Python | Arrow IPC, async/await | Desktop Python | FR-001 through FR-014, NFR-101 through NFR-107 | SD-001 through SD-008 |
| [`src/msghandler.dart`](../src/msghandler.dart) | Dart | Full feature set, Arrow IPC, async/await | Desktop/Flutter/Web | FR-001 through FR-014, NFR-101 through NFR-107 | SD-001 through SD-008 |
| [`src/msghandler.rs`](../src/msghandler.rs) | Rust | Full feature set, Arrow IPC, async/await, type-safe | Uses tokio + serde + arrow2 | FR-001 through FR-014, NFR-101 through NFR-107 | SD-001 through SD-008 |
| [`src/msghandler_mpy.py`](../src/msghandler_mpy.py) | MicroPython | Limited to direct transport | Memory-constrained | FR-005, FR-006 | SD-002, SD-004 |
@@ -936,20 +872,20 @@ The browser implementation ([`src/msghandler_csr.js`](../src/msghandler_csr.js))
### Payload Type Availability by Platform
| Payload Type | Julia | Node.js | Browser | Python | Dart | Rust | MicroPython |
|--------------|-------|---------|---------|--------|------|------|-------------|
| `text` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| `dictionary` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| `arrowtable` | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ |
| `jsontable` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ⚠️ |
| `image` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| `audio` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| `video` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| `binary` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Payload Type | Julia | Browser | Python | Rust | MicroPython |
|--------------|-------|---------|--------|------|-------------|
| `text` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `dictionary` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `arrowtable` | ✅ | ❌ | ✅ | ✅ | ❌ |
| `jsontable` | ✅ | ✅ | ✅ | ✅ | ⚠️ |
| `image` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `audio` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `video` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `binary` | ✅ | ✅ | ✅ | ✅ | ✅ |
---
## Message Flow
## Message Flow (SP-022)
### Sending Flow
@@ -995,7 +931,7 @@ flowchart TD
---
## Validation Rules
## Validation Rules (SP-023)
**Traceability**: FR-001 through FR-014, NFR-101 through NFR-405 | SD-001 through SD-008
@@ -1021,7 +957,7 @@ flowchart TD
---
## Test Contracts
## Test Contracts (SP-024)
**Traceability**: FR-001 through FR-014, NFR-101 through NFR-107 | SD-001 through SD-008
@@ -1065,7 +1001,7 @@ flowchart TD
---
## Dependencies
## Dependencies (SP-025)
### Required Dependencies by Platform
@@ -1075,13 +1011,9 @@ flowchart TD
| Julia | Arrow.jl | Latest | Arrow IPC support | FR-002, FR-012 | SD-004, SD-005 |
| Julia | HTTP.jl | Latest | HTTP file server | FR-008, FR-009 | SD-003 |
| Julia | UUIDs.jl | Latest | UUID generation | FR-011, NFR-401 | SD-008 |
| Node.js | node-fetch | Latest | HTTP file server | FR-008, FR-009 | SD-003 |
| Browser | - | - | Transport-agnostic (caller provides) | FR-013, FR-014 | SD-006 |
| Python | aiohttp | Latest | HTTP file server | FR-008, FR-009 | SD-003 |
| Python | pyarrow | Latest | Arrow IPC support | FR-002, FR-012 | SD-004, SD-005 |
| Dart | http | Latest | HTTP file server | FR-008, FR-009 | SD-003 |
| Dart | uuid | Latest | UUID generation | FR-011, NFR-401 | SD-008 |
| Dart | dart-arrow | Latest | Arrow IPC support | FR-002, FR-012 | SD-004, SD-005 |
| Rust | serde | Latest | JSON serialization | FR-012, NFR-101, NFR-102 | SD-005 |
| Rust | serde_json | Latest | JSON handling | FR-012, NFR-101, NFR-102 | SD-005 |
| Rust | tokio | Latest | Async runtime | FR-013, FR-014 | SD-006 |
@@ -1103,6 +1035,7 @@ flowchart TD
| Date | Version | Changes |
|------|---------|---------|
| 2026-09-16 | 1.4.0 | Aligned to ASG Framework eight pillars; added SP-XXX Specification Item IDs, Specification Traceability Matrix, section-level SP references, and Rate Limiting & Quotas (SP-010.1) |
| 2026-05-15 | 1.3.0 | Made transport layer agnostic |
| - | - | Removed all NATS-specific dependencies (NATS.jl, nats, nats-py, nats.ws) |
| - | - | Updated docs to reference generic message broker/transport |
@@ -1146,10 +1079,8 @@ flowchart TD
| File | Platform | Features | Requirements Traceability | Solution Design Traceability |
|------|----------|----------|--------------------------|------------------------------|
| [`src/msghandler.jl`](../src/msghandler.jl) | Julia | Full feature set, Arrow IPC, multiple dispatch | FR-001 through FR-014, NFR-101 through NFR-405 | SD-001 through SD-008 |
| [`src/msghandler_ssr.js`](../src/msghandler_ssr.js) | Node.js | Arrow IPC, async/await | FR-001 through FR-014, NFR-101 through NFR-405 | SD-001 through SD-008 |
| [`src/msghandler_csr.js`](../src/msghandler_csr.js) | Browser | JSON table only | FR-001 through FR-014, NFR-101 through NFR-405 | SD-001 through SD-008 |
| [`src/msghandler.py`](../src/msghandler.py) | Python | Arrow IPC, async/await | FR-001 through FR-014, NFR-101 through NFR-405 | SD-001 through SD-008 |
| [`src/msghandler.dart`](../src/msghandler.dart) | Dart | Full feature set, Arrow IPC, async/await | FR-001 through FR-014, NFR-101 through NFR-405 | SD-001 through SD-008 |
| [`src/msghandler.rs`](../src/msghandler.rs) | Rust | Full feature set, Arrow IPC, async/await, type-safe | FR-001 through FR-014, NFR-101 through NFR-405 | SD-001 through SD-008 |
| [`src/msghandler_mpy.py`](../src/msghandler_mpy.py) | MicroPython | Limited to direct transport | FR-005, FR-006, FR-012 | SD-002, SD-004 |
@@ -1161,13 +1092,9 @@ flowchart TD
| Julia | Arrow.jl | Latest | Arrow IPC support | FR-002, FR-012 | SD-004, SD-005 |
| Julia | HTTP.jl | Latest | HTTP file server | FR-008, FR-009 | SD-003 |
| Julia | UUIDs.jl | Latest | UUID generation | FR-011, NFR-401 | SD-008 |
| Node.js | node-fetch | Latest | HTTP file server | FR-008, FR-009 | SD-003 |
| Browser | - | - | Transport-agnostic (caller provides) | FR-013, FR-014 | SD-006 |
| Python | aiohttp | Latest | HTTP file server | FR-008, FR-009 | SD-003 |
| Python | pyarrow | Latest | Arrow IPC support | FR-002, FR-012 | SD-004, SD-005 |
| Dart | http | Latest | HTTP file server | FR-008, FR-009 | SD-003 |
| Dart | uuid | Latest | UUID generation | FR-011, NFR-401 | SD-008 |
| Dart | dart-arrow | Latest | Arrow IPC support | FR-002, FR-012 | SD-004, SD-005 |
| Rust | serde | Latest | JSON serialization | FR-012, NFR-101, NFR-102 | SD-005 |
| Rust | serde_json | Latest | JSON handling | FR-012, NFR-101, NFR-102 | SD-005 |
| Rust | tokio | Latest | Async runtime | FR-013, FR-014 | SD-006 |
@@ -1182,6 +1109,7 @@ flowchart TD
| Date | Version | Changes | Requirement ID(s) | Solution Design Ref(s) |
|------|---------|---------|-------------------|------------------------|
| 2026-09-16 | 1.4.0 | Aligned to ASG Framework eight pillars; assigned SP-XXX IDs to all spec items; added Specification Traceability Matrix, Rate Limiting & Quotas (SP-010.1); fixed desktop size threshold to 10,000 bytes per ground truth | All | All SD-XXX |
| 2026-05-22 | 1.3.0 | Updated to ASG Framework v8 pillars | All | All SD-XXX |
| - | - | Added Solution Design references to all specification items | All | All SD-XXX |
| - | - | Updated version to 1.3.0 to match requirements and solution-design | All | - |
-416
View File
@@ -1,416 +0,0 @@
# 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.*
-1278
View File
File diff suppressed because it is too large Load Diff
+44 -4
View File
@@ -27,9 +27,9 @@
// ---------------------------------------------- Constants ---------------------------------------------- //
/**
* Default size threshold for switching from direct to link transport (0.5MB)
* Default size threshold for switching from direct to link transport (10KB)
*/
const DEFAULT_SIZE_THRESHOLD = 500_000;
const DEFAULT_SIZE_THRESHOLD = 10_000;
/**
* Default broker URL
@@ -39,7 +39,7 @@ const DEFAULT_BROKER_URL = 'localhost:4222';
/**
* Default HTTP file server URL for link transport
*/
const DEFAULT_FILESERVER_URL = 'http://localhost:8080';
const DEFAULT_FILESERVER_URL = 'https://fileserver.yiem.cc';
// ---------------------------------------------- Utility Functions ---------------------------------------------- //
@@ -50,7 +50,11 @@ const DEFAULT_FILESERVER_URL = 'http://localhost:8080';
*/
function bufferToBase64(data) {
const bytes = new Uint8Array(data);
const binary = String.fromCharCode(...bytes);
let binary = '';
const chunkSize = 8192;
for (let i = 0; i < bytes.length; i += chunkSize) {
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunkSize));
}
return btoa(binary);
}
@@ -199,6 +203,37 @@ async function deserializeData(data, payloadType, correlationId) {
// ---------------------------------------------- File Server Handlers ---------------------------------------------- //
/**
* Upload data to a pre-signed S3 URL
* @param {string} signUrl - Pre-signed S3 URL (complete URL, not base) or empty string
* @param {string} dataname - The S3 key / object path
* @param {Uint8Array} data - Raw bytes to upload
* @param {Object} options - Additional options
* @param {string} [options.content_type="application/json"] - MIME type for the PUT request
* @returns {Promise<{status: number, uploadid: string, fileid: string, url: string}>}
*/
async function s3UploadHandler(signUrl, dataname, data, options = {}) {
const { content_type = 'application/json' } = options;
const buffer = data instanceof Uint8Array ? data : new Uint8Array(data);
if (signUrl !== '' && signUrl !== undefined) {
const response = await fetch(signUrl, {
method: 'PUT',
headers: { 'Content-Type': content_type },
body: buffer
});
return {
status: response.status,
uploadid: '',
fileid: '',
url: dataname
};
} else {
throw new Error('No pre-signed URL available for S3 upload');
}
}
/**
* Upload data to plik server in one-shot mode
* @param {string} fileServerUrl - Base URL of the plik server
@@ -616,6 +651,11 @@ const msghandlerCSR = {
*/
plikOneshotUpload,
/**
* Upload data to a pre-signed S3 URL
*/
s3UploadHandler,
/**
* Fetch data from URL with exponential backoff
*/
-703
View File
@@ -1,703 +0,0 @@
/**
* msghandler - Cross-Platform Bi-Directional Data Bridge
* JavaScript/Node.js Implementation (Desktop/Server-Side)
*
* This module provides functionality for sending and receiving data across network boundaries
* with support for both direct payload transport and URL-based transport for larger payloads.
*
* Supported payload types: "text", "dictionary", "arrowtable", "jsontable", "image", "audio", "video", "binary"
*
* Node.js-specific features:
* - Apache Arrow IPC support via apache-arrow
* - Buffer for binary data handling
* - Native fetch for HTTP operations
*
* @module msghandler
*/
const crypto = require('crypto');
// Use native fetch available in Node.js 18+
const arrow = require('apache-arrow');
// ---------------------------------------------- UUID Helper ---------------------------------------------- //
/**
* Generate UUID v4 using crypto module (Node.js compatible)
* @returns {string} UUID string
*/
function uuidv4() {
return crypto.randomUUID();
}
// ---------------------------------------------- Constants ---------------------------------------------- //
/**
* Default size threshold for switching from direct to link transport (0.5MB)
*/
const DEFAULT_SIZE_THRESHOLD = 500_000;
/**
* Default broker URL
*/
const DEFAULT_BROKER_URL = 'localhost:4222';
/**
* Default HTTP file server URL for link transport
*/
const DEFAULT_FILESERVER_URL = 'http://localhost:8080';
// ---------------------------------------------- Utility Functions ---------------------------------------------- //
/**
* Convert Buffer to Base64 string
* @param {Buffer} buffer - Buffer to encode
* @returns {string} Base64 encoded string
*/
function bufferToBase64(buffer) {
return buffer.toString('base64');
}
/**
* Log a trace message with correlation ID and timestamp
* @param {string} correlationId - Correlation ID for tracing
* @param {string} message - Message content to log
*/
function logTrace(correlationId, message) {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [Correlation: ${correlationId}] ${message}`);
}
// ---------------------------------------------- Serialization Functions ---------------------------------------------- //
/**
* Serialize data according to specified format
* @param {any} data - Data to serialize
* @param {string} payloadType - Target format: "text", "dictionary", "arrowtable", "jsontable", "image", "audio", "video", "binary"
* @returns {Buffer} Binary representation of the serialized data
*/
async function serializeData(data, payloadType) {
if (payloadType === 'text') {
if (typeof data === 'string') {
return Buffer.from(data, 'utf8');
} else {
throw new Error('Text data must be a string');
}
} else if (payloadType === 'dictionary') {
const jsonStr = JSON.stringify(data);
return Buffer.from(jsonStr, 'utf8');
} else if (payloadType === 'arrowtable') {
// Convert array of objects to Arrow IPC format
if (!Array.isArray(data) || data.length === 0) {
throw new Error('Arrow table data must be a non-empty array of objects');
}
return serializeArrowTable(data);
} else if (payloadType === 'jsontable') {
// Serialize array of objects to JSON format
if (!Array.isArray(data)) {
throw new Error('JSON table data must be an array');
}
const jsonStr = JSON.stringify(data);
return Buffer.from(jsonStr, 'utf8');
} else if (payloadType === 'image') {
if (data instanceof Uint8Array || Buffer.isBuffer(data)) {
return Buffer.from(data);
} else {
throw new Error('Image data must be Uint8Array or Buffer');
}
} else if (payloadType === 'audio') {
if (data instanceof Uint8Array || Buffer.isBuffer(data)) {
return Buffer.from(data);
} else {
throw new Error('Audio data must be Uint8Array or Buffer');
}
} else if (payloadType === 'video') {
if (data instanceof Uint8Array || Buffer.isBuffer(data)) {
return Buffer.from(data);
} else {
throw new Error('Video data must be Uint8Array or Buffer');
}
} else if (payloadType === 'binary') {
if (data instanceof Uint8Array || Buffer.isBuffer(data)) {
return Buffer.from(data);
} else {
throw new Error('Binary data must be Uint8Array or Buffer');
}
} else {
throw new Error(`Unknown payload_type: ${payloadType}`);
}
}
/**
* Helper function to properly serialize table data to Arrow IPC
* @param {Array<Object>} data - Array of objects representing table rows
* @returns {Buffer} Arrow IPC formatted buffer
*/
function serializeArrowTable(data) {
if (!Array.isArray(data) || data.length === 0) {
throw new Error('Table data must be a non-empty array of objects');
}
logTrace('serializeArrowTable', `Serializing table with ${data.length} rows`);
// Use arrow.tableFromArrays which handles the conversion properly
// Convert array of objects to a key-value format expected by tableFromArrays
const columns = {};
for (const key of Object.keys(data[0])) {
columns[key] = data.map(row => row[key]);
}
logTrace('serializeArrowTable', `Columns: ${Object.keys(columns).join(', ')}`);
const table = arrow.tableFromArrays(columns);
logTrace('serializeArrowTable', `Arrow table created with ${table.numRows} rows, ${table.numCols} cols`);
// Convert to IPC format
const ipcBuffer = arrow.tableToIPC(table);
logTrace('serializeArrowTable', `IPC buffer type: ${typeof ipcBuffer}, length: ${ipcBuffer.byteLength}`);
const resultBuffer = Buffer.from(ipcBuffer);
logTrace('serializeArrowTable', `Result buffer: ${resultBuffer.length} bytes`);
// Debug: Show first 20 bytes in hex
const hexPreview = resultBuffer.slice(0, 20).toString('hex');
logTrace('serializeArrowTable', `First 20 bytes (hex): ${hexPreview}`);
return resultBuffer;
}
/**
* Deserialize bytes to data based on type
* @param {Buffer|Uint8Array} data - Serialized data as bytes
* @param {string} payloadType - Data type
* @param {string} correlationId - Correlation ID for logging
* @returns {any} Deserialized data
*/
async function deserializeData(data, payloadType, correlationId) {
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
logTrace(correlationId, `deserializeData: type=${payloadType}, bufferLength=${buffer.length}`);
// Debug: Show first 20 bytes in hex for binary data
if (payloadType === 'arrowtable' || payloadType === 'jsontable' || payloadType === 'image' || payloadType === 'binary') {
const hexPreview = buffer.slice(0, 20).toString('hex');
logTrace(correlationId, `deserializeData: First 20 bytes (hex): ${hexPreview}`);
}
if (payloadType === 'text') {
const result = buffer.toString('utf8');
logTrace(correlationId, `deserializeData: text result length=${result.length}`);
return result;
} else if (payloadType === 'dictionary') {
const jsonStr = buffer.toString('utf8');
const result = JSON.parse(jsonStr);
logTrace(correlationId, `deserializeData: dictionary keys=${Object.keys(result).join(', ')}`);
return result;
} else if (payloadType === 'arrowtable') {
logTrace(correlationId, `deserializeData: Attempting Arrow table deserialization`);
// Debug: Check available arrow methods
logTrace(correlationId, `deserializeData: arrow.tableFromRawBytes exists: ${typeof arrow.tableFromRawBytes}`);
logTrace(correlationId, `deserializeData: arrow.tableFromIPC exists: ${typeof arrow.tableFromIPC}`);
try {
// Try tableFromRawBytes first (older API)
if (typeof arrow.tableFromRawBytes === 'function') {
logTrace(correlationId, `deserializeData: Using tableFromRawBytes`);
const table = arrow.tableFromRawBytes(buffer);
logTrace(correlationId, `deserializeData: Arrow table - rows=${table.numRows}, cols=${table.numCols}`);
return table;
}
} catch (e) {
logTrace(correlationId, `deserializeData: tableFromRawBytes failed: ${e.message}`);
}
try {
// Try tableFromIPC (newer API)
if (typeof arrow.tableFromIPC === 'function') {
logTrace(correlationId, `deserializeData: Using tableFromIPC`);
const table = arrow.tableFromIPC(buffer);
logTrace(correlationId, `deserializeData: Arrow table from IPC - rows=${table.numRows}, cols=${table.numCols}`);
return table;
}
} catch (e) {
logTrace(correlationId, `deserializeData: tableFromIPC failed: ${e.message}`);
}
throw new Error(`Unable to deserialize Arrow table: neither tableFromRawBytes nor tableFromIPC worked`);
} else if (payloadType === 'jsontable') {
const jsonStr = buffer.toString('utf8');
const result = JSON.parse(jsonStr);
logTrace(correlationId, `deserializeData: jsontable result length=${Array.isArray(result) ? result.length : 'N/A'}`);
return result;
} else if (payloadType === 'image') {
logTrace(correlationId, `deserializeData: image buffer length=${buffer.length}`);
return buffer;
} else if (payloadType === 'audio') {
logTrace(correlationId, `deserializeData: audio buffer length=${buffer.length}`);
return buffer;
} else if (payloadType === 'video') {
logTrace(correlationId, `deserializeData: video buffer length=${buffer.length}`);
return buffer;
} else if (payloadType === 'binary') {
logTrace(correlationId, `deserializeData: binary buffer length=${buffer.length}`);
return buffer;
} else {
throw new Error(`Unknown payload_type: ${payloadType}`);
}
}
// ---------------------------------------------- File Server Handlers ---------------------------------------------- //
/**
* Upload data to plik server in one-shot mode
* @param {string} fileServerUrl - Base URL of the plik server
* @param {string} dataname - Name of the file being uploaded
* @param {Buffer|Uint8Array} data - Raw byte data of the file content
* @returns {Promise<{status: number, uploadid: string, fileid: string, url: string}>}
*/
async function plikOneshotUpload(fileServerUrl, dataname, data) {
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
// Get upload id
const urlGetUploadID = `${fileServerUrl}/upload`;
const headers = { 'Content-Type': 'application/json' };
const body = JSON.stringify({ OneShot: true });
const httpResponse = await fetch(urlGetUploadID, {
method: 'POST',
headers,
body
});
const responseJson = await httpResponse.json();
const uploadid = responseJson.id;
const uploadtoken = responseJson.uploadToken;
// Upload file
const urlUpload = `${fileServerUrl}/file/${uploadid}`;
const form = new FormData();
const blob = new Blob([buffer], { type: 'application/octet-stream' });
form.append('file', blob, dataname);
const uploadHeaders = {
'X-UploadToken': uploadtoken
};
const uploadResponse = await fetch(urlUpload, {
method: 'POST',
headers: uploadHeaders,
body: form
});
const uploadJson = await uploadResponse.json();
const fileid = uploadJson.id;
const url = `${fileServerUrl}/file/${uploadid}/${fileid}/${dataname}`;
return {
status: uploadResponse.status,
uploadid,
fileid,
url
};
}
/**
* Fetch data from URL with exponential backoff
* @param {string} url - URL to fetch from
* @param {number} maxRetries - Maximum number of retry attempts
* @param {number} baseDelay - Initial delay in milliseconds
* @param {number} maxDelay - Maximum delay in milliseconds
* @param {string} correlationId - Correlation ID for logging
* @returns {Promise<Uint8Array>} Fetched data as bytes
*/
async function fetchWithBackoff(url, maxRetries, baseDelay, maxDelay, correlationId) {
let delay = baseDelay;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url);
if (response.status === 200) {
logTrace(correlationId, `Successfully fetched data from ${url} on attempt ${attempt}`);
const arrayBuffer = await response.arrayBuffer();
return new Uint8Array(arrayBuffer);
} else {
throw new Error(`Failed to fetch: ${response.status}`);
}
} catch (e) {
logTrace(correlationId, `Attempt ${attempt} failed: ${e.constructor.name} - ${e.message}`);
if (attempt < maxRetries) {
await new Promise(resolve => setTimeout(resolve, delay));
delay = Math.min(delay * 2, maxDelay);
}
}
}
throw new Error(`Failed to fetch data after ${maxRetries} attempts`);
}
// ---------------------------------------------- Core Functions ---------------------------------------------- //
/**
* Build message envelope from payloads and metadata
* @param {string} subject - Subject/topic
* @param {Array} payloads - Array of payload objects
* @param {Object} options - Envelope metadata options
* @returns {Object} Envelope object
*/
function buildEnvelope(subject, payloads, options) {
return {
correlation_id: options.correlation_id,
msg_id: options.msg_id,
timestamp: new Date().toISOString(),
send_to: subject,
msg_purpose: options.msg_purpose,
sender_name: options.sender_name,
sender_id: options.sender_id,
receiver_name: options.receiver_name,
receiver_id: options.receiver_id,
reply_to: options.reply_to,
reply_to_msg_id: options.reply_to_msg_id,
broker_url: options.broker_url,
metadata: options.metadata || {},
payloads: payloads
};
}
/**
* Build payload object from serialized data
* @param {string} dataname - Name of the payload
* @param {string} payloadType - Type of the payload
* @param {Buffer} payloadBytes - Serialized payload bytes
* @param {string} transport - Transport type ("direct" or "link")
* @param {string} data - Data (base64 for direct, URL for link)
* @returns {Object} Payload object
*/
function buildPayload(dataname, payloadType, payloadBytes, transport, data) {
// Determine encoding based on payload type (matching Julia implementation)
let encoding = 'base64';
if (payloadType === 'jsontable') {
encoding = 'json';
} else if (payloadType === 'arrowtable') {
encoding = 'arrow-ipc';
}
return {
id: uuidv4(),
dataname,
payload_type: payloadType,
transport,
encoding,
size: payloadBytes.byteLength,
data,
metadata: transport === 'direct' ? { payload_bytes: payloadBytes.byteLength } : {}
};
}
/**
* Send data with automatic transport selection
*
* This function intelligently routes data delivery based on payload size.
* If the serialized payload is smaller than size_threshold, it encodes the data as Base64
* into a "direct" payload. Otherwise, it uploads the data to a fileserver
* 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.
*
* @param {string} subject - Subject/topic to send the message to
* @param {Array} data - List of [dataname, data, type] tuples to send
* - type: "text", "dictionary", "arrowtable", "jsontable", "image", "audio", "video", "binary"
* @param {Object} options - Optional configuration
* @param {string} [options.broker_url=DEFAULT_BROKER_URL] - Broker URL (for envelope metadata)
* @param {string} [options.fileserver_url=DEFAULT_FILESERVER_URL] - URL of the HTTP file server
* @param {Function} [options.fileserver_upload_handler=plikOneshotUpload] - Function to handle fileserver uploads
* @param {number} [options.size_threshold=DEFAULT_SIZE_THRESHOLD] - Threshold separating direct vs link transport
* @param {string} [options.correlation_id=crypto.randomUUID()] - Correlation ID for tracing
* @param {string} [options.msg_purpose="chat"] - Purpose of the message
* @param {string} [options.sender_name="msghandler"] - Name of the sender
* @param {string} [options.receiver_name=""] - Name of the receiver (empty means broadcast)
* @param {string} [options.receiver_id=""] - UUID of the receiver (empty means broadcast)
* @param {string} [options.reply_to=""] - Topic to reply to
* @param {string} [options.reply_to_msg_id=""] - Message ID this message is replying to
* @param {string} [options.msg_id=crypto.randomUUID()] - Message ID
* @param {string} [options.sender_id=crypto.randomUUID()] - Sender ID
* @returns {Promise<[Object, string]>} Tuple of [env, env_json_str]
*
* @example
* // Send a single payload
* const [env, envJsonStr] = await smartpack(
* "/test",
* [["dataname1", data1, "dictionary"]]
* );
*
* // Send multiple payloads
* const [env, envJsonStr] = await smartpack(
* "/test",
* [
* ["dataname1", data1, "dictionary"],
* ["dataname2", data2, "arrowtable"]
* ]
* );
*
* // Publish via your transport (NATS, MQTT, HTTP, etc.)
* // await myNatsClient.publish("/test", envJsonStr);
*/
async function smartpack(subject, data, options = {}) {
const {
broker_url = DEFAULT_BROKER_URL,
fileserver_url = DEFAULT_FILESERVER_URL,
fileserver_upload_handler = plikOneshotUpload,
size_threshold = DEFAULT_SIZE_THRESHOLD,
correlation_id = uuidv4(),
msg_purpose = 'chat',
sender_name = 'msghandler',
receiver_name = '',
receiver_id = '',
reply_to = '',
reply_to_msg_id = '',
msg_id = uuidv4(),
sender_id = uuidv4()
} = options;
logTrace(correlation_id, `Starting smartpack for subject: ${subject}`);
logTrace(correlation_id, `smartpack: data array length=${data.length}`);
// Debug: Log input data structure
for (let i = 0; i < data.length; i++) {
const [dataname, payloadData, payloadType] = data[i];
logTrace(correlation_id, `smartpack: payload[${i}] dataname=${dataname}, type=${payloadType}, data type=${typeof payloadData}, constructor=${payloadData?.constructor?.name}`);
}
// Process payloads
const payloads = [];
for (const [dataname, payloadData, payloadType] of data) {
logTrace(correlation_id, `smartpack: Processing payload '${dataname}' type=${payloadType}`);
logTrace(correlation_id, `smartpack: payloadData type=${typeof payloadData}, constructor=${payloadData?.constructor?.name}`);
const payloadBytes = await serializeData(payloadData, payloadType);
const payloadSize = payloadBytes.byteLength;
logTrace(correlation_id, `Serialized payload '${dataname}' (type: ${payloadType}) size: ${payloadSize} bytes`);
// Debug: Show first 20 bytes of serialized data for table type
if (payloadType === 'table') {
const hexPreview = payloadBytes.slice(0, 20).toString('hex');
logTrace(correlation_id, `Serialized table data first 20 bytes (hex): ${hexPreview}`);
}
if (payloadSize < size_threshold) {
// Direct path
const payloadB64 = bufferToBase64(payloadBytes);
logTrace(correlation_id, `Using direct transport for ${payloadSize} bytes, base64 length=${payloadB64.length}`);
const payload = buildPayload(dataname, payloadType, payloadBytes, 'direct', payloadB64);
payloads.push(payload);
} else {
// Link path
logTrace(correlation_id, `Using link transport, uploading to fileserver`);
const response = await fileserver_upload_handler(fileserver_url, dataname, payloadBytes);
if (response.status !== 200) {
throw new Error(`Failed to upload data to fileserver: ${response.status}`);
}
logTrace(correlation_id, `Uploaded to URL: ${response.url}`);
const payload = buildPayload(dataname, payloadType, payloadBytes, 'link', response.url);
payloads.push(payload);
}
}
// Build envelope
const env = buildEnvelope(subject, payloads, {
correlation_id,
msg_id,
msg_purpose,
sender_name,
sender_id,
receiver_name,
receiver_id,
reply_to,
reply_to_msg_id,
broker_url
});
const env_json_str = JSON.stringify(env);
return [env, env_json_str];
}
/**
* Receive and process messages
*
* This function processes incoming messages, handling both direct transport
* (base64 decoded payloads) and link transport (URL-based payloads).
* It deserializes the data based on the transport type and returns the result.
*
* @param {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 {Function} [options.fileserver_download_handler=fetchWithBackoff] - Function to handle fileserver downloads
* @param {number} [options.max_retries=5] - Maximum retry attempts for fetching URL
* @param {number} [options.base_delay=100] - Initial delay for exponential backoff in ms
* @param {number} [options.max_delay=5000] - Maximum delay for exponential backoff in ms
* @returns {Promise<Object>} Envelope object with processed payloads
*
* @example
* // Receive from JSON string directly
* const env = await smartunpack(jsonString, {
* fileserver_download_handler: fetchWithBackoff,
* max_retries: 5,
* base_delay: 100,
* 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
* for (const [dataname, data, type] of env.payloads) {
* console.log(`${dataname}: ${data} (type: ${type})`);
* }
*/
async function smartunpack(msg, options = {}) {
const {
fileserver_download_handler = fetchWithBackoff,
max_retries = 5,
base_delay = 100,
max_delay = 5000
} = options;
// Handle both raw JSON strings and transport message objects
let payload;
if (typeof msg === 'string') {
payload = msg;
} else if (msg !== null && typeof msg === 'object') {
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 {
throw new Error('Invalid message format: expected JSON string or message object');
}
logTrace('smartunpack', `smartunpack: raw payload length=${payload.length}`);
// Debug: Show first 200 chars of payload
const payloadPreview = payload.substring(0, 200);
logTrace('smartunpack', `smartunpack: payload preview: ${payloadPreview}`);
let envJsonObj;
try {
envJsonObj = JSON.parse(payload);
} catch (e) {
logTrace('smartunpack', `smartunpack: JSON parse failed: ${e.message}`);
throw e;
}
logTrace(envJsonObj.correlation_id, 'Processing received message');
logTrace(envJsonObj.correlation_id, `smartunpack: envelope has ${envJsonObj.payloads.length} payloads`);
// Process all payloads in the envelope
const payloadsList = [];
const numPayloads = envJsonObj.payloads.length;
logTrace(envJsonObj.correlation_id, `smartunpack: Processing ${numPayloads} payloads`);
for (let i = 0; i < numPayloads; i++) {
const payloadObj = envJsonObj.payloads[i];
const transport = payloadObj.transport;
const dataname = payloadObj.dataname;
const payloadType = payloadObj.payload_type;
logTrace(envJsonObj.correlation_id, `smartunpack: Processing payload ${i + 1}/${numPayloads}: dataname=${dataname}, type=${payloadType}, transport=${transport}`);
if (transport === 'direct') {
logTrace(envJsonObj.correlation_id, `Direct transport - decoding payload '${dataname}'`);
// Extract base64 payload from the payload
const payloadB64 = payloadObj.data;
logTrace(envJsonObj.correlation_id, `Direct transport: base64 length=${payloadB64?.length}`);
// Decode Base64 payload
const payloadBytes = Buffer.from(payloadB64, 'base64');
logTrace(envJsonObj.correlation_id, `Direct transport: decoded bytes=${payloadBytes.length}`);
// Deserialize based on type
const dataType = payloadObj.payload_type;
const data = await deserializeData(payloadBytes, dataType, envJsonObj.correlation_id);
logTrace(envJsonObj.correlation_id, `Direct transport: deserialized data type=${typeof data}, constructor=${data?.constructor?.name}`);
payloadsList.push([dataname, data, dataType]);
} else if (transport === 'link') {
// Extract download URL from the payload
const url = payloadObj.data;
logTrace(envJsonObj.correlation_id, `Link transport - fetching '${dataname}' from URL: ${url}`);
// Fetch with exponential backoff using the download handler
const downloadedData = await fileserver_download_handler(
url,
max_retries,
base_delay,
max_delay,
envJsonObj.correlation_id
);
// Deserialize based on type
const dataType = payloadObj.payload_type;
const data = await deserializeData(downloadedData, dataType, envJsonObj.correlation_id);
payloadsList.push([dataname, data, dataType]);
} else {
throw new Error(`Unknown transport type for payload '${dataname}': ${transport}`);
}
}
logTrace(envJsonObj.correlation_id, `smartunpack: Successfully processed all ${payloadsList.length} payloads`);
envJsonObj.payloads = payloadsList;
return envJsonObj;
}
// ---------------------------------------------- Module Exports ---------------------------------------------- //
const msghandler = {
/**
* Send data with automatic transport selection
*/
smartpack,
/**
* Receive and process messages
*/
smartunpack,
/**
* Upload data to plik server in one-shot mode
*/
plikOneshotUpload,
/**
* Fetch data from URL with exponential backoff
*/
fetchWithBackoff,
/**
* Default constants
*/
DEFAULT_SIZE_THRESHOLD,
DEFAULT_BROKER_URL,
DEFAULT_FILESERVER_URL
};
module.exports = msghandler;
+63 -95
View File
@@ -47,9 +47,9 @@ using JSON, Arrow, HTTP, UUIDs, Dates, Base64, PrettyPrinting, DataFrames
# ---------------------------------------------- 100 --------------------------------------------- #
# Constants
const DEFAULT_SIZE_THRESHOLD = 500_000 # 0.5MB - threshold for switching from direct to link transport
const DEFAULT_SIZE_THRESHOLD = 10_000 # 10KB - threshold for switching from direct to link transport
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 = "" # Default HTTP file server URL for link transport
""" msg_payload_v1 - Internal message payload structure
@@ -369,7 +369,7 @@ Transport publishing must be performed by the caller.
# Keyword Arguments:
- `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_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` - Custom upload handler with signature `handler(fileserver_url::String, dataname::String, data::Vector{UInt8})::Dict{String, Any}`. Must return a `Dict` with keys: `"status"` (Int/HTTP status code), `"uploadid"` (String), `"fileid"` (String), `"url"` (String - full download URL).
- `size_threshold::Int = DEFAULT_SIZE_THRESHOLD` - Threshold in bytes separating direct vs link transport
- `correlation_id::String = string(uuid4())` - Correlation ID for tracing (auto-generated UUID)
- `msg_purpose::String = "chat"` - Purpose of the message: "ACK", "NACK", "updateStatus", "shutdown", "chat", etc.
@@ -414,16 +414,25 @@ env, msg_json = smartpack("chat.subject", [
("audio_clip", audio_data, "audio")
])
# Publish the JSON string directly via your transport (manual publish)
# conn = my_transport.connect(broker_url)
# my_transport.publish(conn, subject, env_json_str)
# Custom fileserver upload handler (e.g., for AWS S3)
function my_custom_upload(fileserver_url::String, dataname::String, data::Vector{UInt8})::Dict{String, Any}
# Upload `data` to your file server at `fileserver_url`, using `dataname` as the file name
# Return a Dict with required keys:
# "status" - HTTP status code (e.g. 200)
# "uploadid" - String ID of the upload session
# "fileid" - String ID of the uploaded file
# "url" - Full download URL for the uploaded file
# Example return:
# Dict("status" => 200, "uploadid" => "abc123", "fileid" => "file456", "url" => "http://my-server.com/abc123/file456/myfile.bin")
throw(NotImplementedError("implement upload logic here"))
end
```
"""
function smartpack(
subject::String, # smartunpack's subject
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, # Broker URL
fileserver_url = DEFAULT_FILESERVER_URL,
fileserver_url::String = DEFAULT_FILESERVER_URL,
fileserver_upload_handler::Function = plik_oneshot_upload, # a function to handle uploading data to specific HTTP fileserver
size_threshold::Int = DEFAULT_SIZE_THRESHOLD,
@@ -446,25 +455,26 @@ function smartpack(
)::Tuple{msg_envelope_v1, String} where {T1<:Any}
# Log start of send operation
log_trace(correlation_id, "Starting smartpack for subject: $subject")
# log_trace(correlation_id, "Starting smartpack for subject: $subject")
# Process each payload in the list
payloads = msg_payload_v1[]
for (dataname, payload_data, payload_type) in data
# @show dataname typeof(payload_data)
# @info "msghandler smartpack() 1" @__LINE__
# Serialize data based on type. use bytes as medium for every datatype
payload_bytes = _serialize_data(payload_data, payload_type)
# @info "msghandler smartpack() 2" @__LINE__
payload_size = length(payload_bytes) # Calculate payload size in bytes
log_trace(correlation_id, "Serialized payload '$dataname' (payload_type: $payload_type) size: $payload_size bytes") # Log payload size
# log_trace(correlation_id, "Serialized payload '$dataname' (payload_type: $payload_type) size: $payload_size bytes") # Log payload size
# Decision: Direct vs Link
if payload_size < size_threshold # Check if payload is small enough for direct transport
# Direct path - Base64 encode and include in message envelope
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
# @info "msghandler smartpack() 3" @__LINE__
# Determine encoding based on payload_type
encoding = "base64"
if payload_type == "jsontable"
@@ -486,8 +496,9 @@ function smartpack(
)
push!(payloads, payload)
else
# @info "msghandler smartpack() 4" @__LINE__
# 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
response = fileserver_upload_handler(fileserver_url, dataname, payload_bytes)
@@ -497,7 +508,7 @@ function smartpack(
end
url = response["url"] # URL for the uploaded data
log_trace(correlation_id, "Uploaded to URL: $url") # Log successful upload
# log_trace(correlation_id, "Uploaded to URL: $url") # Log successful upload
# Determine encoding based on payload_type
encoding = "none"
@@ -520,6 +531,7 @@ function smartpack(
)
push!(payloads, payload)
end
# @info "msghandler smartpack() 5" @__LINE__
end
# Create msg_envelope_v1 with all payloads
@@ -549,7 +561,7 @@ function smartpack(
# # Publish message to NATS using existing connection
# publish_message(NATS_connection, subject, env_json_str, correlation_id)
# end
# @info "msghandler smartpack() 6" @__LINE__
return (env, env_json_str)
end
@@ -637,21 +649,22 @@ function _serialize_data(data::Any, payload_type::String)
"""
if payload_type == "text" # Text data - convert to UTF-8 bytes
if isa(data, String)
data_bytes = Vector{UInt8}(data) # Convert string to UTF-8 bytes
return data_bytes
else
error("Text data must be a String")
end
# @info "msghandler _serialize_data() 1" @__LINE__
data_bytes = Vector{UInt8}(string(data)) # Convert string to UTF-8 bytes
return data_bytes
elseif payload_type == "dictionary" # JSON data - serialize directly
# @info "msghandler _serialize_data() 2" @__LINE__
json_str = JSON.json(data) # Convert Julia data to JSON string
json_str_bytes = Vector{UInt8}(json_str) # Convert JSON string to bytes
return json_str_bytes
elseif payload_type == "arrowtable" # Arrow table data - convert to Arrow IPC stream
# @info "msghandler _serialize_data() 3" @__LINE__
io = IOBuffer() # Create in-memory buffer
Arrow.write(io, data) # Write data as Arrow IPC stream to buffer
return take!(io) # Return the buffer contents as bytes
elseif payload_type == "jsontable" # JSON table data - convert to JSON
# @info "msghandler _serialize_data() 4" @__LINE__
# data can be Vector{NamedTuple}, Vector{Dict}, or DataFrame
# If DataFrame, convert to Vector{Dict} first
if isa(data, DataFrame)
@@ -667,29 +680,35 @@ function _serialize_data(data::Any, payload_type::String)
json_str = JSON.json(rows)
return Vector{UInt8}(json_str)
else
# @info "msghandler _serialize_data() 5" @__LINE__
# Already Vector{NamedTuple} or Vector{Dict}
json_str = JSON.json(data)
return Vector{UInt8}(json_str)
end
elseif payload_type == "image" # Image data - treat as binary
# @info "msghandler _serialize_data() 6" @__LINE__
if isa(data, Vector{UInt8})
return data # Return binary data directly
else
error("Image data must be Vector{UInt8}")
end
elseif payload_type == "audio" # Audio data - treat as binary
# @info "msghandler _serialize_data() 7" @__LINE__
if isa(data, Vector{UInt8})
return data # Return binary data directly
else
error("Audio data must be Vector{UInt8}")
end
elseif payload_type == "video" # Video data - treat as binary
# @info "msghandler _serialize_data() 8" @__LINE__
if isa(data, Vector{UInt8})
return data # Return binary data directly
else
error("Video data must be Vector{UInt8}")
end
elseif payload_type == "binary" # Binary data - treat as binary
# @info "msghandler _serialize_data() 9" @__LINE__
if isa(data, IOBuffer) # Check if data is an IOBuffer
return take!(data) # Return buffer contents as bytes
elseif isa(data, Vector{UInt8}) # Check if data is already binary
@@ -698,80 +717,12 @@ function _serialize_data(data::Any, payload_type::String)
error("Binary data must be binary (Vector{UInt8} or IOBuffer)")
end
else # Unknown type
# @info "msghandler _serialize_data() 10" @__LINE__
error("Unknown payload_type: $payload_type")
end
end
# """ publish_message - Publish message via transport
# This function publishes a message via the transport with proper
# connection management and logging.
# # Arguments:
# - `broker_url::String` - Broker URL (e.g., "localhost:4222")
# - `subject::String` - Subject to publish to (e.g., "/agent/wine/api/v1/prompt")
# - `message::String` - JSON message to publish
# - `correlation_id::String` - Correlation ID for tracing and logging
# # Return:
# - `nothing` - This function performs publishing but returns nothing
# # Example
# ```jldoctest
# using NATS
# # Prepare JSON message
# message = "{\"correlation_id\":\"abc123\",\"payload\":\"test\"}"
# # Publish via transport
# publish_message("localhost:4222", "my.subject", message, "abc123")
# ```
# """
# function publish_message(broker_url::String, subject::String, message::String, correlation_id::String)
# conn = NATS.connect(broker_url) # Create NATS connection
# publish_message(conn, subject, message, correlation_id)
# end
# """ publish_message - Publish message via transport using pre-existing connection
# This function publishes a message via the transport using a pre-existing connection,
# avoiding the overhead of connection establishment.
# # Arguments:
# - `conn` - Pre-existing connection object with publish/close methods
# - `subject::String` - Subject to publish to (e.g., "/agent/wine/api/v1/prompt")
# - `message::String` - JSON message to publish
# - `correlation_id::String` - Correlation ID for tracing and logging
# # Return:
# - `nothing` - This function performs publishing but returns nothing
# # Example
# ```jldoctest
# using NATS
# # Prepare JSON message
# message = "{\"correlation_id\":\"abc123\",\"payload\":\"test\"}"
# # Create connection once and reuse for multiple publishes
# conn = NATS.connect("nats://localhost:4222")
# publish_message(conn, "my.subject", message, "abc123")
# # Connection is automatically drained after publish
# ```
# # Use Case:
# 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.
# """
# function publish_message(conn::NATS.Connection, subject::String, message::String, correlation_id::String)
# try
# NATS.publish(conn, subject, message) # Publish message to NATS
# log_trace(correlation_id, "Message published to $subject") # Log successful publish
# finally
# NATS.drain(conn) # Ensure connection is closed properly
# end
# end
""" smartunpack - Receive and process messages
This function processes incoming messages, handling both direct transport
(base64 decoded payloads) and link transport (URL-based payloads).
@@ -789,7 +740,7 @@ A HTTP file server is required along with its download function.
- `msg_json_str::String` - JSON string from the message payload (e.g., `String(msg.payload)`)
# 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` - Custom download handler with signature `handler(url::String, max_retries::Int, base_delay::Int, max_delay::Int, correlation_id::String)::Vector{UInt8}`. Must accept the download URL, retry/backoff parameters, and correlation ID; return the fetched data as `Vector{UInt8}` bytes.
- `max_retries::Int = 5` - Maximum retry attempts for fetching URL
- `base_delay::Int = 100` - Initial delay for exponential backoff in ms
- `max_delay::Int = 5000` - Maximum delay for exponential backoff in ms
@@ -803,6 +754,23 @@ A HTTP file server is required along with its download function.
msg_json_str = String(msg.payload)
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"), ...]
# Custom fileserver download handler (e.g., for AWS S3 presigned URLs)
function my_custom_download(url::String, max_retries::Int, base_delay::Int, max_delay::Int, correlation_id::String)::Vector{UInt8}
# Fetch data from `url` with retry logic using exponential backoff
# Return the response body as Vector{UInt8} bytes
# Example:
# for attempt in 1:max_retries
# resp = HTTP.request("GET", url)
# if resp.status == 200
# return resp.body
# end
# end
throw(NotImplementedError("implement download logic here"))
end
# Use custom download handler
env = smartunpack(msg_json_str; fileserver_download_handler=my_custom_download)
```
"""
function smartunpack(
@@ -815,7 +783,7 @@ function smartunpack(
# Parse the JSON envelope
env_json_obj = JSON.parse(msg_json_str)
log_trace(env_json_obj["correlation_id"], "Processing received message") # Log message processing start
# log_trace(env_json_obj["correlation_id"], "Processing received message") # Log message processing start
# Process all payloads in the envelope
payloads_list = Tuple{String, Any, String}[]
@@ -829,7 +797,7 @@ function smartunpack(
dataname = String(payload["dataname"])
if transport == "direct" # Direct transport - payload is in the message
log_trace(env_json_obj["correlation_id"], "Direct transport - decoding payload '$dataname'") # Log direct transport handling
# log_trace(env_json_obj["correlation_id"], "Direct transport - decoding payload '$dataname'") # Log direct transport handling
# Extract base64 payload from the payload
payload_b64 = String(payload["data"])
@@ -845,7 +813,7 @@ function smartunpack(
elseif transport == "link" # Link transport - payload is at URL
# Extract download URL from the payload
url = String(payload["data"])
log_trace(env_json_obj["correlation_id"], "Link transport - fetching '$dataname' from URL: $url") # Log link transport handling
# log_trace(env_json_obj["correlation_id"], "Link transport - fetching '$dataname' from URL: $url") # Log link transport handling
# Fetch with exponential backoff using the download handler
downloaded_data = fileserver_download_handler(url, max_retries, base_delay, max_delay, env_json_obj["correlation_id"])
@@ -906,7 +874,7 @@ function _fetch_with_backoff(
try
response = HTTP.request("GET", url) # Make HTTP GET request to URL
if response.status == 200 # Check if request was successful
log_trace(correlation_id, "Successfully fetched data from $url on attempt $attempt") # Log success
# log_trace(correlation_id, "Successfully fetched data from $url on attempt $attempt") # Log success
return response.body # Return response body as bytes
else # Request failed
error("Failed to fetch: $(response.status)") # Throw error for non-200 status
+2 -2
View File
@@ -26,9 +26,9 @@ except ImportError:
# ---------------------------------------------- Constants ---------------------------------------------- #
"""
Default size threshold for switching from direct to link transport (0.5MB)
Default size threshold for switching from direct to link transport (10KB)
"""
DEFAULT_SIZE_THRESHOLD = 500_000
DEFAULT_SIZE_THRESHOLD = 10_000
"""
Default broker URL
+2 -2
View File
@@ -33,8 +33,8 @@ use uuid::Uuid;
// Constants
// ============================================================================
/// Default size threshold (0.5MB) for switching from direct to link transport
pub const DEFAULT_SIZE_THRESHOLD: usize = 500_000;
/// Default size threshold (10KB) for switching from direct to link transport
pub const DEFAULT_SIZE_THRESHOLD: usize = 10_000;
/// Default broker URL
pub const DEFAULT_BROKER_URL: &str = "localhost:4222";
+2 -2
View File
@@ -27,9 +27,9 @@
// ---------------------------------------------- Constants ---------------------------------------------- //
/**
* Default size threshold for switching from direct to link transport (0.5MB)
* Default size threshold for switching from direct to link transport (10KB)
*/
const DEFAULT_SIZE_THRESHOLD = 500_000;
const DEFAULT_SIZE_THRESHOLD = 10_000;
/**
* Default broker URL