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
data/lib/tep/job.rb ADDED
@@ -0,0 +1,186 @@
1
+ # Tep::Job -- sidekiq-shaped background jobs over a SQLite queue.
2
+ #
3
+ # Why a queue at all?
4
+ # -------------------
5
+ # Tep::Parallel covers synchronous fan-out within one request. Some
6
+ # work doesn't fit: it's too slow to inline (an LLM call), needs to
7
+ # survive the request lifetime (a follow-up email), or should run
8
+ # on a cron-like cadence (refresh a cached snapshot). For those,
9
+ # you want sidekiq's shape: enqueue from anywhere, a separate
10
+ # worker process drains the queue.
11
+ #
12
+ # Storage
13
+ # -------
14
+ # SQLite, in a table the framework creates on demand:
15
+ #
16
+ # CREATE TABLE tep_jobs (
17
+ # id INTEGER PRIMARY KEY,
18
+ # job_name TEXT, -- registered class identifier
19
+ # arg TEXT, -- single string payload
20
+ # status TEXT, -- queued|running|done|failed
21
+ # created_at INTEGER,
22
+ # finished_at INTEGER,
23
+ # result TEXT
24
+ # )
25
+ #
26
+ # The single-arg payload is intentional: structured data goes
27
+ # through JSON (Tep::Json) which we already ship. Sidekiq's
28
+ # multi-arg `perform_async(a, b, c)` translates to encoding the
29
+ # tuple as a JSON string and decoding it in `perform`.
30
+ #
31
+ # API
32
+ # ---
33
+ # Define a job by subclassing Tep::Job and overriding `perform`:
34
+ #
35
+ # class HelloJob < Tep::Job
36
+ # def perform(arg)
37
+ # Tep::Logger.new.info("hello " + arg)
38
+ # "done"
39
+ # end
40
+ # end
41
+ #
42
+ # Enqueue from anywhere:
43
+ #
44
+ # Tep::Job.enqueue("HelloJob", "world", DB_PATH)
45
+ #
46
+ # Worker side: fetch one, dispatch, mark done. The dispatch is
47
+ # user-written because spinel doesn't carry cls_id tags through
48
+ # `PtrArray<Tep::Job>`, so the framework can't virtual-dispatch
49
+ # `handler.perform(arg)` to the right subclass on its own.
50
+ #
51
+ # loop do
52
+ # claim = Tep::Job.fetch_next(DB_PATH) # "" if empty, else
53
+ # # "row_id|name|arg"
54
+ # break if claim.length == 0
55
+ # parts = claim.split("|", 3)
56
+ # row_id = parts[0].to_i
57
+ # name = parts[1]
58
+ # arg = parts[2]
59
+ # result = ""
60
+ # if name == "HelloJob"
61
+ # result = HelloJob.new.perform(arg)
62
+ # end
63
+ # Tep::Job.mark_done(DB_PATH, row_id, result)
64
+ # end
65
+ #
66
+ # The verbosity of the `if name == "..."` ladder is the price of
67
+ # type safety in spinel. A future bin/tep pass could generate this
68
+ # dispatcher from the set of `Tep::Job` subclasses at compile time
69
+ # (mirroring the way routes are generated from `get '/x' do .. end`),
70
+ # at which point this surface becomes a one-liner. For v0.5 the
71
+ # manual ladder is fine -- a single tep app rarely has more than
72
+ # a handful of distinct job classes.
73
+ #
74
+ # Comparison to sidekiq
75
+ # ---------------------
76
+ # Sidekiq's `MyJob.perform_async(x)` enqueues on a Redis list keyed
77
+ # by class name. We do the same with SQLite + an explicit name
78
+ # string. The `Tep::Job` subclass + `perform(arg)` shape stays;
79
+ # only the worker drain loop differs (sidekiq does the dispatch via
80
+ # Ruby's `Object.const_get`, which spinel can't lower).
81
+ module Tep
82
+ class Job
83
+ # Subclasses override. The default uses `arg` as :str so spinel's
84
+ # analyzer pins the param type rather than defaulting to :int
85
+ # for an unused parameter -- otherwise subclass `arg.upcase` calls
86
+ # fail to resolve against an int-typed slot.
87
+ def perform(arg)
88
+ "" + arg
89
+ end
90
+
91
+ # Idempotent. Creates the queue table if missing. Pass the same
92
+ # SQLite path to enqueue / fetch_next / mark_done.
93
+ def self.init_schema(db_path)
94
+ db = Tep::SQLite.new
95
+ if db.open(db_path)
96
+ db.exec("CREATE TABLE IF NOT EXISTS tep_jobs (" +
97
+ "id INTEGER PRIMARY KEY, " +
98
+ "job_name TEXT, arg TEXT, status TEXT, " +
99
+ "created_at INTEGER, finished_at INTEGER, result TEXT)")
100
+ db.close
101
+ end
102
+ 0
103
+ end
104
+
105
+ # Append a `queued` row. Returns the new row id (0 on DB error).
106
+ def self.enqueue(name, arg, db_path)
107
+ db = Tep::SQLite.new
108
+ if !db.open(db_path)
109
+ return 0
110
+ end
111
+ db.prepare("INSERT INTO tep_jobs (job_name, arg, status, created_at) VALUES (?, ?, ?, ?)")
112
+ db.bind_str(1, name)
113
+ db.bind_str(2, arg)
114
+ db.bind_str(3, "queued")
115
+ db.bind_int(4, Time.now.to_i)
116
+ db.step
117
+ db.finalize
118
+ id = db.last_rowid
119
+ db.close
120
+ id
121
+ end
122
+
123
+ # Claim the oldest `queued` row and mark it `running`. Returns
124
+ # "row_id|name|arg" packed into one string (the caller splits on
125
+ # "|" with limit 3), or "" if the queue is empty / errored. The
126
+ # row_id is needed for the matching `mark_done` call. Caller is
127
+ # responsible for dispatching to the right subclass and then
128
+ # writing the result back via `mark_done`.
129
+ def self.fetch_next(db_path)
130
+ db = Tep::SQLite.new
131
+ if !db.open(db_path)
132
+ return ""
133
+ end
134
+ db.prepare("SELECT id, job_name, arg FROM tep_jobs WHERE status = 'queued' ORDER BY id ASC LIMIT 1")
135
+ out = ""
136
+ if db.step == 1
137
+ row_id = db.col_int(0)
138
+ job_name = db.col_str(1)
139
+ arg = db.col_str(2)
140
+ out = row_id.to_s + "|" + job_name + "|" + arg
141
+ end
142
+ db.finalize
143
+ if out.length > 0
144
+ db.prepare("UPDATE tep_jobs SET status = 'running' WHERE id = ?")
145
+ db.bind_int(1, row_id)
146
+ db.step
147
+ db.finalize
148
+ end
149
+ db.close
150
+ out
151
+ end
152
+
153
+ # Mark the row `done` and store the result string.
154
+ def self.mark_done(db_path, row_id, result)
155
+ db = Tep::SQLite.new
156
+ if !db.open(db_path)
157
+ return 0
158
+ end
159
+ db.prepare("UPDATE tep_jobs SET status = 'done', finished_at = ?, result = ? WHERE id = ?")
160
+ db.bind_int(1, Time.now.to_i)
161
+ db.bind_str(2, result)
162
+ db.bind_int(3, row_id)
163
+ db.step
164
+ db.finalize
165
+ db.close
166
+ 1
167
+ end
168
+
169
+ # Mark the row `failed`. The error message is not stored by this
170
+ # method; the user writes it via their own SQLite calls if they
171
+ # want it persisted.
172
+ def self.mark_failed(db_path, row_id)
173
+ db = Tep::SQLite.new
174
+ if !db.open(db_path)
175
+ return 0
176
+ end
177
+ db.prepare("UPDATE tep_jobs SET status = 'failed', finished_at = ? WHERE id = ?")
178
+ db.bind_int(1, Time.now.to_i)
179
+ db.bind_int(2, row_id)
180
+ db.step
181
+ db.finalize
182
+ db.close
183
+ 1
184
+ end
185
+ end
186
+ end