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 @@cvars or globals).

App.new
"tep.session"
VERSION =
"0.11.7"

Class Method Summary collapse

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

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.cookie(req, k)
  v = req.cookies[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 + "&amp;"
    elsif c == "<"
      out = out + "&lt;"
    elsif c == ">"
      out = out + "&gt;"
    elsif c == "\""
      out = out + "&quot;"
    elsif c == "'"
      out = out + "&#39;"
    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_shutdownObject

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_fiberObject

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_noopObject



113
114
115
# File 'lib/tep.rb', line 113

def self.seed_fiber_noop
  0
end

.session_secretObject



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_hashObject



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_certObject

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_keyObject



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