DEV Community

Sui Gn
Sui Gn

Posted on

FullTrailer as a real GUI + monad app, live over HTTP and WS through netget

Context

FullTrailer (/Users/suign/Desktop/Neuroverse/apps/FullTrailer, outside the all.this
monorepo) is a fleet-management app already built on this.gui — 7 views, all using
this.gui/atoms/this.gui/molecules/this.gui/react. Two things keep it from being a
real, canonical GUI app: app/src/runtime.tsx reimplements mountApp()/declareApp()/
routing locally instead of using the package's real ones, and there's no server behind
it except a small standalone Express proxy (server/index.js, Samsara fuel API +
photo uploads) — .me state today is purely client-side/ephemeral.

The direction, confirmed in conversation: FullTrailer gets its own monad (the
generic modules/monad/Typescript daemon, run as its own process, not forked), which
registers with the already-running local netget gateway. netget only resolves
hostnames to host:port and reverse-proxies — it does no app logic — so the frontend
stops talking to a bespoke REST API and instead talks through netget to a monad
speaking the standard .me/NRP contract. Confirmed via 3 parallel explorations this
session:

  • netget's gateway config already passes WebSocket upgrades through cleanly on every monad/surface route (proxy_http_version 1.1 + Upgrade/Connection headers in the shared proxyHeaders block, setNginxConfigRoutes.ts:62-70, used at every $monad_proxy_target/$surface_proxy_target proxy_pass). No netget changes needed.
  • The monad already has a /nrp WebSocket endpoint (nrpHandler.ts, attached to the same HTTP server/port the monad registers with netget — confirmed same-process, no separate WS registration needed, and exposure.inbound.allowWebsocket: true is already set in the registration payload). Today it only does namespace resolution (nrp.openresolved) — no live value reads or subscriptions yet.
  • this.gui already ships a full client for this protocol — Beatle (src/gui/All.This/NRP/Beatle/) — but it's unused anywhere in the app (Storybook-only) and only handles the resolution handshake. Its wire contract (Beatle.types.ts) already reserves 'stream'/'data' message types for exactly this next step, and its socket already stays open after resolving rather than closing — built for this.
  • No kernel write-notification mechanism exists anywhere (refSubscribers in me.ts/ core-write.ts is dead code, never wired). Live subscribe needs this built from scratch — confirmed in scope per this conversation ("build live-subscribe now too").
  • A custom local hostname needs an explicit domain record (POST /add-domain); the NRP handle-subdomain pattern (<name>.<machine-hostname>) resolves automatically via surface_proxy.lua's existing namespace/alias matching — chosen for the first test.

What's being built

A. Monad: live semantic subscribe over /nrp (generic — benefits every monad)

