Foreign-Language & Cross-Platform Integration
How non-.NET processes — Python, Node/Bun, browsers, and native mobile apps — join the mesh and use
mesh features natively, and how any React-capable platform renders mesh layout areas. This is the
umbrella over the whole integration; the gRPC transport itself has a deeper-dive companion,
ForeignLanguageBridge.md.
There are two halves, and one unifying idea behind each:
- Talking to the mesh — a foreign process becomes a first-class mesh participant over gRPC, because
the mesh is transport-agnostic: everything crosses one seam, the
IMessageDeliveryJSON envelope. - Rendering the mesh — a foreign UI renders a mesh layout area, because the UI model is a
platform-agnostic JSON
UiControltree — the same tree the Blazor portal and MAUI app render.
Python · Node/Bun · browser · iOS/Android .NET mesh (portal)
┌───────────────────────────────────┐ gRPC bidi ┌────────────────────────┐
│ client SDK (transport) │◄═══ stream ═══►│ MeshWeaver.Hosting.Grpc│
│ @meshweaver/react (UI renderer) │ UiControl │ → the mesh (hubs) │
└───────────────────────────────────┘ tree (JSON) └────────────────────────┘
one tree, many leaf packs ─────────────────────── one envelope, many transports
Part 1 — The transport (joining the mesh over gRPC)
A single gRPC bidirectional stream IS one mesh participant connection — the exact role
SignalRConnectionHub plays for the MAUI app and Blazor-WASM clients. We swap the transport skin
(SignalR → gRPC) and reuse everything else.
- Protobuf frames, JSON carries.
mesh.protoonly frames + streams the connection (aconnecthandshake, thendeliver/receiveframes). The message body stays the existing System.Text.JsonIMessageDeliveryJSON (RawJson,$type-discriminated), so the entire serialization, type-registry, andAccessContextmachinery is reused unchanged and never drifts. gRPC gives typed bidi streaming and first-class codegen for every language from one.proto. - Server = the SignalR host, re-skinned.
MeshWeaver.Hosting.GrpcmirrorsMeshWeaver.Hosting.SignalRalmost line-for-line:GrpcConnectionRegistryvalidates the participant's bearer token, re-stamps every inbound delivery'sAccessContextwith the server-resolved identity (a client-claimed identity is never trusted), and registers the participant for routing. async/await lives only at the transport boundary, exactly as inSignalRConnectionHub. - Participant reachability. A participant is reachable under both runtimes:
RegisterStream(Orleans — theRoutingGrainconsultsStreamRoutedAddressTypes) AND a hosted proxy hub at the participant address (monolith —RouteInMeshshort-circuits onGetHostedHub, the same way a Blazor circuit receives). The proxy's catch-all route forwards messages addressed to the participant onto its gRPC stream and leaves its own lifecycle messages alone. - Two RPC shapes.
Openis a single bidi stream — the natural shape for HTTP/2 clients (Node, Python, .NET). Browsers and React Native can't do bidi (and can't use Node'shttp2), so the service also offers a gRPC-web split: a server-streamingConnect(mesh→client) + a unaryDeliver(client→mesh), enabled withGrpc.AspNetCore.Web(app.UseMeshWeaverGrpcWeb()).Connect's ack returns aconnection_idthe client passes back on eachDeliver.
The transport is proven by network-free in-memory round-trip tests (the bidi Open AND the Connect/
Deliver split) and a live Kestrel (h2c) round-trip with a real GrpcChannel. Full detail + diagrams:
ForeignLanguageBridge.md.
Part 2 — The client SDKs
Each SDK is the in-language equivalent of IMessageHub + MeshWeaver.Mesh.Operations.MeshOperations, speaking the bidi
stream. All build on three primitives, and every mesh operation is a thin composition of them:
| Primitive | What it does |
|---|---|
observe(target, type, msg) |
request/response — send a delivery, await the reply whose properties.RequestId matches |
post(target, type, msg) |
fire-and-forget |
watch(target, streamId, …) |
live stream — subscribe, demux change events by streamId |
search / get / watch / patch and the node-lifecycle ops create / delete / move / copy /
execute are all compositions of these over the existing mesh request types — the same surface across
all three SDKs (Python, Node, and the browser/RN client-web). The lifecycle ops are WIRE:-annotated
where the exact request $type awaits a live capture.
- Python —
clients/python(meshweaver):grpc.aiotransport + aMeshoperations surface.import meshweaver as mw mesh = await mw.Mesh.connect("https://memex.meshweaver.cloud", token="mw_…") stories = await mesh.search("nodeType:Story namespace:ACME") # mesh → python await mesh.patch("ACME/Stories/42", {"content": {"done": True}}) # python → mesh - Node / Bun —
clients/typescript(@meshweaver/client):@grpc/grpc-jstransport (proto loaded at runtime via@grpc/proto-loader— no codegen step),AsyncIterablestreams, same surface. It also ships the node kernel (worker.ts) that executesjavascript/typescriptCode nodes routed tonode/node-kernel, and theHubhelper for defining a hub in Node (see Python Code Nodes for the execution model). - Browser / React Native —
clients/grpc-web(@meshweaver/client-web): the sameobserve/post/watchsurface over the gRPC-web split (Connect-ES), for platforms that can't do the bidiOpen(no HTTP/2 duplex / no Nodehttp2). It's aMeshConnectionLike, so it feeds the renderer'sGrpcAreaSourcedirectly.
Security: the bearer token travels in gRPC call metadata; the server validates it and stamps every write with the caller's identity. A forged client-side identity is never trusted.
Envelope shape (envelope.py / envelope.ts) and the operation request types (marked WIRE:) are
pinned to the mesh's IMessageDelivery JSON — confirm the exact $type/casing against a captured sample
(the C# round-trip test emits one). Everything beneath them (transport, correlation, demux) is correct.
Part 3 — The UI: rendering mesh layout areas (@meshweaver/react)
A MeshWeaver layout area is delivered as a JSON UiControl tree (an {areas, data} object, updated via
RFC 7396 merge-patches). Rendering it is: walk the tree, map each control's $type to a component, resolve
/data bindings, post click/edit events back. clients/react (@meshweaver/react) is exactly that, in
React + Fluent UI.
The swappable-core architecture
The crux — and the direct analog of MAUI's MauiViewPack:
renderer CORE (@meshweaver/react/core — NO DOM/Fluent)
dispatch on $type · pop skins · resolve bindings · area stream · post events
│ pulls components from a RegistryProvider context
┌────────────┴────────────┐
Fluent DOM pack RN pack
@meshweaver/react <View>/<Text>/<TextInput>
(web · Electron · Next) (iOS · Android)
ControlRenderer and area/* import no concrete component — they pull the "leaf pack" (control + skin
components) from context. The web entry installs a Fluent DOM pack; a React Native app installs a native
pack. Same UiControl tree, swappable leaves — exactly how MAUI has a native pack and Blazor a web one.
Because the Blazor portal renders with Fluent UI Blazor, the UiControl → Fluent React mapping is near 1:1.
What the pack covers
The Fluent web pack maps the full vocabulary: layout via skins (Stack/LayoutGrid/Tabs/Toolbar/
Splitter/NavMenu/NavGroup/Card), display (Label/Markdown/Html/Badge/Icon/CodeSample/
Exception), data (DataGrid + Property/Template columns, Catalog, Chart), the full input/form
family, navigation, feedback, editors (textarea — swap in Monaco), and the mesh controls. Unknown $types
render a labeled fallback; extend or override by spreading into the registry.
Data plane
The renderer depends only on an AreaSource (the {areas,data} tree + an event sink), so it's
transport-agnostic:
StaticAreaSource— a literal tree (demos, tests, the portal sample).GrpcAreaSource— subscribes to a live area over@meshweaver/client, folds RFC 7396 patches into{areas,data}, and routes click/edit events back. (Layout-area protocol shapes markedWIRE:.)
Bindings: a control property is a literal or a JsonPointerReference into /data; form edits write back via
the binding's pointer (optimistically applied, exactly as the live stream echoes the merge-patch).
vs MAUI / Blazor
| UiControl tree | Leaf pack | Transport | |
|---|---|---|---|
| Blazor portal | same | Fluent UI Blazor | SignalR (in-process circuit) |
| MAUI app | same | MauiViewPack (native) |
SignalR participant |
@meshweaver/react (web/Electron/Next) |
same | Fluent DOM | gRPC (GrpcAreaSource) |
| React Native | same | RN <View> pack |
gRPC-web (see Targets) |
Part 4 — The targets
| Target | Leaf pack | Extra work | Status |
|---|---|---|---|
| Web / Vite | Fluent (shipped) | none | demo + 11 vitest tests, screenshot |
| Next.js | Fluent (shipped) | "use client" + Fluent SSR (~10 lines) |
guide (clients/react/docs/nextjs.md) |
| Electron (desktop) | Fluent (shipped) | a BrowserWindow (shipped) |
clients/react/electron/main.cjs |
| React Native / Expo (the MAUI peer) | RN pack (shipped) | Expo project + gRPC-web transport (both shipped) | MeshWeaver.Plugins/app/react-native (+ src/live.ts), typechecks + 7 headless render tests |
| Browser / RN live transport | n/a (transport) | none | @meshweaver/client-web (clients/grpc-web), typechecks + builds |
| Portal example | Fluent (shipped) | an app shell (shipped) | clients/portal, builds, screenshot |
Next.js is the easiest target (React-on-the-web → same package, same Fluent pack). React Native is the
"vs MAUI" peer — same core, a native leaf pack. Live data in a browser or React Native uses the
gRPC-web split (Connect+Deliver) — @grpc/grpc-js is Node-only and gRPC-web can't do the bidi
Open. Both halves are shipped: the server (MeshGrpcService.Connect/Deliver, tested) AND the client —
@meshweaver/client-web (clients/grpc-web), a MeshConnectionLike over Connect-ES that drops straight
into GrpcAreaSource. The RN app wires it via MeshWeaver.Plugins/app/react-native/src/live.ts. Node, Electron-main, and
Next.js-server use the bidi Open directly via @meshweaver/client.
Part 5 — Repo layout
clients/
python/ meshweaver — Python SDK (transport + ops)
typescript/ @meshweaver/client — Node/Bun SDK (bidi Open)
grpc-web/ @meshweaver/client-web — browser + RN client (Connect+Deliver split)
react/ @meshweaver/react — Fluent UI renderer (core + web pack) + GrpcAreaSource
docs/ react-native.md · nextjs.md · demo.png
react-native/ meshweaver-mobile — Expo app + RN leaf pack (the MAUI peer) + src/live.ts
portal/ @meshweaver/portal-example — a web portal built from the renderer
MeshWeaver.Plugins/src/MeshWeaver.Hosting.Grpc/ the gRPC mesh transport (server)
src/MeshWeaver.Documentation/Data/Architecture/ForeignLanguageBridge.md transport deep-dive
.github/workflows/clients.yml CI: react (typecheck+test) · client-web (typecheck) · RN (typecheck) · portal (build)
Part 6 — Status & what needs validation
Verified here: the C# transport (in-memory + live Kestrel h2c round-trips), the React renderer
(pixel-verified web render, 11 vitest tests, 0.9 MB bundle), the RN connector (typechecks against real
react-native types AND 7 headless render tests that mount the sample through the RN pack — proving the core
renders with zero DOM/Fluent runtime and resolves bindings), the gRPC-web client
(@meshweaver/client-web typechecks + builds from the canonical mesh.proto, 8 vitest tests driving the
real connection against an in-memory Connect+Deliver service — ack, RequestId correlation, streamId demux —
wired into the RN app's src/live.ts), and the portal (builds + rendered). CI typechecks/tests all of it.
Needs your hardware / a running portal (the WIRE: follow-ups):
- Pin the SDK envelope wire-shape + the layout-area subscription protocol (
SubscribeRequest/DataChangedEvent/ click-edit messages) against a live portal — capture one change + one round-trip. - An Orleans round-trip test (the
RoutingGrainpath), complementing the monolith Kestrel test. - A live run of
@meshweaver/client-webagainst a running portal (browser + an iOS-simulator Expo run); the code typechecks/builds, and RN needs a streaming-fetchpolyfill for theConnectserver-stream. - Widen the operation surface further (threads) —
move/copy/executeand node-lifecycle ops now ship across all three SDKs.
Python Code nodes — running Python through the bridge (built here; see PythonCodeNodes.md): the
in-process kernel runs only C# (Roslyn), so a Code node with Language == "python" is routed over the mesh
to a connected Python worker (clients/python: python -m meshweaver.worker), which executes the
script and patches the run's Activity node — output surfaces identically to a C# run. The worker
(execute_python + CodeWorker) is unit-tested; the .NET side (SubmitCodeRequest.Language, CodeNodeType
re-targeting python, the markdown block forwarding its fence language) compiles under -warnaserror.
Remaining: a live portal+worker run, the WIRE:-marked ActivityLog/SubmitCodeResponse shapes, and a
worker pool (plus a node/bun worker — the routing branch is the one place that grows).