A retrieval-ready knowledge base so an LLM can read/write Five without prior training: overview, syntax, full RTL catalog (from hbrtl/register.go), web/worker idioms (from the solmade app), and a long-tail gotchas file. Every doc has keyword/summary frontmatter; INDEX.md is the routing manifest. Grounded by parallel source exploration; RTL names spot-checked against register.go. The gotchas file is the compounding asset — append new traps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6.3 KiB
doc, title, keywords, summary
| doc | title | keywords | summary | |||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| five-idioms | Five web/worker app idioms (HTTP, Postgres, queue, LLM, build) |
|
Battle-tested patterns from the production solmade app for building HTTP APIs and background workers in Five — endpoint skeleton, file-name routing, Postgres access, the DB job queue, LLM calls, and build/deploy. |
Five idioms / cookbook
All snippets are from the real solmade app (/Users/charleskwon/solmade).
1. HTTP endpoint skeleton
One FUNCTION Main() per .prg file under app/api/.
// POST /api/press-save.prg
FUNCTION Main()
LOCAL nPG := LABDB_GET_PG()
LOCAL hBody, nUser, aRows
IF nPG < 0
ctx_set( "status", 500 )
AP_JSONRESPONSE( { "ok" => .f., "error" => "PG not connected" } )
RETURN NIL
ENDIF
hBody := hb_jsonDecode( AP_BODY() ) // parse JSON request body
IF ! HB_ISHASH( hBody )
ctx_set( "status", 400 )
AP_JSONRESPONSE( { "ok" => .f., "error" => "body must be JSON object" } )
RETURN NIL
ENDIF
nUser := Val( ctx_get( "auth_user_id", "0" ) ) // authed user (string → int)
AP_JSONRESPONSE( { "ok" => .t. } )
RETURN NIL
Core verbs:
AP_BODY()→ raw request body (decode withhb_jsonDecode).AP_GETPAIRS( .t. )→ hash of query-string params (GET).AP_JSONRESPONSE( hHash )→ serialize + send JSON response.ctx_set( "status", N )→ set HTTP status code.ctx_get( "auth_user_id", "0" )→ authed user id (always a STRING; wrap inVal()). Alsoauth_email,auth_role,auth_display_nameset by the auth middleware.
2. File-name routing
A URL path maps deterministically to a function (app/bridge_server.prg):
strip /api/, drop .prg, replace -→_ and /→_, uppercase, append __MAIN.
/api/press-submit.prg → PRESS_SUBMIT__MAIN (Main() in app/api/press_submit.prg)
/api/auth/login.prg → AUTH_LOGIN__MAIN
So: name the file with underscores, put a FUNCTION Main() in it, call it with hyphens.
3. PostgreSQL access
LOCAL nPG := LABDB_GET_PG() // pooled connection handle (opened at startup)
// SELECT → array of hashes. *** Column values come back as STRINGS. ***
LOCAL aRows := PG_QUERY( nPG, ;
"SELECT id, title FROM articles WHERE status = $1 ORDER BY id DESC LIMIT 200", ;
{ "draft" } )
IF aRows == NIL ; aRows := {} ; ENDIF
LOCAL nId := Val( hb_CStr( aRows[1]["id"] ) ) // convert numeric columns explicitly
// non-SELECT
PG_EXEC( nPG, "UPDATE articles SET title=$1 WHERE id=$2", { "New", hb_NToS( nId ) } )
// INSERT ... RETURNING
aRows := PG_QUERY( nPG, ;
"INSERT INTO articles (title) VALUES ($1) RETURNING id", { "Draft" } )
LOCAL nNew := aRows[1]["id"]
// error string
LOCAL cErr := PG_LAST_ERROR( nPG )
Idempotent schema, safe to call inside an endpoint:
FUNCTION ARTICLES_ENSURE( nPG )
PG_EXEC( nPG, ;
"CREATE TABLE IF NOT EXISTS articles ( " + ;
" id SERIAL PRIMARY KEY, " + ;
" title TEXT NOT NULL DEFAULT '(제목 없음)', " + ;
" created_at TIMESTAMPTZ NOT NULL DEFAULT now() " + ;
")" )
RETURN NIL
4. DB job queue (avoid proxy timeout on long work)
Long LLM calls would blow the HTTP/proxy timeout. Pattern: submit → poll.
Submit (web) — insert queued, return id immediately:
aRows := PG_QUERY( nPG, ;
"INSERT INTO text_tasks (kind, input, status, progress_msg, requested_by) " + ;
"VALUES ('press', $1, 'queued', '대기 중', $2) RETURNING id", ;
{ cText, hb_NToS( nUser ) } )
AP_JSONRESPONSE( { "ok" => .t., "task_id" => aRows[1]["id"] } )
Worker (separate binary) — claim one row atomically; no two workers collide:
aClaim := PG_QUERY( nPG, ;
"WITH n AS ( SELECT id FROM text_tasks WHERE status='queued' " + ;
" ORDER BY id FOR UPDATE SKIP LOCKED LIMIT 1 ) " + ;
"UPDATE text_tasks SET status='running', started_at=now() " + ;
"WHERE id=(SELECT id FROM n) RETURNING id, kind, input" )
IF aClaim == NIL .OR. Len( aClaim ) == 0
RETURN .F. // nothing queued; caller sleeps
ENDIF
// ... do work, periodically UPDATE progress_pct/progress_msg ...
PG_EXEC( nPG, "UPDATE text_tasks SET status='done', progress_pct=100, " + ;
"result=$1, finished_at=now() WHERE id=$2", { cOut, hb_NToS( nId ) } )
Status (web) — client polls SELECT status, progress_pct, progress_msg, result.
Run multiple worker processes for concurrency; SKIP LOCKED keeps them race-free.
5. LLM calls
LLM_CHAT(cSystem, cUser, hOpts) → { "ok"=>.T./.F., "text"=>cResult, "error"=>cMsg }.
LOCAL hRes := LLM_CHAT( PRESSER_SYSTEM_PROMPT(), cUser, ;
{ "temperature" => 0.5, "max_tokens" => 4000 } )
IF hb_HGetDef( hRes, "ok", .f. )
cOut := hb_HGetDef( hRes, "text", "" )
ELSE
// hRes["error"] — endpoint unreachable / HTTP != 200 / empty response
ENDIF
Endpoint resolves from SOLMADE_LLM_URL env (OpenAI-compatible /v1); model name is
auto-resolved (a literal "local" is replaced by the actually-loaded model id, because
mlx/llama servers reject unknown model names). <think>...</think> is stripped.
6. Build & deploy (fnode)
Web and worker are separate binaries built from explicit file lists.
# build.sh — web
"$FNODE" build \
app/bridge_server.prg app/auth/*.prg app/lib/*.prg app/api/*.prg "$BRIDGE"/*.prg \
--rtl fivenode_go/hbrtl_ext/pgrtl \
--rtl fivenode_go/hbrtl_ext/httpserver \
--go-replace gitea.fivego.org/kwon_ai/solmade="$SOLMADE_ROOT" \
--module gitea.fivego.org/kwon_ai/solmade/_solmade_web \
-o solmade-web
--rtl <pkg>blank-imports a Go package whoseinit()registers RTL functions (e.g.pgrtlprovidesPG_QUERY/PG_EXEC;httpserverthe web bridge).--go-replace pkg=pathresolves a private module without a proxy.--module <name>sets the temp module path (must sit under the app's module so RTL packages can import the app's internal packages).
Deploy (launchd): launchctl kickstart -k gui/$(id -u)/kr.solmade.web (and
...worker1/2/3). The worker build (build_worker.sh) links cmd_prg/job_worker.prg
plus the shared app/lib/*.prg (so LLM_CHAT and prompts are available to it too).