Repo: modules/monad/Typescript. This is shared daemon infrastructure, not
FullTrailer-specific — the right layer per the same "generalize the underlying
mechanism, don't bandaid" principle this session's own code reviews have been applying.

  1. New src/kernel/pathNotify.ts — a small in-process registry: subscribe(namespace, path, cb): unsubscribe / notify(namespace, path). Match a write to a subscription if the paths are equal or one is a dotted-prefix of the other (a write to apps.fulltrailer.tractos.records.0.status should notify a subscriber watching apps.fulltrailer.tractos.records). In-memory, single-process only — no cross-monad fan-out in this pass.
  2. Hook notify() into the existing write path — inside handlers/commandHandler.ts (meCommandHandler, the POST /me/* handler), which already has the exact namespace+path from the URL after a successful write. Don't touch kernel internals (me.ts/core-write.ts) — this stays a thin call at the one HTTP choke point where writes already land.
  3. Extend nrpHandler.ts's handleMessage() with new message types (add discriminants to BeatleMessage in packages/GUI/Typescript/src/gui/All.This/NRP/Beatle/Beatle.types.ts rather than overloading the existing generic 'data' — confirmed safe, no existing consumer breaks):
    • 'read' (client→server): {namespace, path} → resolve via pathResolver.ts's resolveNamespacePathValue(namespace, path) (existing function, reused as-is) → reply 'data' with {value, disclosure}, same public/closed-only disclosure contract pathResolver.ts already uses.
    • 'subscribe' (client→server): same lookup, then register via pathNotify.subscribe(...), keyed to this connection.
    • 'stream' (server→client, already reserved in the type union): sent whenever pathNotify fires for a path this connection subscribed to — re-resolves and sends the fresh {value, disclosure}.
    • 'unsubscribe' and ws.on('close', ...) both clean up that connection's entries.
    • Namespace for a WS connection is captured once at connect time from the nrp.open message (matches the existing pattern — a WS upgrade has no per-message Host header to re-resolve from).

B. this.gui: a WS-backed RuntimeAdapter (generic — publishable, reusable)

Repo: packages/GUI/Typescript. Lives in the package, not in FullTrailer, so any app
can opt into live semantics the same way FullTrailer will.

  1. New src/runtime/createWsMeRuntime.ts — composes on top of the existing createMeRuntime() (run-me.ts) rather than duplicating it: keep its action/write implementation as-is (writes stay plain HTTP POST /me/* — the monad already notifies WS subscribers once that write lands, per A.2), and replace only subscribe/getSnapshot:
    • One shared WS connection per adapter instance (ws://<host>/nrp), opened lazily, sends nrp.open once.
    • subscribe(path, callback) dedups multiple local listeners on the same path to one server-side 'subscribe' message; routes incoming 'data'/'stream' messages back to matching local callbacks by path; returns an unsubscribe that sends 'unsubscribe' once the last local listener for a path is gone.
    • getSnapshot() reads from a local cache populated by the last 'data'/'stream' message for that path; falls back to one HTTP GET (existing NRP read endpoint) for the very first read before any WS message has arrived for that path yet.
  2. Export from src/runtime-entry.ts (this.gui/runtime).
  3. Build, verify, bump to 2.3.0, publish — same flow just run for 2.2.0. FullTrailer can't consume this until it's published; sequence accordingly.

C. FullTrailer: wire it all together

Repo: /Users/suign/Desktop/Neuroverse/apps/FullTrailer.

  1. Run FullTrailer's monad as its own process — the existing, unmodified modules/monad/Typescript daemon, started with FullTrailer-specific env (ME_NAMESPACE/alias set so fulltrailer.<machine-hostname> resolves via surface_proxy.lua — confirm the exact required metadata.namespace/aliases value empirically against apps.json once it heartbeats, this is a verify-by-running item, not something to hard-code blind), its own port, MONAD_NAME=fulltrailer. Add as a third concurrently process in the root package.json dev script (app, server, now monad) — pointed at the existing modules/monad/Typescript checkout, not copied into FullTrailer's own repo.
  2. server/index.js (Samsara proxy + uploads) stays a separate process — confirmed modules/monad's app.ts is a fixed, monolithic router chain with no extension point for custom app routes; folding app-specific business logic into the generic daemon would be the wrong layer. Two backend processes locally: the monad (.me/NRP data) and this existing Express server (external API integration).
  3. app/src/runtime.tsx: stop reimplementing mountApp()/declareApp(). Import the real ones from this.gui/runtime. The LeftBar nav (NAV_ITEMS + active-route highlighting) has no equivalent in the package's mountApp() today — keep a small FullTrailer-local wrapper for just that nav chrome, but have it call the package's real declareApp()/writeMeValue()/readMeValue() instead of reimplementing that plumbing. Narrows the diff to "stop duplicating infra," not "invent app nav in the package."
  4. views/Home.tsxviews/Home.ts: convert to a defineSpecView()-tagged () => GuiSpecNode factory (mirrors the npx/template conversion pattern already established this session) — the first real consumer of that feature anywhere.
  5. The 5 CRUD views (Tractos, Remolques, Dollies, Operadores, Facturas) stay React.ComponentTypes, unchanged in shape — confirmed earlier this session they're a poor fit for spec trees (stateful forms, modals, computed values, per-row callbacks). They get live updates for free once main.tsx is pointed at createWsMeRuntime() instead of the local-only createMeRuntime()/ME(), since useMeValue already subscribes generically via runtime.subscribe.
  6. Bump this.gui/this.me deps once B is published.

D. netget

No code changes. Verification-only: confirm the heartbeat lands in apps.json with a
namespace/alias surface_proxy.lua actually matches against fulltrailer.<machine-hostname>,
and that a request to that hostname really reaches FullTrailer's monad.

Verification

  • curl -X POST http://fulltrailer.<machine-hostname>/me/apps.fulltrailer.manifest round-trips through netget to FullTrailer's monad.
  • Open a WS connection to ws://fulltrailer.<machine-hostname>/nrp, send nrp.open, then a 'subscribe' for apps.fulltrailer.tractos.records; in a second terminal POST a write to that path via HTTP; confirm a 'stream' message arrives on the open socket with the new value.
  • Run FullTrailer's dev stack (app + server + new monad process) locally, open the app through the netget hostname (not localhost:5173 directly), confirm Home (now spec-tree rendered) and the CRUD views both load.
  • With two browser tabs open to Tractos, edit a record in one tab, confirm the other tab's list updates live without a manual refresh — the actual end-to-end proof of the whole point of this work.
  • npx tsc --noEmit + npx vite build clean in both modules/monad/Typescript and packages/GUI/Typescript before publishing/running.

Top comments (0)