tep 0.11.0
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 +7 -0
- data/LICENSE +21 -0
- data/Makefile +134 -0
- data/README.md +247 -0
- data/SINATRA_COMPAT.md +376 -0
- data/bin/tep +2156 -0
- data/examples/agentic_chat/README.md +103 -0
- data/examples/agentic_chat/app.rb +310 -0
- data/examples/api_gateway/README.md +49 -0
- data/examples/api_gateway/app.rb +66 -0
- data/examples/blog/app.rb +367 -0
- data/examples/blog/views/index.erb +36 -0
- data/examples/blog/views/login.erb +28 -0
- data/examples/blog/views/new_post.erb +25 -0
- data/examples/blog/views/show.erb +16 -0
- data/examples/chat/app.rb +278 -0
- data/examples/chat/assets/logo.svg +13 -0
- data/examples/chat/assets/style.css +209 -0
- data/examples/chat/views/index.erb +142 -0
- data/examples/chatbot/README.md +111 -0
- data/examples/chatbot/app.rb +1024 -0
- data/examples/chatbot/assets/chat.js +249 -0
- data/examples/chatbot/assets/compare.js +93 -0
- data/examples/chatbot/assets/markdown.js +84 -0
- data/examples/chatbot/assets/style.css +215 -0
- data/examples/chatbot/schema.sql +25 -0
- data/examples/chatbot/views/compare.erb +43 -0
- data/examples/chatbot/views/index.erb +42 -0
- data/examples/chatbot/views/login.erb +22 -0
- data/examples/chatbot/views/setup.erb +23 -0
- data/examples/counter/README.md +68 -0
- data/examples/counter/app.rb +85 -0
- data/examples/experiments/AGENTS.md +91 -0
- data/examples/experiments/README.md +99 -0
- data/examples/experiments/app.rb +225 -0
- data/examples/geohash/Gemfile +11 -0
- data/examples/geohash/Gemfile.lock +17 -0
- data/examples/geohash/README.md +58 -0
- data/examples/geohash/app.rb +33 -0
- data/examples/hello.rb +120 -0
- data/examples/llm_gateway/README.md +73 -0
- data/examples/llm_gateway/app.rb +91 -0
- data/examples/maidenhead/Gemfile +7 -0
- data/examples/maidenhead/Gemfile.lock +17 -0
- data/examples/maidenhead/README.md +47 -0
- data/examples/maidenhead/app.rb +46 -0
- data/examples/pg_hello.rb +76 -0
- data/examples/qdrant/Gemfile +11 -0
- data/examples/qdrant/Gemfile.lock +29 -0
- data/examples/qdrant/README.md +54 -0
- data/examples/sinatra_style.rb +32 -0
- data/examples/websocket_echo.rb +37 -0
- data/lib/tep/agent_delegation.rb +35 -0
- data/lib/tep/app.rb +291 -0
- data/lib/tep/assets.rb +52 -0
- data/lib/tep/auth.rb +78 -0
- data/lib/tep/auth_bearer_token.rb +126 -0
- data/lib/tep/auth_oauth2.rb +189 -0
- data/lib/tep/auth_oauth2_client.rb +29 -0
- data/lib/tep/auth_oauth2_code.rb +40 -0
- data/lib/tep/auth_session_cookie.rb +132 -0
- data/lib/tep/broadcast.rb +265 -0
- data/lib/tep/broadcast_subscription.rb +42 -0
- data/lib/tep/cache.rb +49 -0
- data/lib/tep/events.rb +257 -0
- data/lib/tep/filter.rb +21 -0
- data/lib/tep/handler.rb +35 -0
- data/lib/tep/http.rb +599 -0
- data/lib/tep/identity.rb +67 -0
- data/lib/tep/job.rb +186 -0
- data/lib/tep/json.rb +572 -0
- data/lib/tep/jwt.rb +126 -0
- data/lib/tep/live_view.rb +219 -0
- data/lib/tep/llm.rb +505 -0
- data/lib/tep/logger.rb +85 -0
- data/lib/tep/mcp.rb +203 -0
- data/lib/tep/multipart.rb +98 -0
- data/lib/tep/net.rb +155 -0
- data/lib/tep/openai_server.rb +725 -0
- data/lib/tep/parallel.rb +168 -0
- data/lib/tep/parser.rb +81 -0
- data/lib/tep/password.rb +102 -0
- data/lib/tep/pg.rb +1128 -0
- data/lib/tep/presence.rb +589 -0
- data/lib/tep/presence_entry.rb +52 -0
- data/lib/tep/proxy.rb +801 -0
- data/lib/tep/request.rb +194 -0
- data/lib/tep/response.rb +134 -0
- data/lib/tep/router.rb +137 -0
- data/lib/tep/scheduler.rb +342 -0
- data/lib/tep/security.rb +140 -0
- data/lib/tep/server.rb +276 -0
- data/lib/tep/server_scheduled.rb +375 -0
- data/lib/tep/session.rb +98 -0
- data/lib/tep/shell.rb +62 -0
- data/lib/tep/sphttp.c +858 -0
- data/lib/tep/sqlite.rb +215 -0
- data/lib/tep/streamer.rb +31 -0
- data/lib/tep/tep_pg.c +769 -0
- data/lib/tep/tep_sqlite.c +320 -0
- data/lib/tep/url.rb +161 -0
- data/lib/tep/version.rb +3 -0
- data/lib/tep/websocket/connection.rb +171 -0
- data/lib/tep/websocket/driver.rb +169 -0
- data/lib/tep/websocket/frame.rb +238 -0
- data/lib/tep/websocket/handshake.rb +159 -0
- data/lib/tep/websocket.rb +68 -0
- data/lib/tep.rb +981 -0
- data/public/hello.txt +1 -0
- data/public/style.css +4 -0
- data/spinel-ext.json +33 -0
- data/test/helper.rb +248 -0
- data/test/real_world/01_simple.rb +5 -0
- data/test/real_world/02_lifecycle.rb +20 -0
- data/test/real_world/03_chat.rb +75 -0
- data/test/real_world/04_health_api.rb +25 -0
- data/test/real_world/05_todo_api.rb +57 -0
- data/test/real_world/06_basic_auth.rb +25 -0
- data/test/real_world/07_bbc_rest_api.rb +228 -0
- data/test/real_world/07_sklise_things.rb +109 -0
- data/test/real_world/08_jwd83_helloworld.rb +56 -0
- data/test/run_all.rb +7 -0
- data/test/run_parallel.rb +89 -0
- data/test/spinel_scheduled_burst_segv_repro.rb +33 -0
- data/test/test_api_gateway.rb +76 -0
- data/test/test_auth.rb +223 -0
- data/test/test_auth_oauth2.rb +208 -0
- data/test/test_auth_session_cookie.rb +198 -0
- data/test/test_broadcast.rb +197 -0
- data/test/test_broadcast_pg.rb +135 -0
- data/test/test_cache.rb +98 -0
- data/test/test_cache_static.rb +48 -0
- data/test/test_cookies.rb +52 -0
- data/test/test_erb.rb +53 -0
- data/test/test_erb_ivars.rb +58 -0
- data/test/test_events.rb +114 -0
- data/test/test_filters.rb +41 -0
- data/test/test_geohash_example.rb +89 -0
- data/test/test_http.rb +137 -0
- data/test/test_http_pool.rb +122 -0
- data/test/test_http_pool_send.rb +57 -0
- data/test/test_identity.rb +165 -0
- data/test/test_inbound_tls.rb +101 -0
- data/test/test_inbound_tls_scheduled.rb +101 -0
- data/test/test_job.rb +108 -0
- data/test/test_json.rb +168 -0
- data/test/test_jwt.rb +143 -0
- data/test/test_live_view.rb +324 -0
- data/test/test_llm.rb +250 -0
- data/test/test_llm_gateway.rb +95 -0
- data/test/test_logger.rb +101 -0
- data/test/test_maidenhead_example.rb +86 -0
- data/test/test_mcp.rb +264 -0
- data/test/test_misc_v02.rb +54 -0
- data/test/test_modular.rb +43 -0
- data/test/test_multi_filters.rb +40 -0
- data/test/test_mustache.rb +57 -0
- data/test/test_openai_server.rb +598 -0
- data/test/test_optional_segments.rb +45 -0
- data/test/test_parallel.rb +102 -0
- data/test/test_params.rb +99 -0
- data/test/test_pass.rb +42 -0
- data/test/test_password.rb +101 -0
- data/test/test_pg.rb +673 -0
- data/test/test_presence.rb +374 -0
- data/test/test_presence_pg.rb +309 -0
- data/test/test_proxy.rb +556 -0
- data/test/test_proxy_dsl.rb +119 -0
- data/test/test_proxy_streaming.rb +146 -0
- data/test/test_real_world.rb +397 -0
- data/test/test_regex_routes.rb +52 -0
- data/test/test_request_methods.rb +102 -0
- data/test/test_response.rb +123 -0
- data/test/test_routing.rb +109 -0
- data/test/test_scheduler.rb +153 -0
- data/test/test_security.rb +72 -0
- data/test/test_server_scheduled.rb +56 -0
- data/test/test_sessions.rb +59 -0
- data/test/test_shell.rb +54 -0
- data/test/test_sqlite.rb +148 -0
- data/test/test_sqlite_cached.rb +171 -0
- data/test/test_static.rb +57 -0
- data/test/test_streaming.rb +96 -0
- data/test/test_unsupported.rb +32 -0
- data/test/test_websocket.rb +152 -0
- data/test/test_websocket_echo.rb +138 -0
- data/test/views/greet.erb +5 -0
- data/test/views/hello.erb +5 -0
- data/test/views/list.erb +5 -0
- data/test/views/m_ivars.mustache +3 -0
- data/test/views/m_simple.mustache +4 -0
- data/test/views/mixed.erb +3 -0
- metadata +264 -0
data/lib/tep/sqlite.rb
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Tep::SQLite -- a thin wrapper around the system libsqlite3 for
|
|
2
|
+
# spinel-AOT'd apps. Uses tep_sqlite.c (compiled to tep_sqlite.o)
|
|
3
|
+
# as a stable C ABI surface, exposed via spinel's `ffi_func` DSL.
|
|
4
|
+
#
|
|
5
|
+
# Why not the `sqlite3` gem? It's a CRuby-MRI native extension
|
|
6
|
+
# (loadable .so/.bundle), which spinel can't link -- spinel
|
|
7
|
+
# produces a single static binary with everything resolved at
|
|
8
|
+
# compile time. The C-shim approach (same pattern as tep's HTTP
|
|
9
|
+
# server in sphttp.c) replaces "load a gem at runtime" with
|
|
10
|
+
# "link a .o at compile time."
|
|
11
|
+
#
|
|
12
|
+
# Usage
|
|
13
|
+
# -----
|
|
14
|
+
#
|
|
15
|
+
# db = Tep::SQLite.new
|
|
16
|
+
# db.open("./app.db")
|
|
17
|
+
# db.exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)")
|
|
18
|
+
#
|
|
19
|
+
# # Parameterised insert: prepare once, bind, step, finalize.
|
|
20
|
+
# db.prepare("INSERT INTO notes (body) VALUES (?)")
|
|
21
|
+
# db.bind_str(1, "hello")
|
|
22
|
+
# db.step
|
|
23
|
+
# db.finalize
|
|
24
|
+
# id = db.last_rowid
|
|
25
|
+
#
|
|
26
|
+
# # Single-row, single-column read.
|
|
27
|
+
# body = db.first_str("SELECT body FROM notes WHERE id = ?", id.to_s)
|
|
28
|
+
#
|
|
29
|
+
# # Iterating rows.
|
|
30
|
+
# db.prepare("SELECT id, body FROM notes ORDER BY id")
|
|
31
|
+
# while db.step == 1
|
|
32
|
+
# puts db.col_int(0).to_s + ": " + db.col_str(1)
|
|
33
|
+
# end
|
|
34
|
+
# db.finalize
|
|
35
|
+
#
|
|
36
|
+
# Constraints
|
|
37
|
+
# -----------
|
|
38
|
+
# - One in-flight cursor per process (the `prepare`/`step`/`finalize`
|
|
39
|
+
# trio shares a single C-side `sqlite3_stmt *`). Nesting one
|
|
40
|
+
# query inside another's loop will overwrite the parent cursor.
|
|
41
|
+
# The framework runs handlers serially per worker so this is
|
|
42
|
+
# fine for "one DB call per request".
|
|
43
|
+
# - Columns are read as either str or int. Floats / blobs / NULL
|
|
44
|
+
# aren't first-class -- a NULL column returns "" (str) or 0 (int).
|
|
45
|
+
# - The C side caps a single col_str result at 64 KiB. Large blobs
|
|
46
|
+
# would truncate.
|
|
47
|
+
#
|
|
48
|
+
# All FFI plumbing lives at the top level (parallel to `Sock`) so
|
|
49
|
+
# spinel's name resolver finds it from anywhere in the Tep tree.
|
|
50
|
+
module Sqlite
|
|
51
|
+
ffi_cflags "@TEP_SQLITE_O@"
|
|
52
|
+
ffi_lib "sqlite3"
|
|
53
|
+
|
|
54
|
+
ffi_func :tep_sqlite_open, [:str], :int
|
|
55
|
+
ffi_func :tep_sqlite_close, [:int], :int
|
|
56
|
+
ffi_func :tep_sqlite_exec, [:int, :str], :int
|
|
57
|
+
ffi_func :tep_sqlite_prepare, [:int, :str], :int
|
|
58
|
+
ffi_func :tep_sqlite_prepare_cached, [:int, :str], :int
|
|
59
|
+
ffi_func :tep_sqlite_bind_str, [:int, :str], :int
|
|
60
|
+
# bind_int / col_int are 64-bit: the value arg + return use the FFI
|
|
61
|
+
# `:long` (64-bit on LP64) routed through sqlite3_bind_int64 /
|
|
62
|
+
# sqlite3_column_int64, so an integer column > 2^31 round-trips
|
|
63
|
+
# without the 32-bit truncation that wrapped large values negative
|
|
64
|
+
# (issue #171). Spinel's mrb_int is pointer-width, so the Ruby side
|
|
65
|
+
# holds the full range. `:long` still maps to the `int` Spinel token,
|
|
66
|
+
# so callers see an Integer exactly as before.
|
|
67
|
+
ffi_func :tep_sqlite_bind_int, [:int, :long], :int
|
|
68
|
+
ffi_func :tep_sqlite_step, [], :int
|
|
69
|
+
ffi_func :tep_sqlite_col_str, [:int], :str
|
|
70
|
+
ffi_func :tep_sqlite_col_int, [:int], :long
|
|
71
|
+
ffi_func :tep_sqlite_col_count, [], :int
|
|
72
|
+
ffi_func :tep_sqlite_finalize, [], :int
|
|
73
|
+
ffi_func :tep_sqlite_reset, [], :int
|
|
74
|
+
ffi_func :tep_sqlite_last_insert_rowid, [:int], :int
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
module Tep
|
|
78
|
+
class SQLite
|
|
79
|
+
# `:dbh` (rather than the natural `:handle`) -- spinel widens
|
|
80
|
+
# poly dispatch return types when a method name is shared across
|
|
81
|
+
# classes with different signatures. `Tep::Handler#handle(req, res)`
|
|
82
|
+
# is the heart of the framework and returns String; an attr_accessor
|
|
83
|
+
# `handle` on Tep::SQLite would emit a 0-arg / int-return arm,
|
|
84
|
+
# widening the dispatch's return type to poly and cascading
|
|
85
|
+
# through `set_body_if_empty(s)` -> `Response#body` -> the
|
|
86
|
+
# sphttp_write_str(int, const char *) call. (See the gemini-bot
|
|
87
|
+
# commentary in spinel PR #391.)
|
|
88
|
+
attr_accessor :dbh
|
|
89
|
+
|
|
90
|
+
def initialize
|
|
91
|
+
@dbh = -1
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Returns true on success, false on failure. Path may be a real
|
|
95
|
+
# file or `:memory:` for an anonymous in-memory db. Multiple
|
|
96
|
+
# opens on the same instance leak the prior handle; close first.
|
|
97
|
+
def open(path)
|
|
98
|
+
h = Sqlite.tep_sqlite_open(path)
|
|
99
|
+
if h < 0
|
|
100
|
+
return false
|
|
101
|
+
end
|
|
102
|
+
@dbh = h
|
|
103
|
+
true
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def close
|
|
107
|
+
if @dbh >= 0
|
|
108
|
+
Sqlite.tep_sqlite_close(@dbh)
|
|
109
|
+
@dbh = -1
|
|
110
|
+
end
|
|
111
|
+
0
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Run a statement that returns no rows (CREATE / INSERT /
|
|
115
|
+
# UPDATE / DELETE / PRAGMA / BEGIN / COMMIT). Returns true on
|
|
116
|
+
# success. No bind in this form -- inline literal SQL is fine
|
|
117
|
+
# for DDL and constants; for any user-supplied value use
|
|
118
|
+
# prepare + bind + step + finalize.
|
|
119
|
+
def exec(sql)
|
|
120
|
+
if @dbh < 0
|
|
121
|
+
return false
|
|
122
|
+
end
|
|
123
|
+
Sqlite.tep_sqlite_exec(@dbh, sql) == 0
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Open a cursor on a parameterised query. Subsequent
|
|
127
|
+
# bind_str / bind_int calls fill in `?` markers (1-indexed).
|
|
128
|
+
# Always pair with `finalize` once iteration is done.
|
|
129
|
+
def prepare(sql)
|
|
130
|
+
if @dbh < 0
|
|
131
|
+
return false
|
|
132
|
+
end
|
|
133
|
+
Sqlite.tep_sqlite_prepare(@dbh, sql) == 0
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Cached variant. Same surface as `prepare`, but the underlying
|
|
137
|
+
# `sqlite3_stmt *` is memoised per-(db, sql); subsequent calls
|
|
138
|
+
# with the same SQL string reuse the prepared statement, paying
|
|
139
|
+
# the parse cost only once per process. Pair with `finalize` as
|
|
140
|
+
# usual; on the cached path `finalize` becomes
|
|
141
|
+
# `sqlite3_reset + sqlite3_clear_bindings` (the slot stays
|
|
142
|
+
# alive). The cache is bounded (currently 64 distinct SQL
|
|
143
|
+
# strings per process); apps that exceed the bound fall through
|
|
144
|
+
# to uncached prepare so correctness is preserved.
|
|
145
|
+
#
|
|
146
|
+
# Use for hot-path SQL where the string is known + fixed at
|
|
147
|
+
# codegen / boot time. Apps that build SQL with varying
|
|
148
|
+
# whitespace miss the cache (match is literal); format
|
|
149
|
+
# consistently.
|
|
150
|
+
def prepare_cached(sql)
|
|
151
|
+
if @dbh < 0
|
|
152
|
+
return false
|
|
153
|
+
end
|
|
154
|
+
Sqlite.tep_sqlite_prepare_cached(@dbh, sql) == 0
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def bind_str(idx, value); Sqlite.tep_sqlite_bind_str(idx, value); end
|
|
158
|
+
def bind_int(idx, value); Sqlite.tep_sqlite_bind_int(idx, value); end
|
|
159
|
+
|
|
160
|
+
# 1 -> row available, 0 -> done (no more rows), -1 -> error.
|
|
161
|
+
def step; Sqlite.tep_sqlite_step; end
|
|
162
|
+
def col_str(i); Sqlite.tep_sqlite_col_str(i); end
|
|
163
|
+
def col_int(i); Sqlite.tep_sqlite_col_int(i); end
|
|
164
|
+
def col_count; Sqlite.tep_sqlite_col_count; end
|
|
165
|
+
def finalize; Sqlite.tep_sqlite_finalize; end
|
|
166
|
+
def reset; Sqlite.tep_sqlite_reset; end
|
|
167
|
+
|
|
168
|
+
def last_rowid
|
|
169
|
+
if @dbh < 0
|
|
170
|
+
return -1
|
|
171
|
+
end
|
|
172
|
+
Sqlite.tep_sqlite_last_insert_rowid(@dbh)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Convenience: prepare a single-row, single-column query, bind
|
|
176
|
+
# one optional string param (pass "" for "no param"), step
|
|
177
|
+
# once, return col[0]. Always finalises the cursor before
|
|
178
|
+
# returning so the caller doesn't have to.
|
|
179
|
+
def first_str(sql, p1)
|
|
180
|
+
if @dbh < 0
|
|
181
|
+
return ""
|
|
182
|
+
end
|
|
183
|
+
if Sqlite.tep_sqlite_prepare(@dbh, sql) != 0
|
|
184
|
+
return ""
|
|
185
|
+
end
|
|
186
|
+
if p1.length > 0
|
|
187
|
+
Sqlite.tep_sqlite_bind_str(1, p1)
|
|
188
|
+
end
|
|
189
|
+
result = ""
|
|
190
|
+
if Sqlite.tep_sqlite_step == 1
|
|
191
|
+
result = Sqlite.tep_sqlite_col_str(0)
|
|
192
|
+
end
|
|
193
|
+
Sqlite.tep_sqlite_finalize
|
|
194
|
+
result
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def first_int(sql, p1)
|
|
198
|
+
if @dbh < 0
|
|
199
|
+
return 0
|
|
200
|
+
end
|
|
201
|
+
if Sqlite.tep_sqlite_prepare(@dbh, sql) != 0
|
|
202
|
+
return 0
|
|
203
|
+
end
|
|
204
|
+
if p1.length > 0
|
|
205
|
+
Sqlite.tep_sqlite_bind_str(1, p1)
|
|
206
|
+
end
|
|
207
|
+
result = 0
|
|
208
|
+
if Sqlite.tep_sqlite_step == 1
|
|
209
|
+
result = Sqlite.tep_sqlite_col_int(0)
|
|
210
|
+
end
|
|
211
|
+
Sqlite.tep_sqlite_finalize
|
|
212
|
+
result
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
data/lib/tep/streamer.rb
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Tep::Streamer -- subclass and override #pump(out). The framework
|
|
2
|
+
# emits chunked-encoding headers, calls pump with a Stream writer,
|
|
3
|
+
# then emits the end-of-stream marker. Cooperative; pump runs to
|
|
4
|
+
# completion before the connection moves on.
|
|
5
|
+
#
|
|
6
|
+
# spinel can't pass blocks into the framework, so this is the
|
|
7
|
+
# subclass-equivalent of `stream do |out| ... end`. The translator
|
|
8
|
+
# recognises the do/end form and emits a Streamer subclass for you.
|
|
9
|
+
module Tep
|
|
10
|
+
class Streamer
|
|
11
|
+
def pump(out)
|
|
12
|
+
# default no-op; subclasses override
|
|
13
|
+
0
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Per-request handle the user's `pump` writes to. Wraps the client
|
|
18
|
+
# fd so each `out.write(s)` becomes one chunked frame.
|
|
19
|
+
class Stream
|
|
20
|
+
attr_accessor :fd
|
|
21
|
+
|
|
22
|
+
def initialize(fd)
|
|
23
|
+
@fd = fd
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def write(s)
|
|
27
|
+
Sock.sphttp_write_chunk(@fd, s)
|
|
28
|
+
0
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|