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/Connectionheaders in the sharedproxyHeadersblock,setNginxConfigRoutes.ts:62-70, used at every$monad_proxy_target/$surface_proxy_targetproxy_pass). No netget changes needed. - The monad already has a
/nrpWebSocket endpoint (nrpHandler.ts, attached to the same HTTP server/port the monad registers with netget — confirmed same-process, no separate WS registration needed, andexposure.inbound.allowWebsocket: trueis already set in the registration payload). Today it only does namespace resolution (nrp.open→resolved) — no live value reads or subscriptions yet. -
this.guialready 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 (
refSubscribersinme.ts/core-write.tsis 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 viasurface_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.
-
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 toapps.fulltrailer.tractos.records.0.statusshould notify a subscriber watchingapps.fulltrailer.tractos.records). In-memory, single-process only — no cross-monad fan-out in this pass. -
Hook
notify()into the existing write path — insidehandlers/commandHandler.ts(meCommandHandler, thePOST /me/*handler), which already has the exactnamespace+pathfrom 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. -
Extend
nrpHandler.ts'shandleMessage()with new message types (add discriminants toBeatleMessageinpackages/GUI/Typescript/src/gui/All.This/NRP/Beatle/Beatle.types.tsrather than overloading the existing generic'data'— confirmed safe, no existing consumer breaks):-
'read'(client→server):{namespace, path}→ resolve viapathResolver.ts'sresolveNamespacePathValue(namespace, path)(existing function, reused as-is) → reply'data'with{value, disclosure}, same public/closed-only disclosure contractpathResolver.tsalready uses. -
'subscribe'(client→server): same lookup, then register viapathNotify.subscribe(...), keyed to this connection. -
'stream'(server→client, already reserved in the type union): sent wheneverpathNotifyfires for a path this connection subscribed to — re-resolves and sends the fresh{value, disclosure}. -
'unsubscribe'andws.on('close', ...)both clean up that connection's entries. - Namespace for a WS connection is captured once at connect time from the
nrp.openmessage (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.
-
New
src/runtime/createWsMeRuntime.ts— composes on top of the existingcreateMeRuntime()(run-me.ts) rather than duplicating it: keep itsaction/write implementation as-is (writes stay plain HTTPPOST /me/*— the monad already notifies WS subscribers once that write lands, per A.2), and replace onlysubscribe/getSnapshot:- One shared WS connection per adapter instance (
ws://<host>/nrp), opened lazily, sendsnrp.openonce. -
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.
- One shared WS connection per adapter instance (
- Export from
src/runtime-entry.ts(this.gui/runtime). - 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.
-
Run FullTrailer's monad as its own process — the existing, unmodified
modules/monad/Typescriptdaemon, started with FullTrailer-specific env (ME_NAMESPACE/alias set sofulltrailer.<machine-hostname>resolves viasurface_proxy.lua— confirm the exact requiredmetadata.namespace/aliasesvalue empirically againstapps.jsononce it heartbeats, this is a verify-by-running item, not something to hard-code blind), its own port,MONAD_NAME=fulltrailer. Add as a thirdconcurrentlyprocess in the rootpackage.jsondev script (app,server, nowmonad) — pointed at the existingmodules/monad/Typescriptcheckout, not copied into FullTrailer's own repo. -
server/index.js(Samsara proxy + uploads) stays a separate process — confirmedmodules/monad'sapp.tsis 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). -
app/src/runtime.tsx: stop reimplementingmountApp()/declareApp(). Import the real ones fromthis.gui/runtime. The LeftBar nav (NAV_ITEMS+ active-route highlighting) has no equivalent in the package'smountApp()today — keep a small FullTrailer-local wrapper for just that nav chrome, but have it call the package's realdeclareApp()/writeMeValue()/readMeValue()instead of reimplementing that plumbing. Narrows the diff to "stop duplicating infra," not "invent app nav in the package." -
views/Home.tsx→views/Home.ts: convert to adefineSpecView()-tagged() => GuiSpecNodefactory (mirrors thenpx/templateconversion pattern already established this session) — the first real consumer of that feature anywhere. -
The 5 CRUD views (
Tractos,Remolques,Dollies,Operadores,Facturas) stayReact.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 oncemain.tsxis pointed atcreateWsMeRuntime()instead of the local-onlycreateMeRuntime()/ME(), sinceuseMeValuealready subscribes generically viaruntime.subscribe. - Bump
this.gui/this.medeps 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.manifestround-trips through netget to FullTrailer's monad. - Open a WS connection to
ws://fulltrailer.<machine-hostname>/nrp, sendnrp.open, then a'subscribe'forapps.fulltrailer.tractos.records; in a second terminalPOSTa 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+ newmonadprocess) locally, open the app through the netget hostname (notlocalhost:5173directly), confirmHome(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 buildclean in bothmodules/monad/Typescriptandpackages/GUI/Typescriptbefore publishing/running.
Top comments (0)