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
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# Tep::LiveView -- Phoenix.LiveView-shape server-rendered stateful
|
|
2
|
+
# UI over WebSocket. Battery 4 in docs/BATTERIES-DESIGN.md.
|
|
3
|
+
#
|
|
4
|
+
# Ships the base class apps subclass + a pair of cmeths
|
|
5
|
+
# (render_page / dispatch_event) for the manual wiring path, the
|
|
6
|
+
# topic + broadcast_render binding, the handle_presence_diff
|
|
7
|
+
# hook, and auto-wiring via `Tep.live "/path", ViewClass` which
|
|
8
|
+
# lowers to a GET (render_page) + WS (event dispatch + re-render)
|
|
9
|
+
# pair in one DSL call.
|
|
10
|
+
#
|
|
11
|
+
# Usage (chunk 4.1):
|
|
12
|
+
#
|
|
13
|
+
# class CounterView < Tep::LiveView
|
|
14
|
+
# def initialize
|
|
15
|
+
# super
|
|
16
|
+
# @count = 0
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# def render
|
|
20
|
+
# "<div id='tep-live-root'>Count: " + @count.to_s + "</div>"
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# def handle_event(event, payload, req)
|
|
24
|
+
# if event == "inc"
|
|
25
|
+
# @count += 1
|
|
26
|
+
# end
|
|
27
|
+
# 0
|
|
28
|
+
# end
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# # Initial HTML: GET serves the rendered view wrapped in a
|
|
32
|
+
# # bootstrap shell that opens a WS to /counter_live + applies
|
|
33
|
+
# # incoming HTML to the #tep-live-root element.
|
|
34
|
+
# get "/counter" do
|
|
35
|
+
# v = CounterView.new
|
|
36
|
+
# v.mount(req)
|
|
37
|
+
# Tep::LiveView.render_page(v.render, "/counter_live")
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# # WS handler -- per-connection view instance, event dispatch,
|
|
41
|
+
# # re-render + send on every event.
|
|
42
|
+
# websocket "/counter_live" do |ws|
|
|
43
|
+
# v = CounterView.new
|
|
44
|
+
# on_open do |evt|
|
|
45
|
+
# v.mount_via_ws
|
|
46
|
+
# ws.text(v.render)
|
|
47
|
+
# end
|
|
48
|
+
# on_message do |evt|
|
|
49
|
+
# Tep::LiveView.dispatch_event(v, evt.data, req)
|
|
50
|
+
# ws.text(v.render)
|
|
51
|
+
# end
|
|
52
|
+
# end
|
|
53
|
+
#
|
|
54
|
+
# Why the manual wiring shape: tep's bin/tep translator lowers the
|
|
55
|
+
# `websocket` DSL into a generated route + per-event handler
|
|
56
|
+
# subclasses, and the user-supplied block bodies are subject to
|
|
57
|
+
# spinel's closure-capture limits. Wrapping the LiveView in the
|
|
58
|
+
# block is the spinel-friendly path; auto-wire helpers can lean on
|
|
59
|
+
# the translator in 4.2.
|
|
60
|
+
module Tep
|
|
61
|
+
class LiveView
|
|
62
|
+
def initialize
|
|
63
|
+
0
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Called when the view boots -- once on the initial HTTP GET,
|
|
67
|
+
# once on WS open. Subclasses override to seed @ivars from
|
|
68
|
+
# req.params / req.identity / etc.
|
|
69
|
+
def mount(req)
|
|
70
|
+
0
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Render the view's current state to HTML. Subclasses override.
|
|
74
|
+
# Wrap your real content in an element with `id="tep-live-root"`
|
|
75
|
+
# so the client-side bootstrap can swap the innerHTML cleanly.
|
|
76
|
+
def render
|
|
77
|
+
"<div id='tep-live-root'></div>"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Receive an event from the client. `event` and `payload` are
|
|
81
|
+
# strings (the client-side JS sends them as JSON). Subclasses
|
|
82
|
+
# mutate @ivars based on the event; the caller re-renders +
|
|
83
|
+
# sends the new HTML.
|
|
84
|
+
def handle_event(event, payload, req)
|
|
85
|
+
0
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Topic this view is bound to for broadcast fan-out. Default
|
|
89
|
+
# is the empty string -- subclasses override to return a
|
|
90
|
+
# stable id (e.g. "room:" + @id.to_s). When non-empty, the
|
|
91
|
+
# broadcast_render helper publishes re-renders here so every
|
|
92
|
+
# subscribed WS sees the new HTML.
|
|
93
|
+
def topic
|
|
94
|
+
""
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Re-render + publish the result to self.topic via
|
|
98
|
+
# Tep::Broadcast. Apps call this from handle_event after
|
|
99
|
+
# mutating state; subscribed WSes -- including the originating
|
|
100
|
+
# client -- receive the new HTML and the client-side bootstrap
|
|
101
|
+
# outerHTML-swaps it into the DOM.
|
|
102
|
+
#
|
|
103
|
+
# Returns the local-match count from Tep::Broadcast.publish
|
|
104
|
+
# (number of WSes that received the re-render on this worker).
|
|
105
|
+
# 0 here is ambiguous between "no topic configured" (skip) and
|
|
106
|
+
# "topic configured but no subscribers" -- the empty-topic path
|
|
107
|
+
# short-circuits without calling publish, so callers that
|
|
108
|
+
# want to distinguish can check `topic.length == 0` themselves.
|
|
109
|
+
def broadcast_render
|
|
110
|
+
t = topic
|
|
111
|
+
if t.length == 0
|
|
112
|
+
return 0
|
|
113
|
+
end
|
|
114
|
+
Tep::Broadcast.publish(t, render)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Receive a Tep::Presence diff event. Subclasses override to
|
|
118
|
+
# update their state when someone joins / leaves / changes
|
|
119
|
+
# status on the bound topic. Default is a no-op so v1 views
|
|
120
|
+
# without presence-awareness compile cleanly.
|
|
121
|
+
#
|
|
122
|
+
# The diff arrives as the same flat JSON Tep::Presence emits:
|
|
123
|
+
#
|
|
124
|
+
# { "kind": "join" | "leave" | "status",
|
|
125
|
+
# "topic": <topic>,
|
|
126
|
+
# "principal": <principal_id>,
|
|
127
|
+
# "ekind": "human" | "agent_for",
|
|
128
|
+
# "agent_id": <agent_id or empty>,
|
|
129
|
+
# "fd": <session-id surrogate>,
|
|
130
|
+
# "since": <unix ts>,
|
|
131
|
+
# "state": "available" | "busy" | "blocked",
|
|
132
|
+
# "note": <free text>,
|
|
133
|
+
# "until_ts": <unix ts or 0> }
|
|
134
|
+
#
|
|
135
|
+
# Subclasses typically pull a few keys via Tep::Json.get_str /
|
|
136
|
+
# Tep::Json.get_int + update an @presence ivar; then either
|
|
137
|
+
# call broadcast_render (to fan out the new HTML to every
|
|
138
|
+
# subscriber) or just mutate (if the LiveView is the only
|
|
139
|
+
# observer).
|
|
140
|
+
def handle_presence_diff(diff_json)
|
|
141
|
+
0
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Convenience: same shape as dispatch_event_json. Used by apps
|
|
145
|
+
# wiring a presence-diff intercept loop -- they feed received
|
|
146
|
+
# diff JSON strings to this method and it dispatches to
|
|
147
|
+
# handle_presence_diff on the subclass.
|
|
148
|
+
#
|
|
149
|
+
# Note: automatic server-side intercept of diffs (a background
|
|
150
|
+
# fiber per WS that pulls from the presence diff topic and
|
|
151
|
+
# calls apply_presence_diff_json) is not provided -- needs the
|
|
152
|
+
# WS DSL to bridge `req` into on_X handler bodies, tracked at
|
|
153
|
+
# OriPekelman/tep#54. Apps that need server-side reaction wire
|
|
154
|
+
# the intercept loop themselves; apps that only need client-
|
|
155
|
+
# side display can skip this and rely on
|
|
156
|
+
# Tep::Broadcast.subscribe_ws delivering the diff JSON straight
|
|
157
|
+
# to the WS for client-side rendering.
|
|
158
|
+
def apply_presence_diff_json(json)
|
|
159
|
+
handle_presence_diff(json)
|
|
160
|
+
0
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Imeth bridge from the WS-side JSON wire format to the
|
|
164
|
+
# subclass's `handle_event`. Apps call this from their
|
|
165
|
+
# on_message block:
|
|
166
|
+
#
|
|
167
|
+
# on_message do |evt|
|
|
168
|
+
# v.dispatch_event_json(evt.data, req)
|
|
169
|
+
# ws.text(v.render)
|
|
170
|
+
# end
|
|
171
|
+
#
|
|
172
|
+
# Why an imeth and not a cmeth: spinel widens cmeth params
|
|
173
|
+
# to poly (sp_RbVal) when the cmeth has callers across
|
|
174
|
+
# multiple LiveView subclasses, but doesn't auto-box concrete
|
|
175
|
+
# subclass pointers into the poly slot at the call site. An
|
|
176
|
+
# imeth on the base class dispatches through the typed slot
|
|
177
|
+
# of the subclass instance and avoids the box.
|
|
178
|
+
def dispatch_event_json(json_msg, req)
|
|
179
|
+
event = Tep::Json.get_str(json_msg, "event")
|
|
180
|
+
payload = Tep::Json.get_str(json_msg, "payload")
|
|
181
|
+
handle_event(event, payload, req)
|
|
182
|
+
0
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# ---- helpers (cmeths so apps reach for them without a view
|
|
186
|
+
# instance in scope) ----
|
|
187
|
+
|
|
188
|
+
# Wrap `content_html` in a full HTML page with the client-side
|
|
189
|
+
# bootstrap. The JS:
|
|
190
|
+
#
|
|
191
|
+
# 1. Opens a WS to `ws_path`.
|
|
192
|
+
# 2. On each incoming text frame: parses as HTML and assigns
|
|
193
|
+
# to #tep-live-root's innerHTML.
|
|
194
|
+
# 3. Intercepts clicks on anything with [data-event] and
|
|
195
|
+
# sends `{"event": <name>, "payload": <data-payload or "">}`
|
|
196
|
+
# over the WS.
|
|
197
|
+
#
|
|
198
|
+
# That's all the client-side surface for v1. No morphdom, no
|
|
199
|
+
# form-data shipping, no key bindings -- "click + re-render"
|
|
200
|
+
# is enough to demonstrate the pattern. Future chunks can swap
|
|
201
|
+
# in morphdom for diff-on-client.
|
|
202
|
+
def self.render_page(content_html, ws_path)
|
|
203
|
+
"<!doctype html>\n<html><head><meta charset='utf-8'></head><body>\n" +
|
|
204
|
+
content_html + "\n" +
|
|
205
|
+
"<script>(function(){\n" +
|
|
206
|
+
"var ws=new WebSocket((location.protocol==='https:'?'wss://':'ws://')+location.host+'" + ws_path + "');\n" +
|
|
207
|
+
"ws.onmessage=function(e){var r=document.getElementById('tep-live-root');if(r){r.outerHTML=e.data;}};\n" +
|
|
208
|
+
"document.addEventListener('click',function(e){\n" +
|
|
209
|
+
" var t=e.target;while(t&&!t.dataset.event){t=t.parentElement;}\n" +
|
|
210
|
+
" if(!t)return;\n" +
|
|
211
|
+
" e.preventDefault();\n" +
|
|
212
|
+
" ws.send(JSON.stringify({event:t.dataset.event,payload:t.dataset.payload||''}));\n" +
|
|
213
|
+
"});\n" +
|
|
214
|
+
"})();</script>\n" +
|
|
215
|
+
"</body></html>\n"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
end
|
|
219
|
+
end
|