Module: Tep

Defined in:
lib/tep/app.rb,
lib/tep.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.4"

Class Method Summary collapse

Class Method Details

.after(filter) ⇒ Object



928
929
930
# File 'lib/tep.rb', line 928

def self.after(filter)
  APP.set_after(filter)
end

.before(filter) ⇒ Object



924
925
926
# File 'lib/tep.rb', line 924

def self.before(filter)
  APP.set_before(filter)
end

.delete(pattern, handler) ⇒ Object



917
# File 'lib/tep.rb', line 917

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.



913
# File 'lib/tep.rb', line 913

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).



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/tep.rb', line 160

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



932
933
934
# File 'lib/tep.rb', line 932

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.



976
977
978
979
980
981
# File 'lib/tep.rb', line 976

def self.on_shutdown
  if APP.openai_events.enabled?
    APP.openai_events.run_end_aggregated("completed")
  end
  0
end

.patch(pattern, handler) ⇒ Object



916
# File 'lib/tep.rb', line 916

def self.patch(pattern, handler);   APP.add_route("PATCH",   pattern, handler); end

.post(pattern, handler) ⇒ Object



914
# File 'lib/tep.rb', line 914

def self.post(pattern, handler);    APP.add_route("POST",    pattern, handler); end

.public_dir(root) ⇒ Object



920
921
922
# File 'lib/tep.rb', line 920

def self.public_dir(root)
  APP.set_static_root(root)
end

.put(pattern, handler) ⇒ Object



915
# File 'lib/tep.rb', line 915

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).



958
959
960
961
962
963
964
# File 'lib/tep.rb', line 958

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).



118
119
120
# File 'lib/tep.rb', line 118

def self.seed_fiber
  Fiber.new { Tep.seed_fiber_noop }
end

.seed_fiber_noopObject



110
111
112
# File 'lib/tep.rb', line 110

def self.seed_fiber_noop
  0
end

.session_secretObject



194
# File 'lib/tep.rb', line 194

def self.session_secret;     APP.session_secret;        end

.session_secret=(v) ⇒ Object



195
# File 'lib/tep.rb', line 195

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.



143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/tep.rb', line 143

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



123
124
125
126
127
# File 'lib/tep.rb', line 123

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.



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/tep/session.rb', line 74

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"


200
# File 'lib/tep.rb', line 200

def self.tls_cert;     APP.tls_cert;        end

.tls_cert=(v) ⇒ Object



201
# File 'lib/tep.rb', line 201

def self.tls_cert=(v); APP.set_tls_cert(v); end

.tls_keyObject



202
# File 'lib/tep.rb', line 202

def self.tls_key;      APP.tls_key;         end

.tls_key=(v) ⇒ Object



203
# File 'lib/tep.rb', line 203

def self.tls_key=(v);  APP.set_tls_key(v);  end