tep 0.11.3 → 0.11.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Makefile +42 -2
- data/README.md +4 -4
- data/SINATRA_COMPAT.md +20 -20
- data/bin/tep +47 -10
- data/examples/api_gateway/app.rb +1 -1
- data/examples/blog/app.rb +17 -17
- data/examples/chat/app.rb +12 -12
- data/examples/chatbot/README.md +2 -2
- data/examples/chatbot/app.rb +24 -24
- data/examples/llm_gateway/app.rb +4 -4
- data/examples/pg_hello.rb +11 -1
- data/lib/spinel_kit/hex.rb +65 -0
- data/lib/spinel_kit/json.rb +151 -0
- data/lib/spinel_kit/json_decoder.rb +396 -0
- data/lib/{tep/logger.rb → spinel_kit/log.rb} +25 -21
- data/lib/spinel_kit/url.rb +166 -0
- data/lib/tep/auth_bearer_token.rb +6 -6
- data/lib/tep/auth_oauth2.rb +4 -4
- data/lib/tep/broadcast.rb +18 -80
- data/lib/tep/events.rb +37 -37
- data/lib/tep/http.rb +3 -3
- data/lib/tep/job.rb +2 -2
- data/lib/tep/jwt.rb +4 -4
- data/lib/tep/live_view.rb +4 -4
- data/lib/tep/llm.rb +13 -45
- data/lib/tep/mcp.rb +12 -12
- data/lib/tep/multipart.rb +1 -1
- data/lib/tep/net.rb +8 -3
- data/lib/tep/openai_server.rb +102 -94
- data/lib/tep/parser.rb +2 -2
- data/lib/tep/pg.rb +468 -14
- data/lib/tep/presence.rb +33 -329
- data/lib/tep/proxy.rb +7 -7
- data/lib/tep/request.rb +1 -1
- data/lib/tep/response.rb +1 -1
- data/lib/tep/router.rb +1 -1
- data/lib/tep/session.rb +2 -2
- data/lib/tep/version.rb +1 -1
- data/lib/tep.rb +57 -137
- data/spinel-ext.json +6 -0
- data/test/helper.rb +95 -8
- data/test/run_parallel.rb +44 -7
- data/test/test_auth.rb +17 -17
- data/test/test_auth_oauth2.rb +5 -5
- data/test/test_broadcast_pg.rb +1 -0
- data/test/test_http_pool.rb +4 -4
- data/test/test_http_pool_send.rb +3 -3
- data/test/test_json.rb +12 -12
- data/test/test_jwt.rb +4 -4
- data/test/test_live_view.rb +3 -3
- data/test/test_llm.rb +12 -9
- data/test/test_llm_gateway.rb +2 -2
- data/test/test_logger.rb +2 -2
- data/test/test_openai_server.rb +10 -1
- data/test/test_password.rb +3 -3
- data/test/test_pg.rb +1 -0
- data/test/test_presence_pg.rb +1 -0
- data/test/test_real_world.rb +6 -1
- data/test/test_shutdown.rb +40 -0
- metadata +23 -8
- data/lib/tep/json.rb +0 -572
- data/lib/tep/url.rb +0 -161
|
@@ -1,28 +1,33 @@
|
|
|
1
|
-
#
|
|
1
|
+
# VENDORED from OriPekelman/spinelkit @ 09e8558 -- DO NOT EDIT HERE.
|
|
2
|
+
# Edit upstream and re-sync with `make vendor-spinelkit`.
|
|
3
|
+
# SpinelKit::Log -- minimal levelled logger for spinel-AOT'd apps.
|
|
2
4
|
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
5
|
+
# WHY THIS EXISTS. CRuby's stdlib `Logger` is metaprogrammed (the severity
|
|
6
|
+
# dispatch loop, the formatter API, the device-rotation logic) and the
|
|
7
|
+
# spinelgems catalog rejects it (unresolved calls). Most app code that wants
|
|
8
|
+
# logging really wants three things: a level guard, a formatted line, and a
|
|
9
|
+
# destination. Ported verbatim from Tep::Logger; toy gains it for free.
|
|
8
10
|
#
|
|
9
11
|
# Surface
|
|
10
12
|
# -------
|
|
11
|
-
#
|
|
12
|
-
# logger = Tep::Logger.new
|
|
13
|
+
# logger = SpinelKit::Log.new
|
|
13
14
|
# logger.set_level("info") # one of: debug / info / warn / error
|
|
14
15
|
# logger.info("server up on " + port.to_s)
|
|
15
16
|
# logger.error("db connect failed")
|
|
16
17
|
#
|
|
17
18
|
# # File output: appends to the path. Leave unset for stderr.
|
|
18
|
-
# logger.to_file("/var/log/
|
|
19
|
+
# logger.to_file("/var/log/app.log")
|
|
20
|
+
#
|
|
21
|
+
# Each line is `[<unix_seconds>] [<level>] <message>`. The integer-seconds
|
|
22
|
+
# timestamp is what spinel exposes from `Time.now`; wider strftime support
|
|
23
|
+
# would need a C-shim (defer until callers ask for it).
|
|
19
24
|
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
module
|
|
25
|
-
class
|
|
25
|
+
# SPINEL NAMING DISCIPLINE: the method/param names below are the donor's
|
|
26
|
+
# proven-green spellings; the class-side `level_value` keeps the comparison
|
|
27
|
+
# a pure function so spinel pins its arg type to :str cleanly via a consumer
|
|
28
|
+
# type-seed. See docs/spinel-discipline.md.
|
|
29
|
+
module SpinelKit
|
|
30
|
+
class Log
|
|
26
31
|
attr_accessor :min_level, :file_path
|
|
27
32
|
|
|
28
33
|
def initialize
|
|
@@ -54,12 +59,11 @@ module Tep
|
|
|
54
59
|
end
|
|
55
60
|
|
|
56
61
|
def should_log?(level)
|
|
57
|
-
|
|
62
|
+
Log.level_value(level) >= Log.level_value(@min_level)
|
|
58
63
|
end
|
|
59
64
|
|
|
60
|
-
# Class-side helper so the comparison stays a pure function and
|
|
61
|
-
#
|
|
62
|
-
# tep.rb.
|
|
65
|
+
# Class-side helper so the comparison stays a pure function and spinel
|
|
66
|
+
# pins its arg type to :str cleanly via a consumer-side type-seed.
|
|
63
67
|
def self.level_value(name)
|
|
64
68
|
if name == "debug"
|
|
65
69
|
return 0
|
|
@@ -73,8 +77,8 @@ module Tep
|
|
|
73
77
|
if name == "error"
|
|
74
78
|
return 3
|
|
75
79
|
end
|
|
76
|
-
# Unknown level -- treat as info so misspelled labels don't
|
|
77
|
-
#
|
|
80
|
+
# Unknown level -- treat as info so misspelled labels don't vanish
|
|
81
|
+
# silently.
|
|
78
82
|
1
|
|
79
83
|
end
|
|
80
84
|
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# VENDORED from OriPekelman/spinelkit @ 09e8558 -- DO NOT EDIT HERE.
|
|
2
|
+
# Edit upstream and re-sync with `make vendor-spinelkit`.
|
|
3
|
+
require_relative "hex"
|
|
4
|
+
|
|
5
|
+
# SpinelKit::Url -- percent-encode/decode (the CGI / URI-component surface
|
|
6
|
+
# Spinel can't get from stdlib) plus a form-query parser and a small URL
|
|
7
|
+
# splitter. Ported from Tep::Url; the hex digits now come from SpinelKit::Hex.
|
|
8
|
+
#
|
|
9
|
+
# Self-contained: the empty str=>str hashes are seeded inline (the
|
|
10
|
+
# `{"" => ""}`-then-delete idiom that pins Spinel's value type), and substring
|
|
11
|
+
# search is a private `find_idx` (the `< 0` callsites can't narrow against
|
|
12
|
+
# String#index's int|nil under Spinel's current model). All pure string ops.
|
|
13
|
+
module SpinelKit
|
|
14
|
+
class Url
|
|
15
|
+
# "%41+b" -> "A b" (form-decode: `+` is space, `%XX` is a byte).
|
|
16
|
+
def self.unescape(s)
|
|
17
|
+
out = ""
|
|
18
|
+
i = 0
|
|
19
|
+
n = s.length
|
|
20
|
+
while i < n
|
|
21
|
+
c = s[i]
|
|
22
|
+
if c == "+"
|
|
23
|
+
out = out + " "
|
|
24
|
+
i += 1
|
|
25
|
+
elsif c == "%" && i + 2 < n
|
|
26
|
+
hi = Hex.nibble(s[i + 1])
|
|
27
|
+
lo = Hex.nibble(s[i + 2])
|
|
28
|
+
if hi >= 0 && lo >= 0
|
|
29
|
+
out = out + ((hi * 16 + lo).chr)
|
|
30
|
+
i += 3
|
|
31
|
+
else
|
|
32
|
+
out = out + c
|
|
33
|
+
i += 1
|
|
34
|
+
end
|
|
35
|
+
else
|
|
36
|
+
out = out + c
|
|
37
|
+
i += 1
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
out
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Percent-encode everything outside the RFC 3986 unreserved set
|
|
44
|
+
# (ALPHA / DIGIT / `-._~`); the rest becomes `%XX` with UPPERCASE hex.
|
|
45
|
+
# (Space -> `%20`, not `+` -- this is the URI-component, not form, encoder.)
|
|
46
|
+
#
|
|
47
|
+
# Byte-oriented: under Spinel `String#[]` indexes BYTES, so a multi-byte
|
|
48
|
+
# UTF-8 char is encoded byte-by-byte (correct %XX of each byte). Under CRuby,
|
|
49
|
+
# pass a binary string for the same behaviour (else `[]` splits on chars).
|
|
50
|
+
def self.escape(s)
|
|
51
|
+
out = ""
|
|
52
|
+
i = 0
|
|
53
|
+
while i < s.length
|
|
54
|
+
c = s[i]
|
|
55
|
+
if (c >= "a" && c <= "z") || (c >= "A" && c <= "Z") ||
|
|
56
|
+
(c >= "0" && c <= "9") || c == "-" || c == "." ||
|
|
57
|
+
c == "_" || c == "~"
|
|
58
|
+
out = out + c
|
|
59
|
+
else
|
|
60
|
+
b = c.getbyte(0)
|
|
61
|
+
out = out + "%" + Hex.nibble_char(b / 16) + Hex.nibble_char(b % 16)
|
|
62
|
+
end
|
|
63
|
+
i += 1
|
|
64
|
+
end
|
|
65
|
+
out
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# "a=1&b=2&c" -> {"a"=>"1","b"=>"2","c"=>""}. Keys + values are
|
|
69
|
+
# form-decoded (`unescape`).
|
|
70
|
+
def self.parse_query(s)
|
|
71
|
+
h = {"" => ""}
|
|
72
|
+
h.delete("")
|
|
73
|
+
if s.length == 0
|
|
74
|
+
return h
|
|
75
|
+
end
|
|
76
|
+
pairs = s.split("&")
|
|
77
|
+
pairs.each do |pair|
|
|
78
|
+
if pair.length > 0
|
|
79
|
+
eq = Url.find_idx(pair, "=", 0)
|
|
80
|
+
if eq < 0
|
|
81
|
+
h[Url.unescape(pair)] = ""
|
|
82
|
+
else
|
|
83
|
+
k = pair[0, eq]
|
|
84
|
+
v = pair[eq + 1, pair.length - eq - 1]
|
|
85
|
+
h[Url.unescape(k)] = Url.unescape(v)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
h
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Split `http(s)://host[:port]/path?query` into a str=>str hash keyed
|
|
93
|
+
# scheme / host / port / path / query. Without a scheme the input is
|
|
94
|
+
# treated as a path (host stays empty). Default ports follow the scheme
|
|
95
|
+
# (80 / 443); `query` is the raw substring after `?` (not decoded).
|
|
96
|
+
#
|
|
97
|
+
# One body on purpose: Spinel widens a Hash-typed value when a helper
|
|
98
|
+
# mutates it and the caller keeps reading, so `out` stays StrStrHash only
|
|
99
|
+
# if nothing factors the mutation out (find_idx returns an int, no mutate).
|
|
100
|
+
def self.split_url(u)
|
|
101
|
+
out = {"" => ""}
|
|
102
|
+
out.delete("")
|
|
103
|
+
out["scheme"] = ""
|
|
104
|
+
out["host"] = ""
|
|
105
|
+
out["port"] = ""
|
|
106
|
+
out["path"] = "/"
|
|
107
|
+
out["query"] = ""
|
|
108
|
+
|
|
109
|
+
rest = u
|
|
110
|
+
if rest.length >= 7 && rest[0, 7] == "http://"
|
|
111
|
+
out["scheme"] = "http"
|
|
112
|
+
out["port"] = "80"
|
|
113
|
+
rest = rest[7, rest.length - 7]
|
|
114
|
+
elsif rest.length >= 8 && rest[0, 8] == "https://"
|
|
115
|
+
out["scheme"] = "https"
|
|
116
|
+
out["port"] = "443"
|
|
117
|
+
rest = rest[8, rest.length - 8]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
if out["scheme"].length > 0
|
|
121
|
+
slash = Url.find_idx(rest, "/", 0)
|
|
122
|
+
hostport = rest
|
|
123
|
+
tail = "/"
|
|
124
|
+
if slash >= 0
|
|
125
|
+
hostport = rest[0, slash]
|
|
126
|
+
tail = rest[slash, rest.length - slash]
|
|
127
|
+
end
|
|
128
|
+
colon = Url.find_idx(hostport, ":", 0)
|
|
129
|
+
if colon >= 0
|
|
130
|
+
out["host"] = hostport[0, colon]
|
|
131
|
+
out["port"] = hostport[colon + 1, hostport.length - colon - 1]
|
|
132
|
+
else
|
|
133
|
+
out["host"] = hostport
|
|
134
|
+
end
|
|
135
|
+
rest = tail
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
qi = Url.find_idx(rest, "?", 0)
|
|
139
|
+
if qi >= 0
|
|
140
|
+
out["path"] = rest[0, qi]
|
|
141
|
+
out["query"] = rest[qi + 1, rest.length - qi - 1]
|
|
142
|
+
else
|
|
143
|
+
out["path"] = rest
|
|
144
|
+
end
|
|
145
|
+
if out["path"].length == 0
|
|
146
|
+
out["path"] = "/"
|
|
147
|
+
end
|
|
148
|
+
out
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# First index of `needle` in `s` at/after `start`, or -1. Internal
|
|
152
|
+
# (Spinel-safe substring search; see the module comment).
|
|
153
|
+
def self.find_idx(s, needle, start)
|
|
154
|
+
nlen = needle.length
|
|
155
|
+
slen = s.length
|
|
156
|
+
pos = start
|
|
157
|
+
while pos <= slen - nlen
|
|
158
|
+
if s[pos, nlen] == needle
|
|
159
|
+
return pos
|
|
160
|
+
end
|
|
161
|
+
pos += 1
|
|
162
|
+
end
|
|
163
|
+
-1
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
# Tep::AuthBearerToken.set_secret(ENV["JWT_SECRET"])
|
|
10
10
|
#
|
|
11
11
|
# Token payload schema (flat JSON, single level -- matches
|
|
12
|
-
#
|
|
12
|
+
# SpinelKit::Json's flat-object extraction surface):
|
|
13
13
|
#
|
|
14
14
|
# {
|
|
15
15
|
# "sub": "user:42", # principal_id (required)
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
# # agent_id|issued_at|expires_at|origin
|
|
23
23
|
# }
|
|
24
24
|
#
|
|
25
|
-
# Why flat (not nested `acting_via: { ... }`):
|
|
25
|
+
# Why flat (not nested `acting_via: { ... }`): SpinelKit::Json today
|
|
26
26
|
# extracts flat keys only. A nested-object getter is a separate
|
|
27
27
|
# tiny battery; for v1 of Auth the flat pipe-encoded delegate
|
|
28
28
|
# string is the smallest thing that ships and round-trips
|
|
@@ -64,20 +64,20 @@ module Tep
|
|
|
64
64
|
|
|
65
65
|
# Check expiry first -- a token whose exp passed gets rejected
|
|
66
66
|
# even if the signature still verifies. exp is unix epoch sec.
|
|
67
|
-
exp =
|
|
67
|
+
exp = SpinelKit::Json.get_int(payload, "exp")
|
|
68
68
|
if exp > 0 && Time.now.to_i >= exp
|
|
69
69
|
return nil
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
-
sub =
|
|
72
|
+
sub = SpinelKit::Json.get_str(payload, "sub")
|
|
73
73
|
if sub.length == 0
|
|
74
74
|
return nil
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
-
caps_str =
|
|
77
|
+
caps_str = SpinelKit::Json.get_str(payload, "caps")
|
|
78
78
|
caps = Tep::AuthBearerToken.parse_caps(caps_str)
|
|
79
79
|
|
|
80
|
-
delegate_str =
|
|
80
|
+
delegate_str = SpinelKit::Json.get_str(payload, "delegate")
|
|
81
81
|
delegation = Tep::AuthBearerToken.parse_delegate(delegate_str)
|
|
82
82
|
|
|
83
83
|
Tep::Identity.new(sub, delegation, caps)
|
data/lib/tep/auth_oauth2.rb
CHANGED
|
@@ -161,10 +161,10 @@ module Tep
|
|
|
161
161
|
delegate_str = rec.client_id + "|" + now_ts.to_s + "|" +
|
|
162
162
|
exp_ts.to_s + "|oauth_grant"
|
|
163
163
|
payload = "{" +
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
SpinelKit::Json.encode_pair_str("sub", rec.principal_id) + "," +
|
|
165
|
+
SpinelKit::Json.encode_pair_int("exp", exp_ts) + "," +
|
|
166
|
+
SpinelKit::Json.encode_pair_str("caps", rec.caps_str) + "," +
|
|
167
|
+
SpinelKit::Json.encode_pair_str("delegate", delegate_str) +
|
|
168
168
|
"}"
|
|
169
169
|
Tep::Jwt.encode_hs256(payload, secret)
|
|
170
170
|
end
|
data/lib/tep/broadcast.rb
CHANGED
|
@@ -114,14 +114,21 @@ module Tep
|
|
|
114
114
|
# remote deliveries are best-effort and not counted here.
|
|
115
115
|
def self.publish(topic, payload)
|
|
116
116
|
matched = Tep::Broadcast.publish_local_only(topic, payload)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
117
|
+
# Cross-worker fan-out is an OPT-IN hook (#216): the core build
|
|
118
|
+
# has no PG reference, so a non-PG app DCEs the entire libpq
|
|
119
|
+
# closure. `require "tep/pg"` redefines cross_worker_notify with
|
|
120
|
+
# the real broadcast_pg_conn.notify (last-definition-wins).
|
|
121
|
+
Tep::Broadcast.cross_worker_notify(topic, payload)
|
|
122
122
|
matched
|
|
123
123
|
end
|
|
124
124
|
|
|
125
|
+
# No-op cross-worker hook. Overridden by lib/tep/pg.rb when the PG
|
|
126
|
+
# backend is loaded. Keeping the PG NOTIFY out of core is what lets
|
|
127
|
+
# `Tep::Broadcast.publish` compile without pulling tep_pg_*/libpq.
|
|
128
|
+
def self.cross_worker_notify(topic, payload)
|
|
129
|
+
0
|
|
130
|
+
end
|
|
131
|
+
|
|
125
132
|
# Total subscription count across all topics. Useful for
|
|
126
133
|
# diagnostics and the v1 test surface.
|
|
127
134
|
def self.subscriber_count
|
|
@@ -157,81 +164,12 @@ module Tep
|
|
|
157
164
|
|
|
158
165
|
# ---- PG backend (cross-worker pub/sub) ----
|
|
159
166
|
#
|
|
160
|
-
#
|
|
161
|
-
#
|
|
162
|
-
#
|
|
163
|
-
#
|
|
164
|
-
#
|
|
165
|
-
#
|
|
166
|
-
# safe SQL identifier (e.g. "tep_broadcast") since it lands
|
|
167
|
-
# inside a LISTEN / NOTIFY command unescaped.
|
|
168
|
-
#
|
|
169
|
-
# Returns 0 on success, -1 on connection or LISTEN failure.
|
|
170
|
-
def self.enable_pg_backend(conninfo, channel)
|
|
171
|
-
conn = PG::Connection.new(conninfo)
|
|
172
|
-
if conn.pgh < 0
|
|
173
|
-
return -1
|
|
174
|
-
end
|
|
175
|
-
if conn.listen(channel) < 0
|
|
176
|
-
return -1
|
|
177
|
-
end
|
|
178
|
-
Tep::APP.set_broadcast_pg_conn(conn)
|
|
179
|
-
Tep::APP.set_broadcast_pg_channel(channel)
|
|
180
|
-
Tep::APP.set_broadcast_pg_enabled(1)
|
|
181
|
-
0
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
def self.disable_pg_backend
|
|
185
|
-
if Tep::APP.broadcast_pg_enabled == 0
|
|
186
|
-
return 0
|
|
187
|
-
end
|
|
188
|
-
Tep::APP.broadcast_pg_conn.unlisten(Tep::APP.broadcast_pg_channel)
|
|
189
|
-
Tep::APP.broadcast_pg_conn.finish
|
|
190
|
-
Tep::APP.set_broadcast_pg_enabled(0)
|
|
191
|
-
0
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
# Process one notification from the PG channel: parse the wire
|
|
195
|
-
# format, dispatch to local subscribers as if `publish` had
|
|
196
|
-
# been called locally (but WITHOUT re-NOTIFYing -- that would
|
|
197
|
-
# loop). Returns 1 if a notification was processed, 0 on
|
|
198
|
-
# timeout, -1 on connection error or unenabled backend.
|
|
199
|
-
def self.poll_pg_once(timeout_ms)
|
|
200
|
-
if Tep::APP.broadcast_pg_enabled == 0
|
|
201
|
-
return -1
|
|
202
|
-
end
|
|
203
|
-
r = Tep::APP.broadcast_pg_conn.poll_notification(timeout_ms)
|
|
204
|
-
if r != 1
|
|
205
|
-
return r
|
|
206
|
-
end
|
|
207
|
-
wire = Tep::APP.broadcast_pg_conn.last_notify_payload
|
|
208
|
-
Tep::Broadcast.deliver_wire_local(wire)
|
|
209
|
-
1
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
# Wire format: "<topic_byte_length>:<topic><payload>".
|
|
213
|
-
# Length-prefixed so topics and payloads with arbitrary chars
|
|
214
|
-
# (commas, colons, embedded quotes, newlines) round-trip
|
|
215
|
-
# unambiguously. Encoded by `publish` when the PG backend is
|
|
216
|
-
# enabled; decoded by `deliver_wire_local`.
|
|
217
|
-
def self.encode_wire(topic, payload)
|
|
218
|
-
topic.length.to_s + ":" + topic + payload
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def self.deliver_wire_local(wire)
|
|
222
|
-
colon = Tep.str_find(wire, ":", 0)
|
|
223
|
-
if colon <= 0
|
|
224
|
-
return -1
|
|
225
|
-
end
|
|
226
|
-
len_str = wire[0, colon]
|
|
227
|
-
tlen = len_str.to_i
|
|
228
|
-
if tlen < 0 || colon + 1 + tlen > wire.length
|
|
229
|
-
return -1
|
|
230
|
-
end
|
|
231
|
-
topic = wire[colon + 1, tlen]
|
|
232
|
-
payload = wire[colon + 1 + tlen, wire.length - colon - 1 - tlen]
|
|
233
|
-
Tep::Broadcast.publish_local_only(topic, payload)
|
|
234
|
-
end
|
|
167
|
+
# The PG LISTEN/NOTIFY backend is OPT-IN (#216). enable_pg_backend /
|
|
168
|
+
# disable_pg_backend / poll_pg_once, plus the wire encode/decode
|
|
169
|
+
# helpers (encode_wire / deliver_wire_local) and the
|
|
170
|
+
# cross_worker_notify override, live in lib/tep/pg.rb and only
|
|
171
|
+
# compile into apps that `require "tep/pg"`. Core Broadcast carries
|
|
172
|
+
# no PG reference so a non-PG app DCEs the libpq closure entirely.
|
|
235
173
|
|
|
236
174
|
# Same fan-out as #publish but skips the PG NOTIFY step. Used
|
|
237
175
|
# internally by poll_pg_once when delivering a cross-worker
|
data/lib/tep/events.rb
CHANGED
|
@@ -80,19 +80,19 @@ module Tep
|
|
|
80
80
|
# was a bare string before #115. os + arch come from uname() via
|
|
81
81
|
# Sock.sphttp_os_kind / sphttp_arch_kind.
|
|
82
82
|
line = "{" +
|
|
83
|
-
Json.encode_pair_str("kind", "run_start") + "," +
|
|
84
|
-
Json.encode_pair_str("schema", "toy/v1") + "," +
|
|
85
|
-
Json.encode_pair_int("t", 0) + "," +
|
|
86
|
-
Json.encode_pair_str("started_at", started) + "," +
|
|
83
|
+
SpinelKit::Json.encode_pair_str("kind", "run_start") + "," +
|
|
84
|
+
SpinelKit::Json.encode_pair_str("schema", "toy/v1") + "," +
|
|
85
|
+
SpinelKit::Json.encode_pair_int("t", 0) + "," +
|
|
86
|
+
SpinelKit::Json.encode_pair_str("started_at", started) + "," +
|
|
87
87
|
"\"host\":{" +
|
|
88
|
-
Json.encode_pair_str("name", host) + "," +
|
|
89
|
-
Json.encode_pair_str("os", Sock.sphttp_os_kind) + "," +
|
|
90
|
-
Json.encode_pair_str("arch", Sock.sphttp_arch_kind) +
|
|
88
|
+
SpinelKit::Json.encode_pair_str("name", host) + "," +
|
|
89
|
+
SpinelKit::Json.encode_pair_str("os", Sock.sphttp_os_kind) + "," +
|
|
90
|
+
SpinelKit::Json.encode_pair_str("arch", Sock.sphttp_arch_kind) +
|
|
91
91
|
"}," +
|
|
92
|
-
"\"backend\":{" + Json.encode_pair_str("kind", backend_kind) + "}," +
|
|
92
|
+
"\"backend\":{" + SpinelKit::Json.encode_pair_str("kind", backend_kind) + "}," +
|
|
93
93
|
"\"model\":{" +
|
|
94
|
-
Json.encode_pair_str("name", model_name) + "," +
|
|
95
|
-
Json.encode_pair_str("path", model_path) +
|
|
94
|
+
SpinelKit::Json.encode_pair_str("name", model_name) + "," +
|
|
95
|
+
SpinelKit::Json.encode_pair_str("path", model_path) +
|
|
96
96
|
"}," +
|
|
97
97
|
"\"config\":" + config_json +
|
|
98
98
|
"}"
|
|
@@ -120,10 +120,10 @@ module Tep
|
|
|
120
120
|
# Build the merged extra: spec fields first, then caller's
|
|
121
121
|
# fields appended (if non-empty).
|
|
122
122
|
extra = "{" +
|
|
123
|
-
Json.encode_pair_str("model", model) + "," +
|
|
124
|
-
Json.encode_pair_int("prompt_tokens", prompt_tokens) + "," +
|
|
125
|
-
Json.encode_pair_int("completion_tokens", completion_tokens) + "," +
|
|
126
|
-
Json.encode_pair_int("latency_us", wall_us)
|
|
123
|
+
SpinelKit::Json.encode_pair_str("model", model) + "," +
|
|
124
|
+
SpinelKit::Json.encode_pair_int("prompt_tokens", prompt_tokens) + "," +
|
|
125
|
+
SpinelKit::Json.encode_pair_int("completion_tokens", completion_tokens) + "," +
|
|
126
|
+
SpinelKit::Json.encode_pair_int("latency_us", wall_us)
|
|
127
127
|
caller_inner = ""
|
|
128
128
|
if extra_json.length > 2
|
|
129
129
|
# Strip the outer braces -- "{...}" -> "...".
|
|
@@ -134,10 +134,10 @@ module Tep
|
|
|
134
134
|
end
|
|
135
135
|
extra = extra + "}"
|
|
136
136
|
line = "{" +
|
|
137
|
-
Json.encode_pair_str("kind", "eval") + "," +
|
|
138
|
-
Json.encode_pair_str("phase", "serve") + "," +
|
|
139
|
-
Json.encode_pair_int("t", rel_t) + "," +
|
|
140
|
-
Json.encode_pair_str("name", "request") + "," +
|
|
137
|
+
SpinelKit::Json.encode_pair_str("kind", "eval") + "," +
|
|
138
|
+
SpinelKit::Json.encode_pair_str("phase", "serve") + "," +
|
|
139
|
+
SpinelKit::Json.encode_pair_int("t", rel_t) + "," +
|
|
140
|
+
SpinelKit::Json.encode_pair_str("name", "request") + "," +
|
|
141
141
|
"\"extra\":" + extra +
|
|
142
142
|
"}"
|
|
143
143
|
append_line(line)
|
|
@@ -163,14 +163,14 @@ module Tep
|
|
|
163
163
|
end
|
|
164
164
|
ended = Sock.sphttp_iso8601_utc(Time.now.to_i)
|
|
165
165
|
line = "{" +
|
|
166
|
-
Json.encode_pair_str("kind", "run_end") + "," +
|
|
167
|
-
Json.encode_pair_int("t", rel_t) + "," +
|
|
168
|
-
Json.encode_pair_str("ended_at", ended) + "," +
|
|
169
|
-
Json.encode_pair_str("reason", reason) + "," +
|
|
166
|
+
SpinelKit::Json.encode_pair_str("kind", "run_end") + "," +
|
|
167
|
+
SpinelKit::Json.encode_pair_int("t", rel_t) + "," +
|
|
168
|
+
SpinelKit::Json.encode_pair_str("ended_at", ended) + "," +
|
|
169
|
+
SpinelKit::Json.encode_pair_str("reason", reason) + "," +
|
|
170
170
|
"\"stats\":{" +
|
|
171
|
-
Json.encode_pair_int("requests", @req_count) + "," +
|
|
172
|
-
Json.encode_pair_int("errors", @err_count) + "," +
|
|
173
|
-
Json.encode_pair_int("tokens_out", @tok_out) +
|
|
171
|
+
SpinelKit::Json.encode_pair_int("requests", @req_count) + "," +
|
|
172
|
+
SpinelKit::Json.encode_pair_int("errors", @err_count) + "," +
|
|
173
|
+
SpinelKit::Json.encode_pair_int("tokens_out", @tok_out) +
|
|
174
174
|
"}" +
|
|
175
175
|
"}"
|
|
176
176
|
append_line(line)
|
|
@@ -205,28 +205,28 @@ module Tep
|
|
|
205
205
|
Tep.str_find(line_s, "\"name\":\"request\"", 0) >= 0
|
|
206
206
|
reqs += 1
|
|
207
207
|
# completion_tokens now lives nested inside the `extra`
|
|
208
|
-
# object.
|
|
208
|
+
# object. SpinelKit::Json.find_value_start walks only the
|
|
209
209
|
# top-level keys (it skips over nested objects), so we
|
|
210
210
|
# have to extract extra first, then get_int within it.
|
|
211
|
-
extra_pos = Json.find_value_start(line_s, "extra")
|
|
211
|
+
extra_pos = SpinelKit::Json.find_value_start(line_s, "extra")
|
|
212
212
|
if extra_pos >= 0
|
|
213
|
-
obj_end = Json.skip_container(line_s, extra_pos)
|
|
213
|
+
obj_end = SpinelKit::Json.skip_container(line_s, extra_pos)
|
|
214
214
|
extra_obj = line_s[extra_pos, obj_end - extra_pos]
|
|
215
|
-
toks += Json.get_int(extra_obj, "completion_tokens")
|
|
215
|
+
toks += SpinelKit::Json.get_int(extra_obj, "completion_tokens")
|
|
216
216
|
end
|
|
217
217
|
end
|
|
218
218
|
i += 1
|
|
219
219
|
end
|
|
220
220
|
ended = Sock.sphttp_iso8601_utc(Time.now.to_i)
|
|
221
221
|
out = "{" +
|
|
222
|
-
Json.encode_pair_str("kind", "run_end") + "," +
|
|
223
|
-
Json.encode_pair_int("t", rel_t) + "," +
|
|
224
|
-
Json.encode_pair_str("ended_at", ended) + "," +
|
|
225
|
-
Json.encode_pair_str("reason", reason) + "," +
|
|
222
|
+
SpinelKit::Json.encode_pair_str("kind", "run_end") + "," +
|
|
223
|
+
SpinelKit::Json.encode_pair_int("t", rel_t) + "," +
|
|
224
|
+
SpinelKit::Json.encode_pair_str("ended_at", ended) + "," +
|
|
225
|
+
SpinelKit::Json.encode_pair_str("reason", reason) + "," +
|
|
226
226
|
"\"stats\":{" +
|
|
227
|
-
Json.encode_pair_int("requests", reqs) + "," +
|
|
228
|
-
Json.encode_pair_int("errors", errs) + "," +
|
|
229
|
-
Json.encode_pair_int("tokens_out", toks) +
|
|
227
|
+
SpinelKit::Json.encode_pair_int("requests", reqs) + "," +
|
|
228
|
+
SpinelKit::Json.encode_pair_int("errors", errs) + "," +
|
|
229
|
+
SpinelKit::Json.encode_pair_int("tokens_out", toks) +
|
|
230
230
|
"}" +
|
|
231
231
|
"}"
|
|
232
232
|
append_line(out)
|
|
@@ -243,7 +243,7 @@ module Tep
|
|
|
243
243
|
end
|
|
244
244
|
|
|
245
245
|
# Append one JSON line. Best-effort, append mode -- mirrors
|
|
246
|
-
#
|
|
246
|
+
# SpinelKit::Log's file sink. Telemetry must never fail a request, so
|
|
247
247
|
# a malformed/unwritable path degrades to a dropped line rather
|
|
248
248
|
# than a raised error reaching the handler. Callers gate on a
|
|
249
249
|
# non-empty @path before reaching here.
|
data/lib/tep/http.rb
CHANGED
|
@@ -151,7 +151,7 @@ module Tep
|
|
|
151
151
|
|
|
152
152
|
def self.send_req_blocking(verb, url, body, headers)
|
|
153
153
|
out = Tep::Http::Response.new
|
|
154
|
-
parts =
|
|
154
|
+
parts = SpinelKit::Url.split_url(url)
|
|
155
155
|
scheme = parts["scheme"]
|
|
156
156
|
if scheme != "http" && scheme != "https"
|
|
157
157
|
# Unknown scheme.
|
|
@@ -168,7 +168,7 @@ module Tep
|
|
|
168
168
|
# pooling TLS sockets is out of scope for 6.7b (#126). HTTP/1.0 +
|
|
169
169
|
# Connection: close + recv-until-EOF over a fresh verified socket.
|
|
170
170
|
if scheme == "https"
|
|
171
|
-
fd = Sock.sphttp_connect_tls(host, port) # port 443 via
|
|
171
|
+
fd = Sock.sphttp_connect_tls(host, port) # port 443 via SpinelKit::Url
|
|
172
172
|
if fd < 0
|
|
173
173
|
return out
|
|
174
174
|
end
|
|
@@ -275,7 +275,7 @@ module Tep
|
|
|
275
275
|
# docs/MACOS-CONCURRENCY.md for the why.
|
|
276
276
|
def self.send_req_coop(verb, url, body, headers)
|
|
277
277
|
out = Tep::Http::Response.new
|
|
278
|
-
parts =
|
|
278
|
+
parts = SpinelKit::Url.split_url(url)
|
|
279
279
|
scheme = parts["scheme"]
|
|
280
280
|
if scheme != "http" && scheme != "https"
|
|
281
281
|
return out
|
data/lib/tep/job.rb
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
# )
|
|
25
25
|
#
|
|
26
26
|
# The single-arg payload is intentional: structured data goes
|
|
27
|
-
# through JSON (
|
|
27
|
+
# through JSON (SpinelKit::Json) which we already ship. Sidekiq's
|
|
28
28
|
# multi-arg `perform_async(a, b, c)` translates to encoding the
|
|
29
29
|
# tuple as a JSON string and decoding it in `perform`.
|
|
30
30
|
#
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
#
|
|
35
35
|
# class HelloJob < Tep::Job
|
|
36
36
|
# def perform(arg)
|
|
37
|
-
#
|
|
37
|
+
# SpinelKit::Log.new.info("hello " + arg)
|
|
38
38
|
# "done"
|
|
39
39
|
# end
|
|
40
40
|
# end
|
data/lib/tep/jwt.rb
CHANGED
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
# Surface
|
|
14
14
|
# -------
|
|
15
15
|
#
|
|
16
|
-
# payload_json = "{" +
|
|
17
|
-
#
|
|
16
|
+
# payload_json = "{" + SpinelKit::Json.encode_pair_str("sub", user_id) + "," +
|
|
17
|
+
# SpinelKit::Json.encode_pair_int("exp", exp_unix) + "}"
|
|
18
18
|
# token = Tep::Jwt.encode_hs256(payload_json, secret)
|
|
19
19
|
#
|
|
20
20
|
# # On the receiving side:
|
|
21
21
|
# if Tep::Jwt.verify_hs256(token, secret)
|
|
22
22
|
# payload = Tep::Jwt.decode_payload(token) # the JSON string
|
|
23
|
-
# sub =
|
|
23
|
+
# sub = SpinelKit::Json.get_str(payload, "sub")
|
|
24
24
|
# end
|
|
25
25
|
#
|
|
26
26
|
# Scope
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
#
|
|
33
33
|
# **Claims validation:** the `verify_hs256` only checks the
|
|
34
34
|
# signature. `exp` / `nbf` / `iss` / `aud` claim checks are left
|
|
35
|
-
# to caller code -- pull them with `
|
|
35
|
+
# to caller code -- pull them with `SpinelKit::Json.get_int(payload, "exp")`
|
|
36
36
|
# and compare against `Time.now.to_i`. This keeps the surface
|
|
37
37
|
# small and lets the app's policy decide what's required (some
|
|
38
38
|
# apps want skew tolerance, some want strict expiry).
|
data/lib/tep/live_view.rb
CHANGED
|
@@ -132,8 +132,8 @@ module Tep
|
|
|
132
132
|
# "note": <free text>,
|
|
133
133
|
# "until_ts": <unix ts or 0> }
|
|
134
134
|
#
|
|
135
|
-
# Subclasses typically pull a few keys via
|
|
136
|
-
#
|
|
135
|
+
# Subclasses typically pull a few keys via SpinelKit::Json.get_str /
|
|
136
|
+
# SpinelKit::Json.get_int + update an @presence ivar; then either
|
|
137
137
|
# call broadcast_render (to fan out the new HTML to every
|
|
138
138
|
# subscriber) or just mutate (if the LiveView is the only
|
|
139
139
|
# observer).
|
|
@@ -176,8 +176,8 @@ module Tep
|
|
|
176
176
|
# imeth on the base class dispatches through the typed slot
|
|
177
177
|
# of the subclass instance and avoids the box.
|
|
178
178
|
def dispatch_event_json(json_msg, req)
|
|
179
|
-
event =
|
|
180
|
-
payload =
|
|
179
|
+
event = SpinelKit::Json.get_str(json_msg, "event")
|
|
180
|
+
payload = SpinelKit::Json.get_str(json_msg, "payload")
|
|
181
181
|
handle_event(event, payload, req)
|
|
182
182
|
0
|
|
183
183
|
end
|