Module: Tep
- Defined in:
- lib/tep/pg.rb,
lib/tep.rb,
lib/tep/app.rb,
lib/tep/job.rb,
lib/tep/jwt.rb,
lib/tep/llm.rb,
lib/tep/mcp.rb,
lib/tep/auth.rb,
lib/tep/http.rb,
lib/tep/cache.rb,
lib/tep/proxy.rb,
lib/tep/shell.rb,
lib/tep/assets.rb,
lib/tep/events.rb,
lib/tep/filter.rb,
lib/tep/parser.rb,
lib/tep/router.rb,
lib/tep/server.rb,
lib/tep/sqlite.rb,
lib/tep/handler.rb,
lib/tep/request.rb,
lib/tep/session.rb,
lib/tep/version.rb,
lib/tep/identity.rb,
lib/tep/parallel.rb,
lib/tep/password.rb,
lib/tep/presence.rb,
lib/tep/response.rb,
lib/tep/security.rb,
lib/tep/streamer.rb,
lib/tep/broadcast.rb,
lib/tep/live_view.rb,
lib/tep/multipart.rb,
lib/tep/scheduler.rb,
lib/tep/websocket.rb,
lib/tep/auth_oauth2.rb,
lib/tep/openai_server.rb,
lib/tep/presence_entry.rb,
lib/tep/websocket/frame.rb,
lib/tep/agent_delegation.rb,
lib/tep/auth_oauth2_code.rb,
lib/tep/server_scheduled.rb,
lib/tep/websocket/driver.rb,
lib/tep/auth_bearer_token.rb,
lib/tep/auth_oauth2_client.rb,
lib/tep/auth_session_cookie.rb,
lib/tep/websocket/handshake.rb,
lib/tep/websocket/connection.rb,
lib/tep/broadcast_subscription.rb
Overview
Tep::BroadcastSubscription -- one entry in the Tep::Broadcast subscriber registry. Pairs a topic name with an output fd. When a publish matches the topic, the fd gets the payload bytes via Sock.sphttp_write_str.
fd is just an integer file descriptor: typically a WebSocket connection's accepted socket fd, but the registry doesn't care about the protocol on top -- it'll write to any open fd. Apps integrating with WS (via Tep::WebSocket) subscribe their connection fds; non-WS use cases (server-sent events, log fan-out, etc.) work the same way.
Each subscription lives in a single worker's registry. Cross- worker pub-sub goes through PG LISTEN/NOTIFY (see Tep::Broadcast.enable_pg_backend) which fans publishes out without moving subscription state; subscribers always register fd-local. See docs/BATTERIES-DESIGN.md for the broader Broadcast battery design.
Defined Under Namespace
Modules: Auth, AuthOAuth2, Broadcast, Cache, MCP, Multipart, Presence, Security, WebSocket Classes: AgentDelegation, App, Assets, AuthBearerToken, AuthFilter, AuthOAuth2Client, AuthOAuth2Code, AuthSessionCookie, BroadcastSubscription, Events, FiberSlot, Filter, Handler, Http, Identity, Job, Jwt, LiveView, Llm, Parallel, ParallelWorker, Parser, Password, PresenceEntry, Proxy, Request, Response, Route, Router, SQLite, Scheduler, Server, Session, Shell, Stream, Streamer
Constant Summary collapse
- APP =
Session signing secret. Empty by default, which disables session writes (the Set-Cookie path no-ops). Set at app load time:
Tep.session_secret = ENV.fetch("TEP_SESSION_SECRET")Stored on the APP instance (spinel doesn't reliably type-track module-level
@@cvarsor globals). App.new
- COOKIE_NAME =
"tep.session"- VERSION =
"0.11.7"
Class Method Summary collapse
- .after(filter) ⇒ Object
- .before(filter) ⇒ Object
-
.cookie(req, k) ⇒ Object
Read a request cookie.
- .delete(pattern, handler) ⇒ Object
-
.get(pattern, handler) ⇒ Object
---------------- DSL ---------------- Spinel emits every defined method whether called or not, and infers parameter types from concrete call sites; methods nobody calls fall back to int parameters that mismatch the typed ivars they assign.
-
.h(s) ⇒ Object
HTML-escape: minimum safe set for attribute and PCDATA contexts.
- .not_found(handler) ⇒ Object
-
.on_shutdown ⇒ Object
Called by the SERVER PARENT (workers>1) or the single process (workers=1) at SIGTERM/SIGINT, AFTER the worker children have exited.
-
.param(req, k) ⇒ Object
Read a request param.
- .patch(pattern, handler) ⇒ Object
- .post(pattern, handler) ⇒ Object
- .public_dir(root) ⇒ Object
- .put(pattern, handler) ⇒ Object
- .reason(status) ⇒ Object
-
.run!(port, workers, quiet, scheduled = false) ⇒ Object
ARGV access only emits
sp_argvwhen used at top level, so the translator emits the option-parsing loop itself before callingTep.run!. -
.seed_fiber ⇒ Object
A canonical no-op fiber, used to type-seed Fiber-bearing collections without running anything user-visible.
- .seed_fiber_noop ⇒ Object
- .session_secret ⇒ Object
- .session_secret=(v) ⇒ Object
-
.str_find(s, needle, start) ⇒ Object
str_find -- naive substring search returning the int position of
needleinsstarting fromstart, or -1 if not found. - .str_hash ⇒ Object
-
.timing_safe_eq(a, b) ⇒ Object
Constant-time string equality.
-
.tls_cert ⇒ Object
Inbound TLS (tep#148 phase 2).
- .tls_cert=(v) ⇒ Object
- .tls_key ⇒ Object
- .tls_key=(v) ⇒ Object
Class Method Details
.after(filter) ⇒ Object
869 870 871 |
# File 'lib/tep.rb', line 869 def self.after(filter) APP.set_after(filter) end |
.before(filter) ⇒ Object
865 866 867 |
# File 'lib/tep.rb', line 865 def self.before(filter) APP.set_before(filter) end |
.cookie(req, k) ⇒ Object
Read a request cookie. A missing cookie reads as "" -- that is the
documented cookies["k"] DSL contract (test_cookies pins it) --
while the underlying hash reads nil for a missing key. bin/tep
rewrites cookies[k] reads to this call.
136 137 138 139 140 |
# File 'lib/tep.rb', line 136 def self.(req, k) v = req.[k] v = "" if v.nil? v end |
.delete(pattern, handler) ⇒ Object
858 |
# File 'lib/tep.rb', line 858 def self.delete(pattern, handler); APP.add_route("DELETE", pattern, handler); end |
.get(pattern, handler) ⇒ Object
---------------- DSL ---------------- Spinel emits every defined method whether called or not, and infers parameter types from concrete call sites; methods nobody calls fall back to int parameters that mismatch the typed ivars they assign. So the v0.1 surface only exposes what the bundled demos actually use; richer DSL methods (before/after/not_found) are layered on as the demos grow to exercise them.
854 |
# File 'lib/tep.rb', line 854 def self.get(pattern, handler); APP.add_route("GET", pattern, handler); end |
.h(s) ⇒ Object
HTML-escape: minimum safe set for attribute and PCDATA contexts.
Used by the build-time Mustache compiler for the default
{{var}} (escaped) form. Char-by-char to avoid gsub (spinel's
gsub coverage on string-typed receivers is uneven).
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
# File 'lib/tep.rb', line 185 def self.h(s) out = "" i = 0 n = s.length while i < n c = s[i] if c == "&" out = out + "&" elsif c == "<" out = out + "<" elsif c == ">" out = out + ">" elsif c == "\"" out = out + """ elsif c == "'" out = out + "'" else out = out + c end i += 1 end out end |
.not_found(handler) ⇒ Object
873 874 875 |
# File 'lib/tep.rb', line 873 def self.not_found(handler) APP.set_not_found(handler) end |
.on_shutdown ⇒ Object
Called by the SERVER PARENT (workers>1) or the single process (workers=1) at SIGTERM/SIGINT, AFTER the worker children have exited. Children no longer emit run_end themselves -- #128 moved the emission here so a multi-worker deployment writes exactly ONE run_end with aggregated stats from the events.jsonl, not N per worker.
reason: "completed" -- matches toy/v1 vocabulary (was "ok"; #115). Cheap when nothing is configured: openai_events is seeded with an empty path, whose enabled? short-circuits.
917 918 919 920 921 922 |
# File 'lib/tep.rb', line 917 def self.on_shutdown if APP.openai_events.enabled? APP.openai_events.run_end_aggregated("completed") end 0 end |
.param(req, k) ⇒ Object
Read a request param. A missing param reads as "" -- tep's
documented params[k] DSL contract (test_params pins it; a
deliberate divergence from sinatra's nil, ledgered in
docs/mirrors/sinatra.md). bin/tep rewrites params[k] reads to
this call; direct req.params[...] access keeps raw Hash
semantics (nil on miss).
148 149 150 151 152 |
# File 'lib/tep.rb', line 148 def self.param(req, k) v = req.params[k] v = "" if v.nil? v end |
.patch(pattern, handler) ⇒ Object
857 |
# File 'lib/tep.rb', line 857 def self.patch(pattern, handler); APP.add_route("PATCH", pattern, handler); end |
.post(pattern, handler) ⇒ Object
855 |
# File 'lib/tep.rb', line 855 def self.post(pattern, handler); APP.add_route("POST", pattern, handler); end |
.public_dir(root) ⇒ Object
861 862 863 |
# File 'lib/tep.rb', line 861 def self.public_dir(root) APP.set_static_root(root) end |
.put(pattern, handler) ⇒ Object
856 |
# File 'lib/tep.rb', line 856 def self.put(pattern, handler); APP.add_route("PUT", pattern, handler); end |
.reason(status) ⇒ Object
5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# File 'lib/tep/server.rb', line 5 def self.reason(status) if status == 200; return "OK"; end if status == 201; return "Created"; end if status == 204; return "No Content"; end if status == 301; return "Moved Permanently"; end if status == 302; return "Found"; end if status == 304; return "Not Modified"; end if status == 400; return "Bad Request"; end if status == 401; return "Unauthorized"; end if status == 403; return "Forbidden"; end if status == 404; return "Not Found"; end if status == 500; return "Internal Server Error"; end "OK" end |
.run!(port, workers, quiet, scheduled = false) ⇒ Object
ARGV access only emits sp_argv when used at top level, so the
translator emits the option-parsing loop itself before calling
Tep.run!. The scheduled flag picks between the prefork
blocking server (default) and the fiber-per-connection
Tep::Server::Scheduled (opt-in via set :scheduler, :scheduled
in the app source, or -s on the CLI). At the next major tep
release Scheduled becomes the default and Blocking is deleted;
the parallel-classes period exists only to make the rollback
path obvious during the transition.
Single dispatch method (rather than parallel run! / run_scheduled!)
because spinel's codegen mis-declares heap-cell parameters when
two same-arity sibling methods are called from an if/else --
both branches reference quiet as a heap-cell but only the first
path declares it. Bundling the decision inside one method
sidesteps the codegen miss.
scheduled defaults to false so apps that ship the historical
3-arg call (Tep.run!(port, workers, quiet)) keep building. Spinel
accepts the call without the 4th arg only because it supports
default-value params; without this, the 3-arg call silently
miscompiled (matz/spinel arity-warning shape, tep#13).
899 900 901 902 903 904 905 |
# File 'lib/tep.rb', line 899 def self.run!(port, workers, quiet, scheduled = false) if scheduled Server::Scheduled.new(APP).run(port, workers, quiet) else Server.new(APP).run(port, workers, quiet) end end |
.seed_fiber ⇒ Object
A canonical no-op fiber, used to type-seed Fiber-bearing collections without running anything user-visible. The body is a single method call (Fiber tests don't currently support arbitrary inline-block bodies in spinel).
121 122 123 |
# File 'lib/tep.rb', line 121 def self.seed_fiber Fiber.new { Tep.seed_fiber_noop } end |
.seed_fiber_noop ⇒ Object
113 114 115 |
# File 'lib/tep.rb', line 113 def self.seed_fiber_noop 0 end |
.session_secret ⇒ Object
219 |
# File 'lib/tep.rb', line 219 def self.session_secret; APP.session_secret; end |
.session_secret=(v) ⇒ Object
220 |
# File 'lib/tep.rb', line 220 def self.session_secret=(v); APP.set_session_secret(v); end |
.str_find(s, needle, start) ⇒ Object
str_find -- naive substring search returning the int position of
needle in s starting from start, or -1 if not found.
History: workaround for spinel 0210389 which made String#index
return nil for not-found (was -1). spinel 28545ff (matz/spinel#550)
added int|nil narrowing after an explicit nil-guard, so the
nil-side risk is upstream-resolved AND spinel supports the
offset overload s.index(needle, start) directly (emits
sp_str_index_from_poly). The helper stays solely for callsite
ergonomics: the 17 callers all use if x < 0 style int comparison
(which can't narrow against int|nil under spinel's current
narrowing model). Removing it would require a mechanical
< 0 -> .nil? refactor across http.rb / parser.rb / url.rb /
jwt.rb / app.rb. Worth doing eventually; not urgent.
168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/tep.rb', line 168 def self.str_find(s, needle, start) nlen = needle.length slen = s.length pos = start while pos <= slen - nlen if s[pos, nlen] == needle return pos end pos += 1 end -1 end |
.str_hash ⇒ Object
126 127 128 129 130 |
# File 'lib/tep.rb', line 126 def self.str_hash h = {"" => ""} h.delete("") h end |
.timing_safe_eq(a, b) ⇒ Object
Constant-time string equality. Avoids leaking the matching prefix length via early-exit timing. spinel doesn't have a stdlib crypto-safe compare, so we roll our own.
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/tep/session.rb', line 82 def self.timing_safe_eq(a, b) if a.length != b.length return false end diff = 0 i = 0 n = a.length while i < n # getbyte(i), NOT bytes[i]: `String#bytes` allocates a fresh # array on EVERY iteration (O(n^2) garbage). Besides being slow, # that allocation storm drives the GC hard enough to free `b` -- # the HMAC string returned from the Crypto FFI call, held only in # an argument local -- mid-loop, so a valid cookie fails its # signature check ~5% of the time under load (a #1052-family # heap-local rooting gap in spinel, open on master cc94707; the # real fix is upstream, tracked at tep#157). getbyte allocates # nothing, removing the dominant GC trigger here (cuts the flake # ~3x); the residual lives at other unrooted-local sites in the # decode path and clears only when spinel roots heap locals. diff = diff | (a.getbyte(i) ^ b.getbyte(i)) i += 1 end diff == 0 end |
.tls_cert ⇒ Object
Inbound TLS (tep#148 phase 2). Point these at a PEM cert + key and Tep::Server terminates HTTPS itself; unset (default) = plain HTTP. Tep.tls_cert = "cert.pem"; Tep.tls_key = "key.pem"
225 |
# File 'lib/tep.rb', line 225 def self.tls_cert; APP.tls_cert; end |
.tls_cert=(v) ⇒ Object
226 |
# File 'lib/tep.rb', line 226 def self.tls_cert=(v); APP.set_tls_cert(v); end |
.tls_key ⇒ Object
227 |
# File 'lib/tep.rb', line 227 def self.tls_key; APP.tls_key; end |
.tls_key=(v) ⇒ Object
228 |
# File 'lib/tep.rb', line 228 def self.tls_key=(v); APP.set_tls_key(v); end |