@rip-lang/db 1.0.1 → 1.0.2
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.
- package/README.md +103 -120
- package/bin/rip-db +4 -9
- package/db.rip +224 -230
- package/lib/duckdb-binary.rip +546 -0
- package/lib/duckdb.mjs +727 -250
- package/package.json +8 -11
- package/INTERNALS.md +0 -324
- package/build.zig +0 -88
- package/lib/darwin-arm64/duckdb.node +0 -0
- package/src/duckdb.zig +0 -1156
package/db.rip
CHANGED
|
@@ -6,30 +6,38 @@
|
|
|
6
6
|
# Fully compatible with the official DuckDB UI!
|
|
7
7
|
#
|
|
8
8
|
# Usage:
|
|
9
|
-
# rip db.rip <database.duckdb> [--port
|
|
10
|
-
# rip db.rip :memory: --port
|
|
9
|
+
# rip db.rip <database.duckdb> [--port 4213]
|
|
10
|
+
# rip db.rip :memory: --port 4213
|
|
11
11
|
#
|
|
12
|
-
# Then open http://localhost:
|
|
12
|
+
# Then open http://localhost:4213/ for the official DuckDB UI!
|
|
13
13
|
# (UI assets are proxied from ui.duckdb.org)
|
|
14
14
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# GET /health - Health check
|
|
20
|
-
# GET /status - Database info
|
|
21
|
-
# GET /tables - List tables
|
|
22
|
-
# GET /schema/:table - Table schema
|
|
15
|
+
# Architecture:
|
|
16
|
+
# - Pure Bun FFI to DuckDB library (no npm package, no Zig)
|
|
17
|
+
# - Binary serialization in Rip for DuckDB UI protocol
|
|
18
|
+
# - Simple, clean, fast
|
|
23
19
|
#
|
|
24
20
|
# ==============================================================================
|
|
25
21
|
|
|
26
22
|
import { get, post, read, start, use } from '@rip-lang/api'
|
|
27
23
|
import { cors } from '@rip-lang/api/middleware'
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
import { version as VERSION } from './package.json'
|
|
25
|
+
|
|
26
|
+
# Database access via pure Bun FFI
|
|
27
|
+
duckdb = await import('./lib/duckdb.mjs')
|
|
28
|
+
open = duckdb.open
|
|
29
|
+
duckdbVersion = duckdb.version
|
|
30
|
+
|
|
31
|
+
# Binary serialization for DuckDB UI protocol
|
|
32
|
+
{
|
|
33
|
+
serializeSuccessResult
|
|
34
|
+
serializeErrorResult
|
|
35
|
+
serializeEmptyResult
|
|
36
|
+
serializeTokenizeResult
|
|
37
|
+
tokenizeSQL
|
|
38
|
+
} = await import('./lib/duckdb-binary.rip')
|
|
39
|
+
|
|
40
|
+
# Enable CORS for DuckDB UI and other clients
|
|
33
41
|
use cors preflight: true
|
|
34
42
|
|
|
35
43
|
# Log all requests
|
|
@@ -50,7 +58,7 @@ args = process.argv.slice(2)
|
|
|
50
58
|
# Handle --help and --version
|
|
51
59
|
if '--help' in args or '-h' in args
|
|
52
60
|
console.log """
|
|
53
|
-
rip-db v#{VERSION} — DuckDB Server (Bun
|
|
61
|
+
rip-db v#{VERSION} — DuckDB Server (Pure Bun FFI)
|
|
54
62
|
|
|
55
63
|
Usage: rip db.rip [database] [options]
|
|
56
64
|
|
|
@@ -62,24 +70,17 @@ if '--help' in args or '-h' in args
|
|
|
62
70
|
--help Show this help
|
|
63
71
|
--version Show version
|
|
64
72
|
|
|
65
|
-
Environment variables (for rip-server):
|
|
66
|
-
DB_PATH Path to DuckDB file
|
|
67
|
-
DB_PORT Port to listen on
|
|
68
|
-
|
|
69
73
|
Endpoints:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
GET /
|
|
74
|
-
GET /
|
|
74
|
+
GET / Official DuckDB UI
|
|
75
|
+
POST /sql Execute SQL (JSON API)
|
|
76
|
+
POST /ddb/run DuckDB UI binary protocol
|
|
77
|
+
GET /health Health check
|
|
78
|
+
GET /tables List all tables
|
|
75
79
|
|
|
76
80
|
Examples:
|
|
77
81
|
rip db.rip # In-memory database on port 4213
|
|
78
82
|
rip db.rip mydb.duckdb # File-based database
|
|
79
83
|
rip db.rip :memory: --port=8080
|
|
80
|
-
|
|
81
|
-
# With rip-server (hot-reloading):
|
|
82
|
-
DB_PATH=mydb.duckdb rip-server http db.rip
|
|
83
84
|
"""
|
|
84
85
|
process.exit(0)
|
|
85
86
|
|
|
@@ -87,283 +88,277 @@ if '--version' in args or '-v' in args
|
|
|
87
88
|
console.log "rip-db v#{VERSION}"
|
|
88
89
|
process.exit(0)
|
|
89
90
|
|
|
90
|
-
#
|
|
91
|
+
# Database and port configuration
|
|
91
92
|
path = process.env.DB_PATH or args.find((a) -> not a.startsWith('-')) or ':memory:'
|
|
92
|
-
port = parseInt(process.env.DB_PORT or (args.find((a) -> a.startsWith('--port=')))?.split('=')[1]) or 4213
|
|
93
93
|
|
|
94
|
-
#
|
|
94
|
+
# Support both --port=N and --port N
|
|
95
|
+
portArg = do ->
|
|
96
|
+
for a, i in args
|
|
97
|
+
return a.split('=')[1] if a.startsWith('--port=')
|
|
98
|
+
return args[i + 1] if a is '--port' and args[i + 1]
|
|
99
|
+
null
|
|
100
|
+
port = parseInt(process.env.DB_PORT or portArg) or 4213
|
|
101
|
+
|
|
102
|
+
# Open database and create persistent connection
|
|
95
103
|
db = open(path)
|
|
96
|
-
|
|
97
|
-
console.log "rip-db: database=#{path} (bun-
|
|
104
|
+
conn = db.connect()
|
|
105
|
+
console.log "rip-db: database=#{path} (bun-ffi)"
|
|
106
|
+
console.log "rip-db: DuckDB version=#{duckdbVersion()}"
|
|
98
107
|
|
|
99
108
|
# ==============================================================================
|
|
100
109
|
# Helpers
|
|
101
110
|
# ==============================================================================
|
|
102
111
|
|
|
103
|
-
#
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
types = columns.map (col) ->
|
|
109
|
-
switch typeof val = first[col]
|
|
110
|
-
when 'number' then Number.isInteger(val) ? 'INTEGER' : 'DOUBLE'
|
|
111
|
-
when 'string' then 'VARCHAR'
|
|
112
|
-
when 'boolean' then 'BOOLEAN'
|
|
113
|
-
when 'bigint' then 'BIGINT'
|
|
114
|
-
else val is null ? 'NULL' : 'UNKNOWN'
|
|
115
|
-
{ columns, types }
|
|
116
|
-
|
|
117
|
-
# Convert row objects to arrays for compact format
|
|
118
|
-
rowsToArrays = (rows, columns) ->
|
|
119
|
-
rows.map (row) -> columns.map (col) -> row[col]
|
|
120
|
-
|
|
121
|
-
# Check if SQL is a SELECT-type query
|
|
112
|
+
# Helper for binary responses
|
|
113
|
+
binaryResponse = (data) ->
|
|
114
|
+
new Response data, headers: { 'Content-Type': 'application/octet-stream' }
|
|
115
|
+
|
|
116
|
+
# Check if SQL is a query that returns results
|
|
122
117
|
isSelectQuery = (sql) ->
|
|
123
118
|
upper = sql.trim().toUpperCase()
|
|
124
119
|
upper.startsWith('SELECT') or
|
|
125
120
|
upper.startsWith('WITH') or
|
|
126
121
|
upper.startsWith('SHOW') or
|
|
127
122
|
upper.startsWith('DESCRIBE') or
|
|
123
|
+
upper.startsWith('EXPLAIN') or
|
|
128
124
|
upper.startsWith('PRAGMA')
|
|
129
125
|
|
|
130
|
-
#
|
|
131
|
-
|
|
126
|
+
# Log SQL query (truncated if long)
|
|
127
|
+
logSQL = (sql) ->
|
|
128
|
+
preview = sql.replace(/\s+/g, ' ').trim()
|
|
129
|
+
preview = "#{preview.slice(0, 100)}..." if preview.length > 100
|
|
130
|
+
console.log "SQL: #{preview}"
|
|
131
|
+
|
|
132
|
+
# Execute SQL and return result (uses persistent connection)
|
|
133
|
+
executeSQL = (sql, params = []) ->
|
|
134
|
+
logSQL sql
|
|
132
135
|
startTime = Date.now()
|
|
133
|
-
conn = db.connect()
|
|
134
136
|
|
|
135
137
|
try
|
|
136
|
-
rows = conn.query(sql)
|
|
138
|
+
rows = await conn.query(sql, params)
|
|
139
|
+
columns = rows.columns or []
|
|
137
140
|
|
|
138
141
|
if isSelectQuery(sql)
|
|
139
|
-
{ columns, types } = getColumnInfo(rows)
|
|
140
|
-
data = rowsToArrays(rows, columns)
|
|
141
142
|
{
|
|
142
|
-
meta: columns.map((
|
|
143
|
-
data:
|
|
144
|
-
rows:
|
|
143
|
+
meta: columns.map((col) -> { name: col.name, type: col.typeName })
|
|
144
|
+
data: rows.map((row) -> columns.map((col) -> row[col.name]))
|
|
145
|
+
rows: rows.length
|
|
145
146
|
time: (Date.now() - startTime) / 1000
|
|
146
147
|
}
|
|
147
148
|
else
|
|
148
149
|
{ meta: [], data: [], rows: 0, time: (Date.now() - startTime) / 1000 }
|
|
149
150
|
catch err
|
|
150
151
|
{ error: err.message }
|
|
151
|
-
finally
|
|
152
|
-
conn.close()
|
|
153
152
|
|
|
154
153
|
# ==============================================================================
|
|
155
|
-
# Endpoints
|
|
154
|
+
# JSON Endpoints (for custom apps)
|
|
156
155
|
# ==============================================================================
|
|
157
156
|
|
|
158
|
-
# POST / —
|
|
159
|
-
post '/' ->
|
|
160
|
-
sql = read 'body', 'string' or return { error: 'Empty query' }
|
|
161
|
-
# Log the SQL (truncate long queries)
|
|
162
|
-
preview = sql.replace(/\s+/g, ' ').trim()
|
|
163
|
-
preview = "#{preview.slice(0, 120)}..." if preview.length > 120
|
|
164
|
-
console.log "SQL: #{preview}"
|
|
165
|
-
executeSQL sql
|
|
166
|
-
|
|
167
|
-
# POST /sql — JSON body with SQL query
|
|
157
|
+
# POST /sql — Execute SQL with optional parameters
|
|
168
158
|
post '/sql' ->
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
159
|
+
contentType = @req.header('content-type') or ''
|
|
160
|
+
if contentType.includes('application/json')
|
|
161
|
+
body = read()
|
|
162
|
+
sql = body?.sql or body
|
|
163
|
+
params = body?.params or []
|
|
164
|
+
else
|
|
165
|
+
sql = read 'body', 'string'
|
|
166
|
+
params = []
|
|
167
|
+
return { error: 'Empty query' } unless sql
|
|
168
|
+
executeSQL sql, params
|
|
169
|
+
|
|
170
|
+
# POST / — Raw SQL in body (duck-ui compatible)
|
|
171
|
+
post '/' ->
|
|
172
|
+
sql = read 'body', 'string'
|
|
173
|
+
return { error: 'Empty query' } unless sql
|
|
173
174
|
executeSQL sql
|
|
174
175
|
|
|
175
|
-
# GET /health —
|
|
176
|
-
get '/health'
|
|
177
|
-
{ ok:
|
|
178
|
-
|
|
179
|
-
# GET /status — Database info and table list
|
|
180
|
-
get '/status', ->
|
|
181
|
-
conn = db.connect()
|
|
182
|
-
try
|
|
183
|
-
tables = conn.query("SELECT table_name FROM information_schema.tables WHERE table_schema = 'main'")
|
|
184
|
-
{
|
|
185
|
-
ok: true
|
|
186
|
-
database: path
|
|
187
|
-
tables: tables.map((t) -> t.table_name)
|
|
188
|
-
time: new Date().toISOString()
|
|
189
|
-
}
|
|
190
|
-
catch err
|
|
191
|
-
{ ok: false, error: err.message }
|
|
192
|
-
finally
|
|
193
|
-
conn.close()
|
|
176
|
+
# GET /health — Health check
|
|
177
|
+
get '/health' ->
|
|
178
|
+
{ status: 'ok', version: VERSION }
|
|
194
179
|
|
|
195
180
|
# GET /tables — List all tables
|
|
196
|
-
get '/tables'
|
|
197
|
-
conn =
|
|
198
|
-
|
|
199
|
-
tables = conn.query("SELECT table_name FROM information_schema.tables WHERE table_schema = 'main' ORDER BY table_name")
|
|
200
|
-
{ ok: true, tables: tables.map((t) -> t.table_name) }
|
|
201
|
-
catch err
|
|
202
|
-
{ ok: false, error: err.message }
|
|
203
|
-
finally
|
|
204
|
-
conn.close()
|
|
181
|
+
get '/tables' ->
|
|
182
|
+
rows = await conn.query "SELECT table_name FROM information_schema.tables WHERE table_schema = 'main'"
|
|
183
|
+
{ tables: rows.map((r) -> r.table_name) }
|
|
205
184
|
|
|
206
185
|
# GET /schema/:table — Get table schema
|
|
207
|
-
get '/schema/:table'
|
|
208
|
-
table =
|
|
209
|
-
conn = db.connect()
|
|
186
|
+
get '/schema/:table' ->
|
|
187
|
+
table = @req.params.table.replace(/[^a-zA-Z0-9_]/g, '') # sanitize
|
|
210
188
|
try
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if columns.length is 0
|
|
214
|
-
{ ok: false, error: "Table '#{table}' not found" }
|
|
215
|
-
else
|
|
216
|
-
{ ok: true, table: table, columns: columns }
|
|
189
|
+
rows = await conn.query "DESCRIBE \"#{table}\""
|
|
190
|
+
{ schema: rows }
|
|
217
191
|
catch err
|
|
218
|
-
{
|
|
219
|
-
finally
|
|
220
|
-
conn.close()
|
|
192
|
+
{ error: err.message }
|
|
221
193
|
|
|
222
194
|
# ==============================================================================
|
|
223
|
-
# DuckDB UI
|
|
224
|
-
# ==============================================================================
|
|
225
|
-
#
|
|
226
|
-
# These endpoints implement the binary protocol used by DuckDB's official UI.
|
|
227
|
-
# This allows you to use the beautiful DuckDB UI with rip-db!
|
|
228
|
-
#
|
|
229
|
-
# The official UI expects these endpoints:
|
|
230
|
-
# POST /ddb/run - Execute SQL (binary response)
|
|
231
|
-
# POST /ddb/interrupt - Cancel running query
|
|
232
|
-
# POST /ddb/tokenize - Tokenize SQL for syntax highlighting
|
|
233
|
-
# GET /info - Server version info
|
|
234
|
-
#
|
|
235
|
-
# To use the DuckDB UI:
|
|
236
|
-
# 1. Start rip-db: rip-db mydb.duckdb --port 4000
|
|
237
|
-
# 2. Open: http://localhost:4000 (UI proxied from ui.duckdb.org)
|
|
238
|
-
#
|
|
195
|
+
# DuckDB UI Binary Protocol
|
|
239
196
|
# ==============================================================================
|
|
240
197
|
|
|
198
|
+
# Remote UI URL for proxying assets
|
|
199
|
+
UI_REMOTE_URL = 'https://ui.duckdb.org'
|
|
200
|
+
|
|
201
|
+
# Helper to extract parameters from DuckDB UI headers (base64 encoded)
|
|
202
|
+
# The official C++ extension passes all params as strings (VARCHAR).
|
|
203
|
+
# DuckDB handles implicit casting to target types.
|
|
204
|
+
getUIParams = (req) ->
|
|
205
|
+
count = parseInt(req.header('x-duckdb-ui-parameter-count') or '0')
|
|
206
|
+
return [] if count is 0
|
|
207
|
+
|
|
208
|
+
params = []
|
|
209
|
+
for i in [0...count]
|
|
210
|
+
encoded = req.header("x-duckdb-ui-parameter-value-#{i}")
|
|
211
|
+
if encoded
|
|
212
|
+
try
|
|
213
|
+
decoded = atob(encoded)
|
|
214
|
+
# The UI sends literal "null" for NULL values
|
|
215
|
+
if decoded is 'null'
|
|
216
|
+
params.push null
|
|
217
|
+
else
|
|
218
|
+
params.push decoded
|
|
219
|
+
catch
|
|
220
|
+
params.push ''
|
|
221
|
+
else
|
|
222
|
+
params.push null
|
|
223
|
+
params
|
|
224
|
+
|
|
241
225
|
# POST /ddb/run — Execute SQL and return binary result (DuckDB UI protocol)
|
|
242
226
|
post '/ddb/run' ->
|
|
243
227
|
try
|
|
244
|
-
sql = read 'body', 'string'
|
|
228
|
+
sql = read 'body', 'string'
|
|
229
|
+
return binaryResponse serializeErrorResult 'Empty query' unless sql
|
|
245
230
|
|
|
246
|
-
|
|
231
|
+
logSQL sql
|
|
247
232
|
rowLimit = parseInt(@req.header('x-duckdb-ui-result-row-limit') or '10000')
|
|
248
|
-
|
|
233
|
+
params = getUIParams(@req)
|
|
249
234
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
235
|
+
rows = await conn.query sql, params
|
|
236
|
+
columns = rows.columns or []
|
|
237
|
+
|
|
238
|
+
# Build column info for serialization
|
|
239
|
+
colInfo = columns.map (col) -> { name: col.name, type: col.typeName }
|
|
240
|
+
|
|
241
|
+
# Convert rows to arrays (ensure undefined becomes null for serialization)
|
|
242
|
+
limitedRows = rows.slice(0, rowLimit)
|
|
243
|
+
arrayRows = limitedRows.map (row) ->
|
|
244
|
+
columns.map (col) ->
|
|
245
|
+
val = row[col.name]
|
|
246
|
+
if val is undefined then null else val
|
|
247
|
+
|
|
248
|
+
binaryResponse serializeSuccessResult colInfo, arrayRows
|
|
254
249
|
|
|
255
250
|
catch err
|
|
256
|
-
|
|
257
|
-
|
|
251
|
+
msg = err?.message or 'Unknown error'
|
|
252
|
+
# Handle _duckdb_ui catalog queries - return empty success instead of error
|
|
253
|
+
if msg.includes('_duckdb_ui')
|
|
254
|
+
return binaryResponse serializeSuccessResult [], []
|
|
255
|
+
console.error "POST /ddb/run error:", msg
|
|
256
|
+
binaryResponse serializeErrorResult msg
|
|
258
257
|
|
|
259
258
|
# POST /ddb/interrupt — Cancel running query
|
|
260
|
-
post '/ddb/interrupt'
|
|
261
|
-
|
|
262
|
-
# For now, just return empty result
|
|
263
|
-
binaryResponse emptyResult()
|
|
259
|
+
post '/ddb/interrupt' ->
|
|
260
|
+
binaryResponse serializeEmptyResult()
|
|
264
261
|
|
|
265
262
|
# POST /ddb/tokenize — Tokenize SQL for syntax highlighting
|
|
266
263
|
post '/ddb/tokenize' ->
|
|
267
264
|
sql = read 'body', 'string'
|
|
268
|
-
|
|
265
|
+
tokens = tokenizeSQL(sql or '')
|
|
266
|
+
binaryResponse serializeTokenizeResult(tokens)
|
|
267
|
+
|
|
268
|
+
# ==============================================================================
|
|
269
|
+
# DuckDB UI Compatibility Endpoints
|
|
270
|
+
# (Responses match the official DuckDB UI extension exactly)
|
|
271
|
+
# ==============================================================================
|
|
272
|
+
|
|
273
|
+
# UI extension version — must match what /config advertises for our DuckDB version
|
|
274
|
+
UI_EXT_VERSION = '150-5582dfaffc'
|
|
275
|
+
|
|
276
|
+
# Detect platform for DuckDB headers
|
|
277
|
+
PLATFORM = switch "#{process.platform}_#{process.arch}"
|
|
278
|
+
when 'darwin_arm64' then 'osx_arm64'
|
|
279
|
+
when 'darwin_x64' then 'osx_amd64'
|
|
280
|
+
when 'linux_arm64' then 'linux_arm64'
|
|
281
|
+
when 'linux_x64' then 'linux_amd64'
|
|
282
|
+
else "#{process.platform}_#{process.arch}"
|
|
269
283
|
|
|
270
284
|
# GET /version — Tell UI we're running in host/HTTP mode (not WASM)
|
|
271
285
|
get '/version' ->
|
|
272
|
-
{ origin: 'host', version:
|
|
286
|
+
{ origin: 'host', version: "main@e6517d259ec5f27ab713e5755a29d775a7750dc5" }
|
|
273
287
|
|
|
274
|
-
# GET /localToken — Return
|
|
288
|
+
# GET /localToken — Return 401 to skip MotherDuck auth (matches official)
|
|
275
289
|
get '/localToken' ->
|
|
276
|
-
new Response
|
|
290
|
+
new Response null, { status: 401 }
|
|
277
291
|
|
|
278
|
-
# GET /info — Server version info (
|
|
279
|
-
# The UI checks X-DuckDB-UI-Extension-Version to decide HTTP vs WASM mode
|
|
280
|
-
# Version format must match desiredDuckDBUIExtensionVersions in /config
|
|
292
|
+
# GET /info — Server version info (headers only, empty body — matches official)
|
|
281
293
|
get '/info' ->
|
|
282
294
|
@body '', 200,
|
|
283
295
|
'Access-Control-Allow-Origin': '*'
|
|
284
|
-
'
|
|
285
|
-
'X-DuckDB-
|
|
286
|
-
'X-DuckDB-
|
|
296
|
+
'Content-Type': 'text/plain'
|
|
297
|
+
'X-DuckDB-Version': duckdbVersion()
|
|
298
|
+
'X-DuckDB-Platform': PLATFORM
|
|
299
|
+
'X-DuckDB-UI-Extension-Version': UI_EXT_VERSION
|
|
287
300
|
|
|
288
|
-
#
|
|
289
|
-
|
|
290
|
-
new Response buffer,
|
|
291
|
-
headers: { 'Content-Type': 'application/octet-stream' }
|
|
292
|
-
|
|
293
|
-
# ==============================================================================
|
|
294
|
-
# DuckDB UI Proxy — Serve official UI assets from ui.duckdb.org
|
|
295
|
-
# ==============================================================================
|
|
296
|
-
#
|
|
297
|
-
# The official DuckDB UI is a React app hosted at https://ui.duckdb.org
|
|
298
|
-
# We proxy these assets so you can use the beautiful UI with rip-db!
|
|
299
|
-
#
|
|
300
|
-
# The UI makes API requests to relative URLs (/ddb/run, /ddb/tokenize, etc.)
|
|
301
|
-
# which we handle above. This proxy just serves the static UI assets.
|
|
302
|
-
#
|
|
303
|
-
# ==============================================================================
|
|
304
|
-
|
|
305
|
-
UI_REMOTE_URL = 'https://ui.duckdb.org'
|
|
306
|
-
|
|
307
|
-
# GET /config — Local mode configuration (prevents MotherDuck redirect)
|
|
301
|
+
# GET /config — Proxy from ui.duckdb.org with DuckDB version headers
|
|
302
|
+
# The UI checks these HEADERS (not just /info) to decide HTTP vs WASM mode
|
|
308
303
|
get '/config' ->
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
'
|
|
327
|
-
|
|
304
|
+
try
|
|
305
|
+
response = fetch! "#{UI_REMOTE_URL}/config",
|
|
306
|
+
headers: { 'User-Agent': "rip-db/#{VERSION}" }
|
|
307
|
+
body = response.text!
|
|
308
|
+
new Response body,
|
|
309
|
+
status: response.status
|
|
310
|
+
headers:
|
|
311
|
+
'Content-Type': 'application/json'
|
|
312
|
+
'X-DuckDB-Version': duckdbVersion()
|
|
313
|
+
'X-DuckDB-Platform': PLATFORM
|
|
314
|
+
'X-DuckDB-UI-Extension-Version': UI_EXT_VERSION
|
|
315
|
+
catch
|
|
316
|
+
new Response JSON.stringify({
|
|
317
|
+
globalApiUrl: "http://localhost:#{port}"
|
|
318
|
+
apiUrl: "http://localhost:#{port}"
|
|
319
|
+
momApiUrl: "http://localhost:#{port}/mom"
|
|
320
|
+
logging: { enabled: false }
|
|
321
|
+
datadog: { applicationId: '', clientToken: '', site: '', env: 'local', allowedTracingOrigins: [] }
|
|
322
|
+
defaultFeatureValues: {}
|
|
323
|
+
desiredDuckDBUIExtensionVersions: { "#{duckdbVersion()}": UI_EXT_VERSION }
|
|
324
|
+
}),
|
|
325
|
+
headers:
|
|
326
|
+
'Content-Type': 'application/json'
|
|
327
|
+
'X-DuckDB-Version': duckdbVersion()
|
|
328
|
+
'X-DuckDB-Platform': PLATFORM
|
|
329
|
+
'X-DuckDB-UI-Extension-Version': UI_EXT_VERSION
|
|
328
330
|
|
|
329
|
-
# GET /localEvents —
|
|
331
|
+
# GET /localEvents — SSE stream with keepalive (matches official extension)
|
|
332
|
+
# The official blocks 5s per iteration, sends ":\r\r" keepalive if no events.
|
|
330
333
|
get '/localEvents' ->
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
stream = new ReadableStream
|
|
334
|
+
keepalive = null
|
|
335
|
+
new Response new ReadableStream({
|
|
334
336
|
start: (controller) ->
|
|
335
|
-
# Send
|
|
336
|
-
|
|
337
|
-
# Keep-alive with pings every 5 seconds (Bun has 10s idle timeout)
|
|
338
|
-
@interval = setInterval =>
|
|
337
|
+
# Send keepalive every 5 seconds (SSE comment, matches official ":\r\r")
|
|
338
|
+
keepalive = setInterval ->
|
|
339
339
|
try
|
|
340
|
-
controller.enqueue
|
|
340
|
+
controller.enqueue ":\r\r"
|
|
341
341
|
catch
|
|
342
|
-
clearInterval
|
|
342
|
+
clearInterval keepalive
|
|
343
343
|
, 5000
|
|
344
344
|
cancel: ->
|
|
345
|
-
clearInterval
|
|
346
|
-
|
|
347
|
-
new Response stream,
|
|
345
|
+
clearInterval keepalive if keepalive
|
|
346
|
+
}),
|
|
348
347
|
headers:
|
|
349
348
|
'Content-Type': 'text/event-stream'
|
|
350
|
-
|
|
351
|
-
|
|
349
|
+
|
|
350
|
+
# GET /mom/* — MotherDuck API stubs (prevent OAuth, return 401)
|
|
351
|
+
get '/mom/*' ->
|
|
352
|
+
new Response null, { status: 401 }
|
|
352
353
|
|
|
353
354
|
# GET /* — Proxy UI assets from ui.duckdb.org (catch-all, must be last)
|
|
354
355
|
get '/*' ->
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
# Skip if this is one of our API endpoints (shouldn't reach here, but safety)
|
|
358
|
-
return { error: 'Not found' } if path in ['/health', '/status', '/tables', '/info']
|
|
356
|
+
reqPath = @req.path
|
|
359
357
|
|
|
360
358
|
try
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
headers:
|
|
364
|
-
'User-Agent': "rip-db/#{VERSION}"
|
|
359
|
+
response = fetch! "#{UI_REMOTE_URL}#{reqPath}",
|
|
360
|
+
headers: { 'User-Agent': "rip-db/#{VERSION}" }
|
|
365
361
|
|
|
366
|
-
# Get response body and content type
|
|
367
362
|
body = response.arrayBuffer!
|
|
368
363
|
contentType = response.headers.get('Content-Type') or 'application/octet-stream'
|
|
369
364
|
|
|
@@ -371,18 +366,17 @@ get '/*' ->
|
|
|
371
366
|
headers =
|
|
372
367
|
'Content-Type': contentType
|
|
373
368
|
'Cache-Control': response.headers.get('Cache-Control') or 'public, max-age=3600'
|
|
369
|
+
# COOP/COEP required for SharedArrayBuffer (DuckDB WASM fallback)
|
|
370
|
+
'Cross-Origin-Opener-Policy': 'same-origin'
|
|
371
|
+
'Cross-Origin-Embedder-Policy': 'credentialless'
|
|
374
372
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
headers
|
|
378
|
-
headers['X-DuckDB-Platform'] = 'rip-db'
|
|
379
|
-
headers['X-DuckDB-UI-Extension-Version'] = '139-944c08a214'
|
|
380
|
-
|
|
381
|
-
new Response body, { status: response.status, headers }
|
|
373
|
+
new Response body,
|
|
374
|
+
status: response.status
|
|
375
|
+
headers: headers
|
|
382
376
|
|
|
383
377
|
catch err
|
|
384
|
-
console.error "Proxy error for #{
|
|
385
|
-
new Response "Failed to fetch UI asset: #{
|
|
378
|
+
console.error "Proxy error for #{reqPath}:", err?.message
|
|
379
|
+
new Response "Failed to fetch UI asset: #{reqPath}", { status: 502 }
|
|
386
380
|
|
|
387
381
|
# ==============================================================================
|
|
388
382
|
# Start Server
|