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.
Files changed (193) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/Makefile +134 -0
  4. data/README.md +247 -0
  5. data/SINATRA_COMPAT.md +376 -0
  6. data/bin/tep +2156 -0
  7. data/examples/agentic_chat/README.md +103 -0
  8. data/examples/agentic_chat/app.rb +310 -0
  9. data/examples/api_gateway/README.md +49 -0
  10. data/examples/api_gateway/app.rb +66 -0
  11. data/examples/blog/app.rb +367 -0
  12. data/examples/blog/views/index.erb +36 -0
  13. data/examples/blog/views/login.erb +28 -0
  14. data/examples/blog/views/new_post.erb +25 -0
  15. data/examples/blog/views/show.erb +16 -0
  16. data/examples/chat/app.rb +278 -0
  17. data/examples/chat/assets/logo.svg +13 -0
  18. data/examples/chat/assets/style.css +209 -0
  19. data/examples/chat/views/index.erb +142 -0
  20. data/examples/chatbot/README.md +111 -0
  21. data/examples/chatbot/app.rb +1024 -0
  22. data/examples/chatbot/assets/chat.js +249 -0
  23. data/examples/chatbot/assets/compare.js +93 -0
  24. data/examples/chatbot/assets/markdown.js +84 -0
  25. data/examples/chatbot/assets/style.css +215 -0
  26. data/examples/chatbot/schema.sql +25 -0
  27. data/examples/chatbot/views/compare.erb +43 -0
  28. data/examples/chatbot/views/index.erb +42 -0
  29. data/examples/chatbot/views/login.erb +22 -0
  30. data/examples/chatbot/views/setup.erb +23 -0
  31. data/examples/counter/README.md +68 -0
  32. data/examples/counter/app.rb +85 -0
  33. data/examples/experiments/AGENTS.md +91 -0
  34. data/examples/experiments/README.md +99 -0
  35. data/examples/experiments/app.rb +225 -0
  36. data/examples/geohash/Gemfile +11 -0
  37. data/examples/geohash/Gemfile.lock +17 -0
  38. data/examples/geohash/README.md +58 -0
  39. data/examples/geohash/app.rb +33 -0
  40. data/examples/hello.rb +120 -0
  41. data/examples/llm_gateway/README.md +73 -0
  42. data/examples/llm_gateway/app.rb +91 -0
  43. data/examples/maidenhead/Gemfile +7 -0
  44. data/examples/maidenhead/Gemfile.lock +17 -0
  45. data/examples/maidenhead/README.md +47 -0
  46. data/examples/maidenhead/app.rb +46 -0
  47. data/examples/pg_hello.rb +76 -0
  48. data/examples/qdrant/Gemfile +11 -0
  49. data/examples/qdrant/Gemfile.lock +29 -0
  50. data/examples/qdrant/README.md +54 -0
  51. data/examples/sinatra_style.rb +32 -0
  52. data/examples/websocket_echo.rb +37 -0
  53. data/lib/tep/agent_delegation.rb +35 -0
  54. data/lib/tep/app.rb +291 -0
  55. data/lib/tep/assets.rb +52 -0
  56. data/lib/tep/auth.rb +78 -0
  57. data/lib/tep/auth_bearer_token.rb +126 -0
  58. data/lib/tep/auth_oauth2.rb +189 -0
  59. data/lib/tep/auth_oauth2_client.rb +29 -0
  60. data/lib/tep/auth_oauth2_code.rb +40 -0
  61. data/lib/tep/auth_session_cookie.rb +132 -0
  62. data/lib/tep/broadcast.rb +265 -0
  63. data/lib/tep/broadcast_subscription.rb +42 -0
  64. data/lib/tep/cache.rb +49 -0
  65. data/lib/tep/events.rb +257 -0
  66. data/lib/tep/filter.rb +21 -0
  67. data/lib/tep/handler.rb +35 -0
  68. data/lib/tep/http.rb +599 -0
  69. data/lib/tep/identity.rb +67 -0
  70. data/lib/tep/job.rb +186 -0
  71. data/lib/tep/json.rb +572 -0
  72. data/lib/tep/jwt.rb +126 -0
  73. data/lib/tep/live_view.rb +219 -0
  74. data/lib/tep/llm.rb +505 -0
  75. data/lib/tep/logger.rb +85 -0
  76. data/lib/tep/mcp.rb +203 -0
  77. data/lib/tep/multipart.rb +98 -0
  78. data/lib/tep/net.rb +155 -0
  79. data/lib/tep/openai_server.rb +725 -0
  80. data/lib/tep/parallel.rb +168 -0
  81. data/lib/tep/parser.rb +81 -0
  82. data/lib/tep/password.rb +102 -0
  83. data/lib/tep/pg.rb +1128 -0
  84. data/lib/tep/presence.rb +589 -0
  85. data/lib/tep/presence_entry.rb +52 -0
  86. data/lib/tep/proxy.rb +801 -0
  87. data/lib/tep/request.rb +194 -0
  88. data/lib/tep/response.rb +134 -0
  89. data/lib/tep/router.rb +137 -0
  90. data/lib/tep/scheduler.rb +342 -0
  91. data/lib/tep/security.rb +140 -0
  92. data/lib/tep/server.rb +276 -0
  93. data/lib/tep/server_scheduled.rb +375 -0
  94. data/lib/tep/session.rb +98 -0
  95. data/lib/tep/shell.rb +62 -0
  96. data/lib/tep/sphttp.c +858 -0
  97. data/lib/tep/sqlite.rb +215 -0
  98. data/lib/tep/streamer.rb +31 -0
  99. data/lib/tep/tep_pg.c +769 -0
  100. data/lib/tep/tep_sqlite.c +320 -0
  101. data/lib/tep/url.rb +161 -0
  102. data/lib/tep/version.rb +3 -0
  103. data/lib/tep/websocket/connection.rb +171 -0
  104. data/lib/tep/websocket/driver.rb +169 -0
  105. data/lib/tep/websocket/frame.rb +238 -0
  106. data/lib/tep/websocket/handshake.rb +159 -0
  107. data/lib/tep/websocket.rb +68 -0
  108. data/lib/tep.rb +981 -0
  109. data/public/hello.txt +1 -0
  110. data/public/style.css +4 -0
  111. data/spinel-ext.json +33 -0
  112. data/test/helper.rb +248 -0
  113. data/test/real_world/01_simple.rb +5 -0
  114. data/test/real_world/02_lifecycle.rb +20 -0
  115. data/test/real_world/03_chat.rb +75 -0
  116. data/test/real_world/04_health_api.rb +25 -0
  117. data/test/real_world/05_todo_api.rb +57 -0
  118. data/test/real_world/06_basic_auth.rb +25 -0
  119. data/test/real_world/07_bbc_rest_api.rb +228 -0
  120. data/test/real_world/07_sklise_things.rb +109 -0
  121. data/test/real_world/08_jwd83_helloworld.rb +56 -0
  122. data/test/run_all.rb +7 -0
  123. data/test/run_parallel.rb +89 -0
  124. data/test/spinel_scheduled_burst_segv_repro.rb +33 -0
  125. data/test/test_api_gateway.rb +76 -0
  126. data/test/test_auth.rb +223 -0
  127. data/test/test_auth_oauth2.rb +208 -0
  128. data/test/test_auth_session_cookie.rb +198 -0
  129. data/test/test_broadcast.rb +197 -0
  130. data/test/test_broadcast_pg.rb +135 -0
  131. data/test/test_cache.rb +98 -0
  132. data/test/test_cache_static.rb +48 -0
  133. data/test/test_cookies.rb +52 -0
  134. data/test/test_erb.rb +53 -0
  135. data/test/test_erb_ivars.rb +58 -0
  136. data/test/test_events.rb +114 -0
  137. data/test/test_filters.rb +41 -0
  138. data/test/test_geohash_example.rb +89 -0
  139. data/test/test_http.rb +137 -0
  140. data/test/test_http_pool.rb +122 -0
  141. data/test/test_http_pool_send.rb +57 -0
  142. data/test/test_identity.rb +165 -0
  143. data/test/test_inbound_tls.rb +101 -0
  144. data/test/test_inbound_tls_scheduled.rb +101 -0
  145. data/test/test_job.rb +108 -0
  146. data/test/test_json.rb +168 -0
  147. data/test/test_jwt.rb +143 -0
  148. data/test/test_live_view.rb +324 -0
  149. data/test/test_llm.rb +250 -0
  150. data/test/test_llm_gateway.rb +95 -0
  151. data/test/test_logger.rb +101 -0
  152. data/test/test_maidenhead_example.rb +86 -0
  153. data/test/test_mcp.rb +264 -0
  154. data/test/test_misc_v02.rb +54 -0
  155. data/test/test_modular.rb +43 -0
  156. data/test/test_multi_filters.rb +40 -0
  157. data/test/test_mustache.rb +57 -0
  158. data/test/test_openai_server.rb +598 -0
  159. data/test/test_optional_segments.rb +45 -0
  160. data/test/test_parallel.rb +102 -0
  161. data/test/test_params.rb +99 -0
  162. data/test/test_pass.rb +42 -0
  163. data/test/test_password.rb +101 -0
  164. data/test/test_pg.rb +673 -0
  165. data/test/test_presence.rb +374 -0
  166. data/test/test_presence_pg.rb +309 -0
  167. data/test/test_proxy.rb +556 -0
  168. data/test/test_proxy_dsl.rb +119 -0
  169. data/test/test_proxy_streaming.rb +146 -0
  170. data/test/test_real_world.rb +397 -0
  171. data/test/test_regex_routes.rb +52 -0
  172. data/test/test_request_methods.rb +102 -0
  173. data/test/test_response.rb +123 -0
  174. data/test/test_routing.rb +109 -0
  175. data/test/test_scheduler.rb +153 -0
  176. data/test/test_security.rb +72 -0
  177. data/test/test_server_scheduled.rb +56 -0
  178. data/test/test_sessions.rb +59 -0
  179. data/test/test_shell.rb +54 -0
  180. data/test/test_sqlite.rb +148 -0
  181. data/test/test_sqlite_cached.rb +171 -0
  182. data/test/test_static.rb +57 -0
  183. data/test/test_streaming.rb +96 -0
  184. data/test/test_unsupported.rb +32 -0
  185. data/test/test_websocket.rb +152 -0
  186. data/test/test_websocket_echo.rb +138 -0
  187. data/test/views/greet.erb +5 -0
  188. data/test/views/hello.erb +5 -0
  189. data/test/views/list.erb +5 -0
  190. data/test/views/m_ivars.mustache +3 -0
  191. data/test/views/m_simple.mustache +4 -0
  192. data/test/views/mixed.erb +3 -0
  193. 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