The blog render path (comp-fold + relations + typed-block) JIT-compiles on first call, so the first visitor after a restart paid ~2.5s (vs ~78ms warm) — that was the /:slug p99 tail. Define the route groups once, render / + welcome + nt-live-encore + /otel through a throwaway app at boot to force compilation, then reset the otel ring so warmup spans don't skew live metrics.
209 lines
9.2 KiB
Bash
Executable File
209 lines
9.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# host-on-sx live server launcher. Loads the kernel stdlib, the subsystem
|
|
# libraries, and the host modules into one sx_server process, then calls
|
|
# (host/serve PORT ...) which binds the native http-listen server to the
|
|
# Dream-shaped host app. Runs in the FOREGROUND (http-listen blocks), so this
|
|
# doubles as a container entrypoint and a local launcher.
|
|
#
|
|
# Usage:
|
|
# bash lib/host/serve.sh # serve on $HOST_PORT (default 8910)
|
|
# HOST_PORT=8920 bash lib/host/serve.sh # pick a port
|
|
#
|
|
# The module list is kept identical to lib/host/conformance.sh so what serves is
|
|
# exactly what the suites verify.
|
|
|
|
set -uo pipefail
|
|
# Project root: SX_PROJECT_DIR in containers (set to /app by the compose stack),
|
|
# else the git toplevel for local runs.
|
|
cd "${SX_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || echo .)}"
|
|
|
|
SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}"
|
|
if [ ! -x "$SX_SERVER" ]; then
|
|
SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe"
|
|
fi
|
|
if [ ! -x "$SX_SERVER" ]; then
|
|
echo "ERROR: sx_server.exe not found." >&2
|
|
exit 1
|
|
fi
|
|
|
|
PORT="${HOST_PORT:-8910}"
|
|
|
|
# Modules: every load line from conformance.sh's MODULES list, minus the ledger
|
|
# (not needed to serve). server.sx supplies host/serve.
|
|
MODULES=(
|
|
"spec/stdlib.sx"
|
|
"lib/r7rs.sx"
|
|
"lib/apl/runtime.sx"
|
|
"lib/datalog/tokenizer.sx"
|
|
"lib/datalog/parser.sx"
|
|
"lib/datalog/unify.sx"
|
|
"lib/datalog/db.sx"
|
|
"lib/datalog/builtins.sx"
|
|
"lib/datalog/aggregates.sx"
|
|
"lib/datalog/strata.sx"
|
|
"lib/datalog/eval.sx"
|
|
"lib/datalog/api.sx"
|
|
"lib/datalog/magic.sx"
|
|
"lib/acl/schema.sx"
|
|
"lib/acl/facts.sx"
|
|
"lib/acl/engine.sx"
|
|
"lib/acl/explain.sx"
|
|
"lib/acl/audit.sx"
|
|
"lib/acl/federation.sx"
|
|
"lib/acl/api.sx"
|
|
"lib/relations/schema.sx"
|
|
"lib/relations/engine.sx"
|
|
"lib/relations/api.sx"
|
|
"lib/relations/explain.sx"
|
|
"lib/relations/federation.sx"
|
|
"lib/relations/tree.sx"
|
|
"lib/feed/normalize.sx"
|
|
"lib/feed/stream.sx"
|
|
"lib/feed/api.sx"
|
|
"lib/persist/event.sx"
|
|
"lib/persist/backend.sx"
|
|
"lib/persist/log.sx"
|
|
"lib/persist/kv.sx"
|
|
"lib/persist/api.sx"
|
|
"lib/persist/durable.sx"
|
|
"spec/render.sx"
|
|
"web/adapter-html.sx"
|
|
"lib/dream/types.sx"
|
|
"lib/dream/json.sx"
|
|
"lib/dream/auth.sx"
|
|
"lib/dream/error.sx"
|
|
"lib/dream/form.sx"
|
|
"lib/dream/session.sx"
|
|
"lib/dream/router.sx"
|
|
"lib/host/handler.sx"
|
|
"lib/host/middleware.sx"
|
|
"lib/host/session.sx"
|
|
"lib/host/auth.sx"
|
|
"lib/host/sxtp.sx"
|
|
"lib/host/router.sx"
|
|
"lib/host/static.sx"
|
|
"lib/host/sx/relate-picker.sx"
|
|
"lib/host/sx/kg-cards.sx"
|
|
"lib/host/feed.sx"
|
|
"lib/host/relations.sx"
|
|
"lib/host/compose.sx"
|
|
"lib/host/execute.sx"
|
|
"lib/host/htmlsx.sx"
|
|
"lib/host/blog.sx"
|
|
"lib/host/server.sx"
|
|
"lib/host/otel.sx"
|
|
)
|
|
|
|
# Admin login credentials + session signing secret. Override via the container
|
|
# env; the in-source defaults are dev-only. The blog write routes are now GUARDED
|
|
# (session login or Bearer), so these gate publishing on blog.rose-ash.com.
|
|
ADMIN_USER="${SX_ADMIN_USER:-admin}"
|
|
ADMIN_PASS="${SX_ADMIN_PASSWORD:-letmein}"
|
|
SESSION_SECRET="${SX_SESSION_SECRET:-rose-ash-host-dev-secret-change-me}"
|
|
|
|
EPOCH=1
|
|
{
|
|
for M in "${MODULES[@]}"; do
|
|
echo "(epoch $EPOCH)"; echo "(load \"$M\")"; EPOCH=$((EPOCH+1))
|
|
done
|
|
# 100% serving JIT — NO host exclude. The serving-JIT perform-in-HO-callback
|
|
# miscompile (map/rest/drop wrong args → blank pages, empty picker) is fixed by
|
|
# two composing pieces: sx-vm-extensions 81177d0e resolves a callback's IO
|
|
# inline (instead of unwinding the native HO loop) WHEN a synchronous resolver
|
|
# is installed, and sx_server.ml's http-listen now installs that resolver (it
|
|
# mirrors cek_run_with_io exactly). So the whole request path — host app +
|
|
# Dream + Datalog — runs under JIT with no exclude. Verified: ephemeral durable
|
|
# server, 100% JIT, zero fallbacks, real content, picker lists candidates.
|
|
# Point the blog at the DURABLE file backend (persists under $SX_PERSIST_DIR),
|
|
# then idempotently seed a welcome post (sx_content = SX element markup, the
|
|
# editor's content model). Re-seeding is a no-op if the slug already exists.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-use-store! (persist/durable-backend))\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# Rebuild the relations graph from the durable edge store. lib/relations holds
|
|
# the graph in memory only, so without this, related/tags/types vanish on every
|
|
# restart even though the posts persist.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-load-edges!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# Sessions on the DURABLE store, LAZILY: only a logged-in session (one that
|
|
# writes a field) persists, so a login survives a restart while anonymous /
|
|
# crawler traffic leaves no rows. host/session-init! bumps the per-boot epoch
|
|
# that keeps sids unique across restarts. Then the signing secret + admin
|
|
# credentials, and grant admin "edit" on "blog" so a logged-in session passes
|
|
# the ACL gate on the write routes.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/session-use-store! (persist/durable-backend))\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/session-init!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/session-set-secret! \\\"$SESSION_SECRET\\\")\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/auth-set-admin! \\\"$ADMIN_USER\\\" \\\"$ADMIN_PASS\\\")\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(acl/load! (list (acl-grant \\\"$ADMIN_USER\\\" \\\"edit\\\" \\\"blog\\\")))\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# Idempotently seed a welcome post (sx_content = SX element markup, the editor's
|
|
# content model). Re-seeding is a no-op if the slug already exists.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-seed! \\\"welcome\\\" \\\"Welcome to the SX host\\\" \\\"(article (h1 \\\\\\\"Welcome to the SX host\\\\\\\") (p \\\\\\\"Rendered by lib/host via render-to-html, from the durable SX store.\\\\\\\"))\\\" \\\"published\\\")\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# Seed the root type-posts (type, tag) — types ARE posts. Idempotent.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-seed-types!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# Seed a live demo of the composition fold (plans/composition-objects.md): /compose-demo
|
|
# is one composition object rendered by host/comp-render — renders differently by context.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-seed-compose-demo!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# Seed the EXECUTE-fold demo (composition step 7): /workflow-demo runs ONE composition
|
|
# object through host/exec-run — the same algebra as render, folded to an effect log.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-seed-workflow-demo!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# Seed a REAL imported blog post (rose-ash.com/nt-live-encore) decomposed into the :body
|
|
# composition — so the import survives store wipes, reseeded on boot like the demos.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-seed-nt-live-encore!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# Seed the layer-2 demo: a Landing type with TWO composition fields (:body + :aside) + a
|
|
# populated instance — so the two-field composition editor + render show side by side.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-seed-landing-demo!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# Load relation metadata (symmetry/labels) from the relation-posts into the
|
|
# in-memory cache, so render paths read it without a (VmSuspending) durable read.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/blog-load-rel-kinds!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# Index the web-stack .sxbc by content hash so /sx/h/{hash} can serve them
|
|
# immutably and the shell can emit the data-sx-manifest (content-addressed
|
|
# client module cache). Done once at boot.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/static-build-sxh-index!)\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# Route groups defined ONCE so warmup + serve use the exact same list. otel early
|
|
# so /otel isn't swallowed by /:slug; blog-routes LAST — its GET /:slug catch-all
|
|
# must not shadow the rest. The write routes (POST /new, POST/PUT/DELETE /posts)
|
|
# are guarded by host/require-user (session login OR Bearer) + ACL; make-app
|
|
# auto-mounts /login + /logout and wraps all of it in the signed-session middleware.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(define host/-serve-groups (list host/static-routes otel/routes host/feed-routes host/relations-routes (host/blog-write-routes (fn (tok) nil)) host/blog-routes))\")"
|
|
EPOCH=$((EPOCH+1))
|
|
# JIT warmup: render the hot pages once through a throwaway app so the FIRST real
|
|
# visitor after a restart doesn't eat the cold-compile cost. The blog render path
|
|
# (comp-fold + relations + typed-block) JIT-compiles on first call — that was the
|
|
# ~2.5s p99 on /:slug, vs ~78ms warm. Reset the otel ring after so warmup spans
|
|
# don't skew the live metrics.
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(let ((warm (host/make-app host/-serve-groups))) (begin (warm (dream-request \\\"GET\\\" \\\"/\\\" {} \\\"\\\")) (warm (dream-request \\\"GET\\\" \\\"/welcome\\\" {} \\\"\\\")) (warm (dream-request \\\"GET\\\" \\\"/nt-live-encore\\\" {} \\\"\\\")) (warm (dream-request \\\"GET\\\" \\\"/otel\\\" {} \\\"\\\")) (otel/reset!) nil))\")"
|
|
EPOCH=$((EPOCH+1))
|
|
echo "(epoch $EPOCH)"
|
|
echo "(eval \"(host/serve $PORT host/-serve-groups)\")"
|
|
} | exec "$SX_SERVER"
|