From 3bbfbb7010a8a24b30d4dfd11f7b19edab417aff Mon Sep 17 00:00:00 2001 From: Charles KWON OhJun Date: Wed, 27 May 2026 16:46:03 +0900 Subject: [PATCH] feat(labdb): wire bridge_server to live PostgreSQL via pgrtl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit End-to-end PRG → Go RTL → pgxpool → labdb queries → JSON response, from a single 24 MB binary with no Node.js, FFI, or Apache anywhere in the request path. app/bridge_server.prg: Main now opens a pgxpool handle from LABDB_DSN (or the local Homebrew default) before HTTP_SERVER_START, then publishes it via LABDB_SET_PG so request goroutines can pick it up. BridgeDispatch calls FetchRouteData before the per-path PRG handler runs. FetchRouteData maps a few /api/* routes to the same SQL the upstream server.js endpoint runs, encodes results via hb_jsonEncode, and stuffs them into ctx fields under the same keys the existing labdb api/*.prg handlers already read (rows, total, devices, etc.). PRG handlers stay unmodified. hbrtl_ext/labdb_state/: Tiny Go RTL that holds the PG handle in a sync/atomic.Int64. Picked this over a PRG STATIC/PUBLIC variable to sidestep Five's stricter parser (now that parser.go errors on missing terminators, anything more elaborate in the entry .prg adds friction without buying anything). Verified against a Homebrew postgres@16 cluster seeded with 2 devices + 2 sessions: /api/admin-stats.prg → {"devices":2,"sessions":2,"records":0,"active_sessions":1} /api/sessions-list.prg → {"sessions":[...two rows with full fields...],"total":2} /api/admin-devices.prg → {"devices":[...two rows with api_key/created_at...]} /api/hello.prg (no DB) → unchanged from 1a.4-3 Co-Authored-By: Claude Opus 4.7 (1M context) --- app/bridge_server.prg | 55 +++++++++++++++++++++++++++++++++- cmd/fnode/main.go | 1 + hbrtl_ext/labdb_state/state.go | 36 ++++++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 hbrtl_ext/labdb_state/state.go diff --git a/app/bridge_server.prg b/app/bridge_server.prg index f608b5e..95d973d 100644 --- a/app/bridge_server.prg +++ b/app/bridge_server.prg @@ -7,7 +7,19 @@ // and use AP_METHOD/AP_BODY/AP_JSONRESPONSE/ctx_get/... freely. FUNCTION Main() - LOCAL cErr := HTTP_SERVER_START( ":8090", "BRIDGEDISPATCH" ) + LOCAL cDsn := hb_GetEnv( "LABDB_DSN", "postgres://" + hb_GetEnv( "USER", "" ) + "@127.0.0.1:5432/labdb?sslmode=disable" ) + LOCAL nH, cErr + + ? "labdb: opening PG", cDsn + nH := PG_OPEN( cDsn ) + IF nH < 0 + ? "labdb: PG_OPEN failed (handlers will see empty ctx — server still starts)" + ELSE + ? "labdb: PG handle =", nH + ENDIF + LABDB_SET_PG( nH ) + + cErr := HTTP_SERVER_START( ":8090", "BRIDGEDISPATCH" ) IF cErr != NIL ? "httpserver:", cErr ENDIF @@ -26,6 +38,8 @@ FUNCTION BridgeDispatch( hReq ) "headers_out" => { => }, ; "status" => 200 ; } + FetchRouteData( hReq[ "path" ], hCtx ) + _CTX_SET_JSON( hb_jsonEncode( hCtx ) ) _OUT_CLEAR() @@ -52,6 +66,45 @@ FUNCTION BridgeDispatch( hReq ) "body" => _OUT_GET() ; } +// FetchRouteData runs the per-route SQL the matching server.js endpoint +// would have run and drops the values into ctx fields the corresponding +// PRG handler reads via ctx_get(). Routes without a SQL block here fall +// through to whatever default the handler uses (which is what 1a.4-3 +// validated). Extend by adding cases as more endpoints come online. +FUNCTION FetchRouteData( cPath, hCtx ) + LOCAL aRows + LOCAL nPG := LABDB_GET_PG() + + IF nPG < 0 + RETURN NIL + ENDIF + + DO CASE + CASE cPath == "/api/admin-stats.prg" .OR. cPath == "/api/admin-stats" + aRows := PG_QUERY( nPG, "SELECT (SELECT COUNT(*) FROM devices) AS devices, (SELECT COUNT(*) FROM sessions) AS sessions, (SELECT COUNT(*) FROM records) AS records, (SELECT COUNT(*) FROM sessions WHERE status = 'active') AS active_sessions" ) + IF aRows != NIL .AND. Len( aRows ) > 0 + hCtx[ "devices" ] := hb_NToS( aRows[ 1 ][ "devices" ] ) + hCtx[ "sessions" ] := hb_NToS( aRows[ 1 ][ "sessions" ] ) + hCtx[ "records" ] := hb_NToS( aRows[ 1 ][ "records" ] ) + hCtx[ "active_sessions" ] := hb_NToS( aRows[ 1 ][ "active_sessions" ] ) + ENDIF + + CASE cPath == "/api/sessions-list.prg" .OR. cPath == "/api/sessions-list" + aRows := PG_QUERY( nPG, "SELECT session_id, device_id, session_name, start_time, end_time, record_count, status FROM sessions ORDER BY start_time DESC LIMIT 50" ) + IF aRows != NIL + // sessions-list.prg reads ctx_get("rows", "[]") + hCtx[ "rows" ] := hb_jsonEncode( aRows ) + hCtx[ "total" ] := hb_NToS( Len( aRows ) ) + ENDIF + + CASE cPath == "/api/admin-devices.prg" .OR. cPath == "/api/admin-devices" + aRows := PG_QUERY( nPG, "SELECT device_id, device_name, api_key, is_active, created_at FROM devices ORDER BY created_at DESC" ) + IF aRows != NIL + hCtx[ "rows" ] := hb_jsonEncode( aRows ) + ENDIF + ENDCASE +RETURN NIL + // /api/admin-stats.prg -> ADMIN_STATS__MAIN // /api/hello.prg -> HELLO__MAIN // /api/hello -> HELLO__MAIN (extension optional) diff --git a/cmd/fnode/main.go b/cmd/fnode/main.go index 2ff5380..e85e50f 100644 --- a/cmd/fnode/main.go +++ b/cmd/fnode/main.go @@ -39,6 +39,7 @@ var defaultRTL = []string{ "fivenode_go/hbrtl_ext/bridge_capi", "fivenode_go/hbrtl_ext/pgrtl", "fivenode_go/hbrtl_ext/dispatch", + "fivenode_go/hbrtl_ext/labdb_state", } func main() { diff --git a/hbrtl_ext/labdb_state/state.go b/hbrtl_ext/labdb_state/state.go new file mode 100644 index 0000000..0fef909 --- /dev/null +++ b/hbrtl_ext/labdb_state/state.go @@ -0,0 +1,36 @@ +// Package labdb_state holds a process-global Postgres handle. Saves +// app/bridge_server.prg from depending on Five's STATIC / PUBLIC +// semantics for the same purpose. +// +// PRG surface +// +// LABDB_SET_PG(nH) -> NIL +// LABDB_GET_PG() -> integer (-1 until SET) +package labdb_state + +import ( + "sync/atomic" + + "five/hbrt" +) + +var pgHandle atomic.Int64 + +func init() { + pgHandle.Store(-1) + hbrt.HB_FUNC("LABDB_SET_PG", labdbSetPG) + hbrt.HB_FUNC("LABDB_GET_PG", labdbGetPG) +} + +func labdbSetPG(ctx *hbrt.HBContext) { + if ctx.PCount() < 1 || !ctx.IsNumeric(1) { + ctx.RetNil() + return + } + pgHandle.Store(int64(ctx.ParNI(1))) + ctx.RetNil() +} + +func labdbGetPG(ctx *hbrt.HBContext) { + ctx.RetNI(int(pgHandle.Load())) +}