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/url.rb,
lib/tep/auth.rb,
lib/tep/http.rb,
lib/tep/json.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/logger.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, Json, Jwt, LiveView, Llm, Logger, Parallel, ParallelWorker, Parser, Password, PresenceEntry, Proxy, Request, Response, Route, Router, SQLite, Scheduler, Server, Session, Shell, Stream, Streamer, Url

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.1"

Class Method Summary collapse

Class Method Details

.after(filter) ⇒ Object



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

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

.before(filter) ⇒ Object



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

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

.delete(pattern, handler) ⇒ Object



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

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.



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

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



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

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



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

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.



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

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

.patch(pattern, handler) ⇒ Object



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

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

.post(pattern, handler) ⇒ Object



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

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

.public_dir(root) ⇒ Object



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

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

.put(pattern, handler) ⇒ Object



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

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



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

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



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

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

.seed_fiber_noopObject



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

def self.seed_fiber_noop
  0
end

.session_secretObject



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

def self.session_secret;     APP.session_secret;        end

.session_secret=(v) ⇒ Object



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

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.



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

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



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

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"


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

def self.tls_cert;     APP.tls_cert;        end

.tls_cert=(v) ⇒ Object



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

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

.tls_keyObject



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

def self.tls_key;      APP.tls_key;         end

.tls_key=(v) ⇒ Object



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

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