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

Class Method Summary collapse

Class Method Details

.after(filter) ⇒ Object



847
848
849
# File 'lib/tep.rb', line 847

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

.before(filter) ⇒ Object



843
844
845
# File 'lib/tep.rb', line 843

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

.delete(pattern, handler) ⇒ Object



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

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.



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

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



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

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



851
852
853
# File 'lib/tep.rb', line 851

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.



895
896
897
898
899
900
# File 'lib/tep.rb', line 895

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

.patch(pattern, handler) ⇒ Object



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

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

.post(pattern, handler) ⇒ Object



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

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

.public_dir(root) ⇒ Object



839
840
841
# File 'lib/tep.rb', line 839

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

.put(pattern, handler) ⇒ Object



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

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



877
878
879
880
881
882
883
# File 'lib/tep.rb', line 877

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



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

def self.session_secret;     APP.session_secret;        end

.session_secret=(v) ⇒ Object



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

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.



146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/tep.rb', line 146

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.



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"


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

def self.tls_cert;     APP.tls_cert;        end

.tls_cert=(v) ⇒ Object



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

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

.tls_keyObject



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

def self.tls_key;      APP.tls_key;         end

.tls_key=(v) ⇒ Object



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

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