@rip-lang/server 1.3.126 → 1.4.1

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 (69) hide show
  1. package/{docs/READ_VALIDATORS.md → API.md} +41 -119
  2. package/CONFIG.md +408 -0
  3. package/README.md +246 -1109
  4. package/acme/crypto.rip +0 -2
  5. package/browse.rip +62 -0
  6. package/control/cli.rip +89 -34
  7. package/control/lifecycle.rip +67 -1
  8. package/control/manager.rip +250 -0
  9. package/control/mdns.rip +3 -0
  10. package/middleware.rip +1 -1
  11. package/package.json +14 -11
  12. package/server.rip +189 -673
  13. package/serving/config.rip +766 -0
  14. package/{edge → serving}/forwarding.rip +2 -2
  15. package/serving/logging.rip +101 -0
  16. package/{edge → serving}/metrics.rip +29 -1
  17. package/serving/proxy.rip +99 -0
  18. package/{edge → serving}/queue.rip +1 -1
  19. package/{edge → serving}/ratelimit.rip +1 -1
  20. package/{edge → serving}/realtime.rip +71 -2
  21. package/{edge → serving}/registry.rip +1 -1
  22. package/{edge → serving}/router.rip +3 -3
  23. package/{edge → serving}/runtime.rip +18 -16
  24. package/{edge → serving}/security.rip +1 -1
  25. package/serving/static.rip +393 -0
  26. package/{edge → serving}/tls.rip +3 -7
  27. package/{edge → serving}/upstream.rip +4 -4
  28. package/{edge → serving}/verify.rip +16 -16
  29. package/streams/{tls_clienthello.rip → clienthello.rip} +1 -1
  30. package/streams/config.rip +8 -8
  31. package/streams/index.rip +5 -5
  32. package/streams/router.rip +2 -2
  33. package/tests/acme.rip +1 -1
  34. package/tests/config.rip +215 -0
  35. package/tests/control.rip +1 -1
  36. package/tests/{runtime_entrypoints.rip → entrypoints.rip} +11 -7
  37. package/tests/extracted.rip +118 -0
  38. package/tests/helpers.rip +4 -4
  39. package/tests/metrics.rip +3 -3
  40. package/tests/proxy.rip +9 -8
  41. package/tests/read.rip +1 -1
  42. package/tests/realtime.rip +3 -3
  43. package/tests/registry.rip +4 -4
  44. package/tests/router.rip +27 -27
  45. package/tests/runner.rip +70 -0
  46. package/tests/security.rip +4 -4
  47. package/tests/servers.rip +102 -136
  48. package/tests/static.rip +2 -2
  49. package/tests/streams_clienthello.rip +2 -2
  50. package/tests/streams_index.rip +4 -4
  51. package/tests/streams_pipe.rip +1 -1
  52. package/tests/streams_router.rip +10 -10
  53. package/tests/streams_runtime.rip +4 -4
  54. package/tests/streams_upstream.rip +1 -1
  55. package/tests/upstream.rip +2 -2
  56. package/tests/verify.rip +18 -18
  57. package/tests/watchers.rip +4 -4
  58. package/default.rip +0 -435
  59. package/docs/edge/CONFIG_LIFECYCLE.md +0 -111
  60. package/docs/edge/CONTRACTS.md +0 -137
  61. package/docs/edge/EDGEFILE_CONTRACT.md +0 -282
  62. package/docs/edge/M0B_REVIEW_NOTES.md +0 -102
  63. package/docs/edge/SCHEDULER.md +0 -46
  64. package/docs/logo.png +0 -0
  65. package/docs/logo.svg +0 -13
  66. package/docs/social.png +0 -0
  67. package/edge/config.rip +0 -607
  68. package/edge/static.rip +0 -69
  69. package/tests/edgefile.rip +0 -165
package/server.rip CHANGED
@@ -1,9 +1,9 @@
1
1
  # ==============================================================================
2
- # @rip-lang/server — Pure Rip Application Server
2
+ # @rip-lang/server — Rip Server serves content
3
3
  # ==============================================================================
4
4
  #
5
- # A multi-worker application server written entirely in Rip.
6
- # Provides hot reloading, HTTPS, mDNS, and production-grade features.
5
+ # One Bun-native runtime to serve static sites, apps, proxied HTTP services,
6
+ # and TCP/TLS services. Written entirely in Rip.
7
7
  #
8
8
  # Usage:
9
9
  # bun server.rip <app-path> # Start server
@@ -12,405 +12,45 @@
12
12
  # bun server.rip list # List registered hosts
13
13
  # ==============================================================================
14
14
 
15
- import { existsSync, statSync, readFileSync, writeFileSync, unlinkSync, watch, utimesSync } from 'node:fs'
16
- import { basename, dirname, isAbsolute, join, resolve } from 'node:path'
15
+ import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs'
16
+ import { dirname, join } from 'node:path'
17
17
  import { networkInterfaces } from 'node:os'
18
- import { isCurrentVersion as schedulerIsCurrentVersion, getNextAvailableSocket as schedulerGetNextAvailableSocket, releaseWorker as schedulerReleaseWorker, shouldRetryBodylessBusy } from './edge/queue.rip'
19
- import { formatPort, maybeAddSecurityHeaders as edgeMaybeAddSecurityHeaders, buildStatusBody as edgeBuildStatusBody, buildRipdevUrl as edgeBuildRipdevUrl, generateRequestId } from './edge/forwarding.rip'
20
- import { loadTlsMaterial as edgeLoadTlsMaterial, printCertSummary as edgePrintCertSummary } from './edge/tls.rip'
21
- import { createAppRegistry, registerApp, resolveHost, getAppState, removeApp } from './edge/registry.rip'
22
- import { watchUnavailableResponse, serverBusyResponse, serviceUnavailableResponse, gatewayTimeoutResponse, queueTimeoutResponse, buildUpstreamResponse, forwardOnceWithTimeout, createWsPassthrough } from './edge/forwarding.rip'
23
- import { processQueuedJob, drainQueueOnce } from './edge/queue.rip'
24
- import { getControlSocketPath, getPidFilePath } from './control/control.rip'
25
- import { handleWorkerControl, handleWatchControl, handleRegistryControl, handleReloadControl } from './control/control.rip'
26
- import { getLanIP as controlGetLanIP, startMdnsAdvertisement as controlStartMdnsAdvertisement, stopMdnsAdvertisements as controlStopMdnsAdvertisements } from './control/mdns.rip'
27
- import { registerWatchGroup, handleWatchGroup, broadcastWatchChange, registerAppWatchDirs, shouldTriggerCodeReload } from './control/watchers.rip'
28
- import { computeHideUrls, logStartupSummary, createCleanup, installShutdownHandlers } from './control/lifecycle.rip'
29
- import { runVersionOutput, runHelpOutput, stopServer, runReloadSubcommand, resolveAppEntry, parseFlags, coerceInt } from './control/cli.rip'
30
- import { runSetupMode, runWorkerMode } from './control/worker.rip'
31
- import { spawnTrackedWorker, postWorkerQuit, waitForWorkerReady } from './control/workers.rip'
32
- import { createChallengeStore, handleChallengeRequest } from './acme/store.rip'
33
- import { createAcmeManager } from './acme/manager.rip'
34
- import { loadCert as acmeLoadCert, defaultCertDir } from './acme/store.rip'
35
- import { createMetrics } from './edge/metrics.rip'
36
- import { setEventJsonMode, logEvent } from './control/lifecycle.rip'
37
- import { createHub, generateClientId, addClient, removeClient, processResponse, handlePublish, getRealtimeStats } from './edge/realtime.rip'
38
- import { findConfigFile, findEdgeFile, resolveConfigSource, applyConfig, applyEdgeConfig, formatConfigErrors } from './edge/config.rip'
39
- import { createEdgeRuntime, createReloadHistoryEntry, restoreRegistrySnapshot, configNote, toUpstreamWsUrl, loadRuntimeConfig, runCheckConfig } from './edge/runtime.rip'
40
- import { createRateLimiter, rateLimitResponse } from './edge/ratelimit.rip'
41
- import { validateRequest } from './edge/security.rip'
42
- import { createUpstreamPool, addUpstream, getUpstream, listUpstreams, selectTarget, markTargetBusy, releaseTarget, shouldRetry as shouldRetryUpstream, computeRetryDelayMs, startHealthChecks, stopHealthChecks, checkTargetHealth } from './edge/upstream.rip'
43
- import { compileRouteTable, matchRoute, describeRoute } from './edge/router.rip'
44
- import { buildVerificationResult, verifyRouteRuntime } from './edge/verify.rip'
45
- import { serveStaticRoute, buildRedirectResponse } from './edge/static.rip'
46
- import { buildTlsArray } from './edge/tls.rip'
47
- import { buildStreamRuntime, startStreamListeners, stopStreamListeners, streamDiagnostics } from './streams/index.rip'
48
- import { createStreamRuntime, streamUsesListenPort } from './streams/runtime.rip'
49
-
50
- # Match capture holder for Rip's =~
51
- _ = null
52
-
53
- # ==============================================================================
54
- # Constants
55
- # ==============================================================================
56
-
57
- MAX_BACKOFF_MS = 30000 # Max delay between worker restart attempts
58
- MAX_RESTART_COUNT = 10 # Max consecutive worker crashes before giving up
59
- STABILITY_THRESHOLD_MS = 60000 # Worker uptime before restart count resets
60
-
61
- # ==============================================================================
62
- # Utilities
63
- # ==============================================================================
64
-
65
- nowMs = -> Date.now()
66
-
67
- _envOverride = null
68
- _debugMode = false
69
-
70
- isDev = ->
71
- env = (_envOverride or process.env.NODE_ENV or '').toLowerCase()
72
- env in ['development', 'dev', '']
73
-
74
- isDebug = -> _debugMode or process.env.RIP_DEBUG?
75
-
76
- applyFlagSideEffects = (flags) ->
77
- _envOverride = flags.envOverride if flags.envOverride
78
- _debugMode = flags.debug is true
79
-
80
- formatTimestamp = ->
81
- now = new Date()
82
- pad = (n, w = 2) -> String(n).padStart(w, '0')
83
- timestamp = "#{now.getFullYear()}-#{pad(now.getMonth() + 1)}-#{pad(now.getDate())} #{pad(now.getHours())}:#{pad(now.getMinutes())}:#{pad(now.getSeconds())}.#{String(now.getMilliseconds()).padStart(3, '0')}"
84
- tzMin = now.getTimezoneOffset()
85
- tzSign = if tzMin <= 0 then '+' else '-'
86
- tzAbs = Math.abs(tzMin)
87
- timezone = "#{tzSign}#{String(Math.floor(tzAbs / 60)).padStart(2, '0')}#{String(tzAbs % 60).padStart(2, '0')}"
88
- { timestamp, timezone }
89
-
90
- scale = (value, unit, pad = true) ->
91
- if value > 0 and Number.isFinite(value)
92
- span = ['T', 'G', 'M', 'k', (if pad then ' ' else ''), 'm', 'µ', 'n', 'p']
93
- base = 4
94
- minSlot = 0
95
- maxSlot = span.length - 1
96
- slot = base
97
-
98
- while value < 0.995 and slot <= maxSlot # use 0.05 to try to keep units
99
- value *= 1000
100
- slot++
101
- while value >= 999.5 and slot >= minSlot
102
- value /= 1000
103
- slot--
104
-
105
- if slot >= minSlot and slot <= maxSlot
106
- tens = Math.round(value * 10) / 10
107
- if tens >= 99.5
108
- nums = Math.round(value).toString()
109
- else if tens >= 10
110
- nums = Math.round(value).toString()
111
- else
112
- nums = tens.toFixed(1)
113
- nums = nums.padStart(3, ' ') if pad
114
- return "#{nums}#{span[slot]}#{unit}"
115
-
116
- return (if pad then ' 0 ' else '0') + unit if value is 0
117
- '???' + (if pad then ' ' else '') + unit
118
-
119
- logAccessJson = (app, req, res, totalSeconds, workerSeconds) ->
120
- url = new URL(req.url)
121
- len = res.headers.get('content-length')
122
- type = (res.headers.get('content-type') or '').split(';')[0] or undefined
123
- p JSON.stringify
124
- t: new Date().toISOString()
125
- app: app
126
- method: req.method or 'GET'
127
- path: url.pathname
128
- status: res.status
129
- totalSeconds: totalSeconds
130
- workerSeconds: workerSeconds
131
- type: type
132
- length: if len then Number(len) else undefined
133
-
134
- typeAbbrev =
135
- html: 'html', css: 'css', javascript: 'js', json: 'json', plain: 'text'
136
- png: 'png', jpeg: 'jpg', gif: 'gif', webp: 'webp', svg: 'svg'
137
- 'svg+xml': 'svg', 'x-icon': 'ico', 'octet-stream': 'bin', 'x-rip': 'rip'
138
-
139
- logAccessHuman = (app, req, res, totalSeconds, workerSeconds) ->
140
- { timestamp, timezone } = formatTimestamp()
141
- dur = scale(totalSeconds, 's')
142
- method = req.method or 'GET'
143
- url = new URL(req.url)
144
- path = url.pathname
145
- status = res.status
146
- bytes = Number(res.headers.get('content-length') or 0)
147
- size = scale(bytes, 'B')
148
- contentType = (res.headers.get('content-type') or '').split(';')[0] or ''
149
- sub = if contentType.includes('/') then contentType.split('/')[1] else contentType
150
- type = (typeAbbrev[sub] or sub or '').padEnd(4)
151
- p "#{timestamp} #{timezone} #{dur} │ #{status} #{type} #{size} │ #{method} #{path}"
152
-
153
- INTERNAL_HEADERS = new Set(['rip-worker-busy', 'rip-worker-id', 'rip-no-log'])
154
-
155
- stripInternalHeaders = (h) ->
156
- out = new Headers()
157
- for [k, v] as h.entries()
158
- continue if INTERNAL_HEADERS.has(k.toLowerCase())
159
- out.append(k, v)
160
- out
161
-
162
- defaultEntry = join(import.meta.dir, 'default.rip')
163
-
164
- # Edge runtime lifecycle helpers are in edge/runtime.rip
165
-
166
- # ==============================================================================
167
- # Worker Mode
168
- # ==============================================================================
169
-
170
- # ==============================================================================
171
- # Manager Class
172
- # ==============================================================================
173
-
174
- class Manager
175
- constructor: (@flags) ->
176
- @appWorkers = new Map()
177
- @shuttingDown = false
178
- @lastCheck = 0
179
- @currentMtimes = new Map()
180
- @rollingApps = new Set()
181
- @lastRollAt = new Map()
182
- @nextWorkerId = -1
183
- @retiringIds = new Set()
184
- @restartBudgets = new Map() # slotIndex -> { count, backoffMs }
185
- @deferredDeaths = new Set() # worker IDs that died during rolling restart
186
- @currentVersion = 1
187
- @server = null
188
- @dbUrl = null
189
- @appWatchers = new Map()
190
- @codeWatchers = new Map()
191
- @defaultAppId = @flags.appName
192
-
193
- getWorkers: (appId) ->
194
- appId = appId or @defaultAppId
195
- unless @appWorkers.has(appId)
196
- @appWorkers.set(appId, [])
197
- @appWorkers.get(appId)
198
-
199
- getAppState: (appId) ->
200
- @server?.appRegistry?.apps?.get(appId) or null
201
-
202
- allAppStates: ->
203
- return [] unless @server?.appRegistry?.apps
204
- Array.from(@server.appRegistry.apps.values())
205
-
206
- runSetupForApp: (app) ->
207
- setupFile = join(app.config.appBaseDir or dirname(app.config.entry or @flags.appEntry), 'setup.rip')
208
- return unless existsSync(setupFile)
209
-
210
- dbUrl = process.env.DB_URL or 'http://localhost:4213'
211
- dbAlreadyRunning = try
212
- fetch! dbUrl + '/health'
213
- true
214
- catch
215
- false
216
- @dbUrl = dbUrl unless dbAlreadyRunning
217
-
218
- setupEnv = Object.assign {}, process.env, app.config.env or {},
219
- RIP_SETUP_MODE: '1'
220
- RIP_SETUP_FILE: setupFile
221
- APP_ID: String(app.appId)
222
- APP_ENTRY: app.config.entry or @flags.appEntry
223
- APP_BASE_DIR: app.config.appBaseDir or @flags.appBaseDir
224
- proc = Bun.spawn ['rip', import.meta.path],
225
- stdout: 'inherit'
226
- stderr: 'inherit'
227
- stdin: 'ignore'
228
- cwd: process.cwd()
229
- env: setupEnv
230
-
231
- code = await proc.exited
232
- if code isnt 0
233
- console.error "rip-server: setup exited with code #{code} for app #{app.appId}"
234
- exit 1
235
-
236
- start: ->
237
- @stop!
238
-
239
- for app in @allAppStates()
240
- @runSetupForApp!(app) if app.config.entry
241
-
242
- for app in @allAppStates()
243
- workers = @getWorkers(app.appId)
244
- workers.length = 0
245
- desiredWorkers = app.config.workers or @flags.workers
246
- for i in [0...desiredWorkers]
247
- w = @spawnWorker!(app.appId, @currentVersion)
248
- workers.push(w)
249
-
250
- if @flags.reload
251
- for app in @allAppStates()
252
- @currentMtimes.set(app.appId, @getEntryMtime(app.appId))
253
-
254
- interval = setInterval =>
255
- return clearInterval(interval) if @shuttingDown
256
- now = Date.now()
257
- return if now - @lastCheck < 100
258
- @lastCheck = now
259
- for app in @allAppStates()
260
- appId = app.appId
261
- mt = @getEntryMtime(appId)
262
- previous = @currentMtimes.get(appId) or 0
263
- if mt > previous
264
- continue if @rollingApps.has(appId)
265
- continue if now - (@lastRollAt.get(appId) or 0) < 200
266
- @currentMtimes.set(appId, mt)
267
- @rollingApps.add(appId)
268
- @lastRollAt.set(appId, now)
269
- @rollingRestart(appId).finally => @rollingApps.delete(appId)
270
- , 50
271
-
272
- # Watch files in each app directory (on by default, --static disables)
273
- if @flags.watch
274
- for app in @allAppStates()
275
- continue unless app.config.entry and app.config.appBaseDir
276
- entryFile = app.config.entry
277
- entryBase = basename(entryFile)
278
- try
279
- watcher = watch app.config.appBaseDir, { recursive: true }, (event, filename) =>
280
- return unless shouldTriggerCodeReload(filename, @flags.watch, entryBase)
281
- try
282
- now = new Date()
283
- utimesSync(entryFile, now, now)
284
- catch
285
- null
286
- @codeWatchers.set(app.appId, watcher)
287
- catch e
288
- warn "rip-server: directory watch failed for #{app.appId}: #{e.message}"
289
-
290
- stop: ->
291
- for [appId, workers] as @appWorkers
292
- for w in workers
293
- try w.process.kill()
294
- try unlinkSync(w.socketPath)
295
- @appWorkers.clear()
296
-
297
- # Close all FS watcher handles
298
- for [prefix, entry] as @appWatchers
299
- clearTimeout(entry.timer) if entry.timer
300
- for watcher in (entry.watchers or [])
301
- try watcher.close()
302
- @appWatchers.clear()
303
- for [appId, watcher] as @codeWatchers
304
- try watcher.close()
305
- @codeWatchers.clear()
306
-
307
- watchDirs: (prefix, dirs) ->
308
- registerAppWatchDirs(@appWatchers, prefix, dirs, @server, (-> process.cwd()))
309
-
310
- spawnWorker: (appId, version) ->
311
- workerId = ++@nextWorkerId
312
- app = @getAppState(appId) or { appId: appId, config: {} }
313
- tracked = spawnTrackedWorker(@flags, workerId, (version or @currentVersion), nowMs, import.meta.path, appId, app.config)
314
- @monitor(tracked)
315
- tracked
316
-
317
- monitor: (w) ->
318
- # Watch for process exit in background (don't block)
319
- w.process.exited.then =>
320
- return if @shuttingDown
321
- return if @retiringIds.has(w.id)
322
- if @rollingApps.has(w.appId)
323
- @deferredDeaths.add(w.id)
324
- return
325
18
 
326
- # Notify server to remove dead worker's socket entry (fire-and-forget)
327
- postWorkerQuit(@flags.socketPrefix, w.id)
19
+ import { nowMs, isDebug, applyFlagSideEffects, logAccessJson, logAccessHuman, stripInternalHeaders } from './serving/logging.rip'
20
+ import { applyConfig, formatConfigErrors } from './serving/config.rip'
21
+ import { createAppRegistry, registerApp, resolveHost, getAppState, removeApp } from './serving/registry.rip'
22
+ import { compileRouteTable, matchRoute, describeRoute } from './serving/router.rip'
23
+ import { serveStaticRoute, buildRedirectResponse } from './serving/static.rip'
24
+ import { createUpstreamPool, addUpstream, getUpstream, releaseTarget, startHealthChecks, stopHealthChecks, checkTargetHealth } from './serving/upstream.rip'
25
+ import { formatPort, maybeAddSecurityHeaders as addSecurityHeaders, buildStatusBody, buildRipdevUrl, generateRequestId, watchUnavailableResponse, serverBusyResponse, serviceUnavailableResponse, gatewayTimeoutResponse, queueTimeoutResponse, buildUpstreamResponse, forwardOnceWithTimeout, createWsPassthrough } from './serving/forwarding.rip'
26
+ import { proxyRouteToUpstream as proxyRouteToUpstreamFn, upgradeProxyWebSocket as upgradeProxyWebSocketFn } from './serving/proxy.rip'
27
+ import { isCurrentVersion as schedulerIsCurrentVersion, getNextAvailableSocket as schedulerGetNextAvailableSocket, releaseWorker as schedulerReleaseWorker, shouldRetryBodylessBusy, processQueuedJob, drainQueueOnce } from './serving/queue.rip'
28
+ import { createServingRuntime, createReloadHistoryEntry, restoreRegistrySnapshot, configNote, loadRuntimeConfig, runCheckConfig } from './serving/runtime.rip'
29
+ import { buildVerificationResult, verifyRouteRuntime } from './serving/verify.rip'
30
+ import { loadTlsMaterial as loadTls, printCertSummary, buildTlsArray } from './serving/tls.rip'
31
+ import { createMetrics, buildDiagnosticsBody } from './serving/metrics.rip'
32
+ import { createRateLimiter, rateLimitResponse } from './serving/ratelimit.rip'
33
+ import { validateRequest } from './serving/security.rip'
34
+ import { createHub, generateClientId, handlePublish, getRealtimeStats, proxyRealtimeToWorker as realtimeProxyToWorker, buildWebSocketHandlers as buildWsHandlers } from './serving/realtime.rip'
328
35
 
329
- # Track restart budget by slot (survives worker replacement)
330
- workers = @getWorkers(w.appId)
331
- slotIdx = workers.findIndex((x) -> x.id is w.id)
332
- return if slotIdx < 0
333
-
334
- budget = @restartBudgets.get(slotIdx) or { count: 0, backoffMs: 1000 }
335
-
336
- # Reset budget if worker ran long enough to be considered stable
337
- if nowMs() - w.startedAt > STABILITY_THRESHOLD_MS
338
- budget.count = 0
339
- budget.backoffMs = 1000
340
-
341
- budget.count++
342
- budget.backoffMs = Math.min(budget.backoffMs * 2, MAX_BACKOFF_MS)
343
- @restartBudgets.set(slotIdx, budget)
344
-
345
- if budget.count > MAX_RESTART_COUNT
346
- logEvent('worker_abandon', { workerId: w.id, slot: slotIdx, restarts: budget.count })
347
- return
348
- @server?.metrics and @server.metrics.workerRestarts++
349
- logEvent('worker_restart', { workerId: w.id, slot: slotIdx, attempt: budget.count, backoffMs: budget.backoffMs })
350
- setTimeout =>
351
- workers[slotIdx] = @spawnWorker(w.appId, @currentVersion) if slotIdx < workers.length
352
- , budget.backoffMs
353
-
354
- waitWorkerReady: (socketPath, timeoutMs = 5000) ->
355
- waitForWorkerReady(socketPath, timeoutMs)
356
-
357
- rollingRestart: (appId = null) ->
358
- apps = if appId then [@getAppState(appId)].filter(Boolean) else @allAppStates()
359
- nextVersion = @currentVersion + 1
360
-
361
- for app in apps
362
- workers = @getWorkers(app.appId)
363
- olds = [...workers]
364
- pairs = []
365
-
366
- for oldWorker in olds
367
- replacement = @spawnWorker!(app.appId, nextVersion)
368
- workers.push(replacement)
369
- pairs.push({ old: oldWorker, replacement })
370
-
371
- readyResults = Promise.all!(pairs.map((p) => @waitWorkerReady(p.replacement.socketPath, 3000)))
372
- allReady = readyResults.every((ready) -> ready)
373
- unless allReady
374
- console.error "[manager] Rolling restart aborted: not all new workers ready for app #{app.appId}"
375
- for pair, i in pairs
376
- unless readyResults[i]
377
- try pair.replacement.process.kill()
378
- idx = workers.indexOf(pair.replacement)
379
- workers.splice(idx, 1) if idx >= 0
380
- if @deferredDeaths.size > 0
381
- for deadId as @deferredDeaths
382
- idx = workers.findIndex((x) -> x.id is deadId)
383
- workers[idx] = @spawnWorker(app.appId, @currentVersion) if idx >= 0
384
- @deferredDeaths.clear()
385
- return
386
-
387
- @currentVersion = nextVersion
388
-
389
- for { old } in pairs
390
- @retiringIds.add(old.id)
391
- try old.process.kill()
36
+ import { buildStreamRuntime, startStreamListeners, stopStreamListeners, streamDiagnostics } from './streams/index.rip'
37
+ import { createStreamRuntime, streamUsesListenPort } from './streams/runtime.rip'
392
38
 
393
- Promise.all!(pairs.map(({ old }) -> old.process.exited.catch(-> null)))
39
+ import { getControlSocketPath, getPidFilePath, handleWorkerControl, handleWatchControl, handleRegistryControl, handleReloadControl } from './control/control.rip'
40
+ import { getLanIP as controlGetLanIP, startMdnsAdvertisement as controlStartMdnsAdvertisement, stopMdnsAdvertisements as controlStopMdnsAdvertisements } from './control/mdns.rip'
41
+ import { registerWatchGroup, handleWatchGroup, broadcastWatchChange } from './control/watchers.rip'
42
+ import { computeHideUrls, logStartupSummary, createCleanup, installShutdownHandlers, setEventJsonMode, logEvent } from './control/lifecycle.rip'
43
+ import { runVersionOutput, runHelpOutput, stopServer, reloadConfig, listApps, showInfo, parseFlags } from './control/cli.rip'
44
+ import { runSetupMode, runWorkerMode } from './control/worker.rip'
45
+ import { Manager } from './control/manager.rip'
394
46
 
395
- for { old } in pairs
396
- postWorkerQuit(@flags.socketPrefix, old.id)
47
+ import { createChallengeStore, handleChallengeRequest, loadCert as acmeLoadCert, defaultCertDir } from './acme/store.rip'
48
+ import { createAcmeManager } from './acme/manager.rip'
397
49
 
398
- retiring = new Set(pairs.map((p) -> p.old.id))
399
- filtered = workers.filter((w) -> not retiring.has(w.id))
400
- workers.length = 0
401
- workers.push(...filtered)
402
- @retiringIds.delete(id) for id as retiring
50
+ _ = null
403
51
 
404
- if @deferredDeaths.size > 0
405
- for deadId as @deferredDeaths
406
- idx = workers.findIndex((x) -> x.id is deadId)
407
- workers[idx] = @spawnWorker(app.appId, @currentVersion) if idx >= 0
408
- @deferredDeaths.clear()
52
+ defaultEntry = join(import.meta.dir, 'browse.rip')
409
53
 
410
- getEntryMtime: (appId = null) ->
411
- app = @getAppState(appId or @defaultAppId)
412
- entry = app?.config?.entry or @flags.appEntry
413
- try statSync(entry).mtimeMs catch then 0
414
54
 
415
55
  # ==============================================================================
416
56
  # Server Class
@@ -441,12 +81,12 @@ class Server
441
81
  @watchGroups = new Map()
442
82
  @manager = null
443
83
  @configuredAppIds = new Set()
444
- @edgeRuntime = createEdgeRuntime()
445
- @retiredEdgeRuntimes = []
84
+ @servingRuntime = createServingRuntime()
85
+ @retiredServingRuntimes = []
446
86
  @streamRuntime = createStreamRuntime()
447
87
  @retiredStreamRuntimes = []
448
88
  @reloadAttemptSeq = 0
449
- @configInfo = @edgeRuntime.configInfo
89
+ @configInfo = @servingRuntime.configInfo
450
90
  @configInfo.reloadHistory = []
451
91
  @configInfo.lastReload = null
452
92
  try
@@ -482,21 +122,21 @@ class Server
482
122
  removeApp(@appRegistry, appId)
483
123
  @configuredAppIds.clear()
484
124
 
485
- retainEdgeRuntime: (runtime) ->
125
+ retainRuntime: (runtime) ->
486
126
  runtime.inflight++
487
127
  runtime
488
128
 
489
- releaseEdgeRuntime: (runtime) ->
129
+ releaseRuntime: (runtime) ->
490
130
  runtime.inflight = Math.max(0, runtime.inflight - 1)
491
- @cleanupRetiredEdgeRuntimes()
131
+ @cleanupRetiredServingRuntimes()
492
132
 
493
- retainEdgeRuntimeWs: (runtime) ->
133
+ retainRuntimeWs: (runtime) ->
494
134
  runtime.wsConnections++
495
135
  runtime
496
136
 
497
- releaseEdgeRuntimeWs: (runtime) ->
137
+ releaseRuntimeWs: (runtime) ->
498
138
  runtime.wsConnections = Math.max(0, runtime.wsConnections - 1)
499
- @cleanupRetiredEdgeRuntimes()
139
+ @cleanupRetiredServingRuntimes()
500
140
 
501
141
  retainStreamRuntime: (runtime) ->
502
142
  runtime.inflight++
@@ -506,14 +146,14 @@ class Server
506
146
  runtime.inflight = Math.max(0, runtime.inflight - 1)
507
147
  @cleanupRetiredStreamRuntimes()
508
148
 
509
- cleanupRetiredEdgeRuntimes: ->
149
+ cleanupRetiredServingRuntimes: ->
510
150
  keep = []
511
- for runtime in @retiredEdgeRuntimes
151
+ for runtime in @retiredServingRuntimes
512
152
  if runtime.inflight > 0 or runtime.wsConnections > 0
513
153
  keep.push(runtime)
514
154
  else
515
155
  stopHealthChecks(runtime.upstreamPool)
516
- @retiredEdgeRuntimes = keep
156
+ @retiredServingRuntimes = keep
517
157
 
518
158
  cleanupRetiredStreamRuntimes: ->
519
159
  keep = []
@@ -531,18 +171,18 @@ class Server
531
171
  lastReload: entry
532
172
  reloadHistory: history
533
173
  )
534
- @edgeRuntime.configInfo = @configInfo if @edgeRuntime?.configInfo
174
+ @servingRuntime.configInfo = @configInfo if @servingRuntime?.configInfo
535
175
 
536
176
  nextReloadAttemptId: ->
537
177
  @reloadAttemptSeq++
538
178
  "reload-#{@reloadAttemptSeq}"
539
179
 
540
- verifyEdgeRuntime: (runtime, loaded) ->
541
- return buildVerificationResult(true) unless runtime?.configInfo?.kind is 'edge'
180
+ verifyRuntime: (runtime, loaded) ->
181
+ return buildVerificationResult(true) unless runtime?.configInfo?.kind in ['serve', 'edge']
542
182
  verifyRouteRuntime(runtime, @appRegistry, @defaultAppId, getUpstream, checkTargetHealth, getAppState, runtime.verifyPolicy or runtime.configInfo.verifyPolicy or {})
543
183
 
544
- buildEdgeRuntime: (loaded) ->
545
- return createEdgeRuntime() unless loaded?.source?.kind is 'edge'
184
+ buildRuntime: (loaded) ->
185
+ return createServingRuntime() unless loaded?.source?.kind in ['serve', 'edge']
546
186
  upstreamPool = createUpstreamPool()
547
187
  for upstreamId, upstreamConfig of (loaded.normalized.upstreams or {})
548
188
  addUpstream(upstreamPool, upstreamId, upstreamConfig)
@@ -559,17 +199,19 @@ class Server
559
199
  lastErrorCode: null
560
200
  lastErrorDetails: null
561
201
  rolledBackFrom: null
562
- verifyPolicy: loaded.normalized.edge?.verify or null
563
- certs: loaded.normalized.certs or null
202
+ verifyPolicy: loaded.normalized.server?.verify or null
203
+ acmeDomains: loaded.normalized.server?.acmeDomains or []
204
+ certDir: loaded.normalized.server?.certDir or null
205
+ certs: loaded.normalized.resolvedCerts or null
564
206
  activeRouteDescriptions: (routeTable.routes or []).map(describeRoute).filter(Boolean)
565
207
  lastReload: @configInfo?.lastReload or null
566
208
  reloadHistory: @configInfo?.reloadHistory or []
567
- runtime = createEdgeRuntime(configInfo, upstreamPool, routeTable)
209
+ runtime = createServingRuntime(configInfo, upstreamPool, routeTable)
568
210
  startHealthChecks(runtime.upstreamPool)
569
211
  runtime
570
212
 
571
213
  buildStreamRuntimeForConfig: (loaded) ->
572
- return createStreamRuntime() unless loaded?.source?.kind is 'edge'
214
+ return createStreamRuntime() unless loaded?.source?.kind in ['serve', 'edge']
573
215
  buildStreamRuntime(loaded.normalized)
574
216
 
575
217
  startAppServerOnPort: (p, opts = {}) ->
@@ -650,15 +292,15 @@ class Server
650
292
  startStreamListeners(@streamRuntime, @buildStreamListenerOptions())
651
293
  @streamListenersDeferred = false
652
294
 
653
- activateEdgeRuntime: (runtime) ->
295
+ activateRuntime: (runtime) ->
654
296
  return unless runtime
655
- oldRuntime = @edgeRuntime
656
- @edgeRuntime = runtime
297
+ oldRuntime = @servingRuntime
298
+ @servingRuntime = runtime
657
299
  @configInfo = runtime.configInfo
658
300
  if oldRuntime and oldRuntime isnt runtime
659
301
  oldRuntime.retiredAt = new Date().toISOString()
660
- @retiredEdgeRuntimes.push(oldRuntime)
661
- @cleanupRetiredEdgeRuntimes()
302
+ @retiredServingRuntimes.push(oldRuntime)
303
+ @cleanupRetiredServingRuntimes()
662
304
  oldRuntime
663
305
 
664
306
  activateStreamRuntime: (runtime, options = {}) ->
@@ -687,15 +329,15 @@ class Server
687
329
  @cleanupRetiredStreamRuntimes()
688
330
  oldRuntime
689
331
 
690
- rollbackEdgeRuntime: (oldRuntime, failedRuntime, snapshot, verification) ->
332
+ rollbackRuntime: (oldRuntime, failedRuntime, snapshot, verification) ->
691
333
  restoreRegistrySnapshot(@appRegistry, snapshot)
692
334
  @configuredAppIds = new Set(snapshot.configured)
693
335
 
694
336
  stopHealthChecks(failedRuntime.upstreamPool)
695
- @retiredEdgeRuntimes = @retiredEdgeRuntimes.filter((runtime) -> runtime isnt oldRuntime and runtime isnt failedRuntime)
337
+ @retiredServingRuntimes = @retiredServingRuntimes.filter((runtime) -> runtime isnt oldRuntime and runtime isnt failedRuntime)
696
338
  if oldRuntime
697
339
  oldRuntime.retiredAt = null
698
- @edgeRuntime = oldRuntime
340
+ @servingRuntime = oldRuntime
699
341
  @configInfo = Object.assign({}, oldRuntime.configInfo,
700
342
  lastResult: 'rolled_back'
701
343
  lastError: verification.message
@@ -711,8 +353,8 @@ class Server
711
353
  lastErrorCode: verification.code
712
354
  lastErrorDetails: verification.details or null
713
355
  )
714
- @retiredEdgeRuntimes.push(failedRuntime)
715
- @cleanupRetiredEdgeRuntimes()
356
+ @retiredServingRuntimes.push(failedRuntime)
357
+ @cleanupRetiredServingRuntimes()
716
358
 
717
359
  rollbackStreamRuntime: (oldRuntime, failedRuntime) ->
718
360
  stopStreamListeners(failedRuntime, true)
@@ -740,10 +382,8 @@ class Server
740
382
  details = if applied then null else (@configInfo?.lastErrorDetails or null)
741
383
  entry = createReloadHistoryEntry(attemptId, source, oldVersion, newVersion, result, reason, code, details)
742
384
  @appendReloadHistory(entry)
743
- if loaded and not flags.quiet
744
- label = if loaded.source.kind is 'edge' then 'Edgefile.rip' else 'config.rip'
745
- p "rip-server: loaded #{label} with #{loaded.summary.counts.apps} app(s), #{loaded.summary.counts.upstreams} upstream(s), #{loaded.summary.counts.routes} route(s)"
746
- p "rip-server: note: #{@configInfo.note}" if @configInfo.note
385
+ if loaded and not flags.quiet and source isnt 'startup'
386
+ p "rip-server: loaded serve.rip with #{loaded.summary.counts.apps} app(s), #{loaded.summary.counts.proxies} proxy(s), #{loaded.summary.counts.routes} route(s)"
747
387
  logEvent('config_loaded',
748
388
  id: attemptId
749
389
  source: source
@@ -755,10 +395,10 @@ class Server
755
395
  reason: reason
756
396
  code: code
757
397
  apps: @configInfo.counts.apps
758
- upstreams: @configInfo.counts.upstreams
398
+ proxies: @configInfo.counts.proxies
759
399
  routes: @configInfo.counts.routes
760
- sites: @configInfo.counts.sites
761
- ) if loaded or source isnt 'startup'
400
+ hosts: @configInfo.counts.hosts
401
+ ) if source isnt 'startup'
762
402
  {
763
403
  ok: applied
764
404
  id: attemptId
@@ -781,7 +421,7 @@ class Server
781
421
  entry = createReloadHistoryEntry(attemptId, source, oldVersion, @configInfo?.version or null, 'rejected', e.message or String(e), 'reload_exception', null)
782
422
  @appendReloadHistory(entry)
783
423
  if e.validationErrors
784
- label = if flags.edgefilePath or findEdgeFile(flags.appEntry) then 'Edgefile.rip' else 'config.rip'
424
+ label = 'serve.rip'
785
425
  console.error formatConfigErrors(label, e.validationErrors)
786
426
  else
787
427
  console.error "rip-server: failed to load active config: #{e.message or e}"
@@ -816,31 +456,28 @@ class Server
816
456
  hostIndex: new Map(@appRegistry.hostIndex)
817
457
  wildcardIndex: new Map(@appRegistry.wildcardIndex)
818
458
  configured: new Set(@configuredAppIds)
819
- oldRuntime = @edgeRuntime
459
+ oldRuntime = @servingRuntime
820
460
  oldStreamRuntime = @streamRuntime
821
- stagedRuntime = @buildEdgeRuntime(loaded)
461
+ stagedRuntime = @buildRuntime(loaded)
822
462
  stagedStreamRuntime = @buildStreamRuntimeForConfig(loaded)
823
463
 
824
464
  @clearConfiguredApps()
825
465
  unless loaded
826
- @activateEdgeRuntime(stagedRuntime)
466
+ @activateRuntime(stagedRuntime)
827
467
  @activateStreamRuntime(stagedStreamRuntime, deferListeners: source is 'startup')
828
468
  return
829
469
 
830
470
  baseDir = dirname(loaded.source.path)
831
471
  try
832
- registered = if loaded.source.kind is 'edge'
833
- applyEdgeConfig(loaded.normalized, @appRegistry, registerApp, baseDir)
834
- else
835
- applyConfig(loaded.normalized, @appRegistry, registerApp, baseDir)
472
+ registered = applyConfig(loaded.normalized, @appRegistry, registerApp, baseDir)
836
473
  for app in registered
837
474
  @configuredAppIds.add(app.id) unless app.id is @defaultAppId
838
- oldRuntime = @activateEdgeRuntime(stagedRuntime)
475
+ oldRuntime = @activateRuntime(stagedRuntime)
839
476
  oldStreamRuntime = @activateStreamRuntime(stagedStreamRuntime, deferListeners: source is 'startup')
840
477
  if verifyAfterActivate
841
- result = @verifyEdgeRuntime!(stagedRuntime, loaded)
478
+ result = @verifyRuntime!(stagedRuntime, loaded)
842
479
  unless result.ok
843
- @rollbackEdgeRuntime(oldRuntime, stagedRuntime, snapshot, result)
480
+ @rollbackRuntime(oldRuntime, stagedRuntime, snapshot, result)
844
481
  @rollbackStreamRuntime(oldStreamRuntime, stagedStreamRuntime)
845
482
  logEvent('config_rollback',
846
483
  source: source
@@ -858,7 +495,7 @@ class Server
858
495
  rolledBackFrom: null
859
496
  loadedAt: new Date().toISOString()
860
497
  )
861
- @edgeRuntime.configInfo = @configInfo
498
+ @servingRuntime.configInfo = @configInfo
862
499
  logEvent('config_activated',
863
500
  source: source
864
501
  oldVersion: oldRuntime?.configInfo?.version
@@ -881,14 +518,15 @@ class Server
881
518
  @flags.httpPort = @server.port
882
519
  else
883
520
  @tlsMaterial = @loadTlsMaterial!
884
- if @edgeRuntime?.configInfo?.certs and Object.keys(@edgeRuntime.configInfo.certs).length > 0
885
- @tlsMaterial = buildTlsArray(@tlsMaterial, @edgeRuntime.configInfo.certs)
521
+ if @servingRuntime?.configInfo?.certs and Object.keys(@servingRuntime.configInfo.certs).length > 0
522
+ @tlsMaterial = buildTlsArray(@tlsMaterial, @servingRuntime.configInfo.certs)
886
523
  publicHttpsPort = @flags.httpsPort or 443
887
524
  @applyHttpsMode(@shouldUseInternalHttps(@streamRuntime, publicHttpsPort), publicHttpsPort)
888
525
  httpsPort = @flags.httpsPort
889
526
  @streamListenersDeferred = true if @internalHttpsServer and @streamRuntime.listeners.size is 0
890
527
 
891
- if @flags.redirectHttp or @flags.acme or @flags.acmeStaging
528
+ configHasAcme = @servingRuntime?.configInfo?.acmeDomains?.length > 0
529
+ if @flags.redirectHttp or @flags.acme or @flags.acmeStaging or configHasAcme
892
530
  challengeStore = @challengeStore
893
531
  try
894
532
  @server = Bun.serve
@@ -921,8 +559,8 @@ class Server
921
559
  , 1000
922
560
 
923
561
  stop: ->
924
- stopHealthChecks(@edgeRuntime.upstreamPool)
925
- stopHealthChecks(runtime.upstreamPool) for runtime in @retiredEdgeRuntimes
562
+ stopHealthChecks(@servingRuntime.upstreamPool)
563
+ stopHealthChecks(runtime.upstreamPool) for runtime in @retiredServingRuntimes
926
564
  stopStreamListeners(@streamRuntime, true)
927
565
  stopStreamListeners(runtime, true) for runtime in @retiredStreamRuntimes
928
566
  clearInterval(@queueSweepTimer) if @queueSweepTimer
@@ -942,90 +580,10 @@ class Server
942
580
  controlStopMdnsAdvertisements(@mdnsProcesses)
943
581
 
944
582
  proxyRouteToUpstream: (req, route, requestId, clientIp, runtime) ->
945
- upstream = getUpstream(runtime.upstreamPool, route.upstream)
946
- return serviceUnavailableResponse() unless upstream
947
-
948
- attempt = 1
949
- start = performance.now()
950
-
951
- while attempt <= upstream.retry.attempts
952
- target = selectTarget(upstream, runtime.upstreamPool.nowFn)
953
- return serviceUnavailableResponse() unless target
954
-
955
- markTargetBusy(target)
956
- workerSeconds = 0
957
- res = null
958
- success = false
959
-
960
- try
961
- t0 = performance.now()
962
- timeoutMs = route.timeouts?.readMs or upstream.timeouts?.readMs or @flags.readTimeoutMs
963
- res = proxyToUpstream!(req, target.url,
964
- timeoutMs: timeoutMs
965
- clientIp: clientIp
966
- )
967
- workerSeconds = (performance.now() - t0) / 1000
968
- success = res.status < 500
969
- catch err
970
- console.error "[server] proxyRouteToUpstream error:", err.message or err if isDebug()
971
- res = serviceUnavailableResponse()
972
- finally
973
- releaseTarget(target, workerSeconds * 1000, success, runtime.upstreamPool)
974
-
975
- if res and shouldRetryUpstream(upstream.retry, req.method, res.status, false) and attempt < upstream.retry.attempts
976
- delayMs = computeRetryDelayMs(upstream.retry, attempt, runtime.upstreamPool.randomFn)
977
- await new Promise (r) -> setTimeout(r, delayMs)
978
- attempt++
979
- continue
980
-
981
- totalSeconds = (performance.now() - start) / 1000
982
- @metrics.forwarded++
983
- @metrics.recordLatency(totalSeconds)
984
- @metrics.recordStatus(res.status)
985
- response = buildUpstreamResponse(
986
- res,
987
- req,
988
- totalSeconds,
989
- workerSeconds,
990
- @maybeAddSecurityHeaders.bind(@),
991
- @logAccess.bind(@),
992
- stripInternalHeaders
993
- )
994
- response.headers.set('X-Request-Id', requestId) if requestId
995
- response.headers.set('X-Rip-Route', route.id) if route.id
996
- return response
997
-
998
- serviceUnavailableResponse()
583
+ proxyRouteToUpstreamFn!(req, route, requestId, clientIp, runtime, @metrics, @flags, @maybeAddSecurityHeaders.bind(@), @logAccess.bind(@))
999
584
 
1000
585
  upgradeProxyWebSocket: (req, bunServer, route, requestId, runtime) ->
1001
- upstream = getUpstream(runtime.upstreamPool, route.upstream)
1002
- return new Response('Service unavailable', { status: 503 }) unless upstream
1003
- target = selectTarget(upstream, runtime.upstreamPool.nowFn)
1004
- return new Response('Service unavailable', { status: 503 }) unless target
1005
-
1006
- inUrl = new URL(req.url)
1007
- protocols = (req.headers.get('sec-websocket-protocol') or '')
1008
- .split(',')
1009
- .map((p) -> p.trim())
1010
- .filter(Boolean)
1011
-
1012
- markTargetBusy(target)
1013
- data =
1014
- kind: 'edge-proxy'
1015
- runtime: runtime
1016
- requestId: requestId
1017
- routeId: route.id
1018
- upstreamTarget: target
1019
- upstreamUrl: toUpstreamWsUrl(target.url, inUrl.pathname, inUrl.search)
1020
- protocols: protocols
1021
- passthrough: null
1022
- released: false
1023
-
1024
- if bunServer.upgrade(req, { data })
1025
- return
1026
-
1027
- releaseTarget(target, 0, false, runtime.upstreamPool)
1028
- new Response('WebSocket upgrade failed', { status: 400 })
586
+ upgradeProxyWebSocketFn(req, bunServer, route, requestId, runtime)
1029
587
 
1030
588
  fetch: (req, bunServer) ->
1031
589
  url = new URL(req.url)
@@ -1085,17 +643,17 @@ class Server
1085
643
  # Assign request ID for tracing
1086
644
  requestId = req.headers.get('x-request-id') or generateRequestId()
1087
645
 
1088
- # Edge route table — upstream proxy routes can handle hosts outside the managed app registry
1089
- runtime = @edgeRuntime
646
+ # Route table — proxy routes can handle hosts outside the managed app registry
647
+ runtime = @servingRuntime
1090
648
  matchedRoute = matchRoute(runtime.routeTable, host, url.pathname, req.method)
1091
- if matchedRoute?.upstream and matchedRoute.websocket and bunServer
649
+ if matchedRoute?.proxy and matchedRoute.websocket and bunServer
1092
650
  return @upgradeProxyWebSocket(req, bunServer, matchedRoute, requestId, runtime)
1093
- if matchedRoute?.upstream
1094
- @retainEdgeRuntime(runtime)
651
+ if matchedRoute?.proxy
652
+ @retainRuntime(runtime)
1095
653
  try
1096
654
  return @proxyRouteToUpstream!(req, matchedRoute, requestId, clientIp, runtime)
1097
655
  finally
1098
- @releaseEdgeRuntime(runtime)
656
+ @releaseRuntime(runtime)
1099
657
  if matchedRoute?.static
1100
658
  return serveStaticRoute(req, url, matchedRoute)
1101
659
  if matchedRoute?.redirect
@@ -1107,7 +665,7 @@ class Server
1107
665
  app = getAppState(@appRegistry, appId)
1108
666
  return new Response('Host not found', { status: 404 }) unless app
1109
667
 
1110
- @retainEdgeRuntime(runtime) if matchedRoute?.app
668
+ @retainRuntime(runtime) if matchedRoute?.app
1111
669
 
1112
670
  # Fast path: try available worker
1113
671
  if app.inflightTotal < Math.max(1, app.sockets.length)
@@ -1118,13 +676,13 @@ class Server
1118
676
  try
1119
677
  return @forwardToWorker!(req, sock, app, requestId)
1120
678
  finally
1121
- @releaseEdgeRuntime(runtime) if matchedRoute?.app
679
+ @releaseRuntime(runtime) if matchedRoute?.app
1122
680
  app.inflightTotal--
1123
681
  setImmediate => @drainQueue(app)
1124
682
 
1125
683
  if app.queue.length >= app.maxQueue
1126
684
  @metrics.queueShed++
1127
- @releaseEdgeRuntime(runtime) if matchedRoute?.app
685
+ @releaseRuntime(runtime) if matchedRoute?.app
1128
686
  return serverBusyResponse()
1129
687
 
1130
688
  @metrics.queued++
@@ -1133,66 +691,56 @@ class Server
1133
691
  originalResolve = resolve
1134
692
  originalReject = reject
1135
693
  resolve = (value) =>
1136
- @releaseEdgeRuntime(runtime)
694
+ @releaseRuntime(runtime)
1137
695
  originalResolve(value)
1138
696
  reject = (err) =>
1139
- @releaseEdgeRuntime(runtime)
697
+ @releaseRuntime(runtime)
1140
698
  originalReject(err)
1141
699
  app.queue.push({ req, resolve, reject, enqueuedAt: nowMs() })
1142
700
 
1143
701
  status: ->
1144
702
  app = getAppState(@appRegistry, @defaultAppId)
1145
703
  workerCount = if app then app.sockets.length else 0
1146
- body = edgeBuildStatusBody(@startedAt, workerCount, @appRegistry.hostIndex, @serverVersion, @flags.appName, @flags.httpPort, @flags.httpsPort, nowMs)
704
+ body = buildStatusBody(@startedAt, workerCount, @appRegistry.hostIndex, @serverVersion, @flags.appName, @flags.httpPort, @flags.httpsPort, nowMs)
1147
705
  headers = new Headers({ 'content-type': 'application/json', 'cache-control': 'no-cache' })
1148
706
  @maybeAddSecurityHeaders(headers)
1149
707
  new Response(body, { headers })
1150
708
 
1151
709
  diagnostics: ->
1152
- snap = @metrics.snapshot(@startedAt, @appRegistry, @edgeRuntime.upstreamPool)
1153
- body = JSON.stringify
1154
- status: if snap.gauges.workersActive > 0 then 'healthy' else 'degraded'
1155
- version: { server: @serverVersion, rip: @ripVersion }
1156
- uptime: snap.uptime
1157
- apps: snap.apps
1158
- upstreams: snap.upstreams
1159
- metrics:
1160
- requests: snap.counters.requests
1161
- responses: snap.counters.responses
1162
- latency: snap.latency
1163
- queue: snap.counters.queue
1164
- workers: snap.counters.workers
1165
- acme: snap.counters.acme
1166
- websocket: snap.counters.websocket
1167
- gauges: snap.gauges
1168
- realtime: getRealtimeStats(@realtimeHub)
710
+ body = buildDiagnosticsBody
711
+ metrics: @metrics
712
+ startedAt: @startedAt
713
+ appRegistry: @appRegistry
714
+ servingRuntime: @servingRuntime
715
+ serverVersion: @serverVersion
716
+ ripVersion: @ripVersion
717
+ configInfo: @configInfo
718
+ realtimeStats: getRealtimeStats(@realtimeHub)
1169
719
  hosts: Array.from(@appRegistry.hostIndex.keys())
1170
- streams: streamDiagnostics(@streamRuntime)
1171
- config: Object.assign({}, @configInfo,
1172
- multiplexer:
1173
- enabled: Boolean(@internalHttpsServer)
1174
- publicHttpsPort: @multiplexerPorts.publicHttpsPort
1175
- internalHttpsPort: @multiplexerPorts.internalHttpsPort
1176
- activeRuntime:
1177
- id: @edgeRuntime.id
1178
- inflight: @edgeRuntime.inflight
1179
- wsConnections: @edgeRuntime.wsConnections
1180
- retiredRuntimes: @retiredEdgeRuntimes.map((runtime) ->
1181
- id: runtime.id
1182
- inflight: runtime.inflight
1183
- wsConnections: runtime.wsConnections
1184
- retiredAt: runtime.retiredAt
1185
- )
1186
- activeStreamRuntime:
1187
- id: @streamRuntime.id
1188
- inflight: @streamRuntime.inflight
1189
- listeners: Array.from(@streamRuntime.listeners.keys())
1190
- retiredStreamRuntimes: @retiredStreamRuntimes.map((runtime) ->
1191
- id: runtime.id
1192
- inflight: runtime.inflight
1193
- listeners: Array.from(runtime.listeners.keys())
1194
- retiredAt: runtime.retiredAt
1195
- )
720
+ streamDiagnostics: streamDiagnostics(@streamRuntime)
721
+ multiplexer:
722
+ enabled: Boolean(@internalHttpsServer)
723
+ publicHttpsPort: @multiplexerPorts.publicHttpsPort
724
+ internalHttpsPort: @multiplexerPorts.internalHttpsPort
725
+ activeRuntime:
726
+ id: @servingRuntime.id
727
+ inflight: @servingRuntime.inflight
728
+ wsConnections: @servingRuntime.wsConnections
729
+ retiredRuntimes: @retiredServingRuntimes.map((runtime) ->
730
+ id: runtime.id
731
+ inflight: runtime.inflight
732
+ wsConnections: runtime.wsConnections
733
+ retiredAt: runtime.retiredAt
734
+ )
735
+ activeStreamRuntime:
736
+ id: @streamRuntime.id
737
+ inflight: @streamRuntime.inflight
738
+ listeners: Array.from(@streamRuntime.listeners.keys())
739
+ retiredStreamRuntimes: @retiredStreamRuntimes.map((runtime) ->
740
+ id: runtime.id
741
+ inflight: runtime.inflight
742
+ listeners: Array.from(runtime.listeners.keys())
743
+ retiredAt: runtime.retiredAt
1196
744
  )
1197
745
  headers = new Headers({ 'content-type': 'application/json', 'cache-control': 'no-cache' })
1198
746
  @maybeAddSecurityHeaders(headers)
@@ -1326,9 +874,8 @@ class Server
1326
874
 
1327
875
  port = @flags.httpsPort or @flags.httpPort or 80
1328
876
  protocol = if @flags.httpsPort then 'https' else 'http'
1329
- url = edgeBuildRipdevUrl(@flags.appName, protocol, port, formatPort)
877
+ url = buildRipdevUrl(@flags.appName, protocol, port, formatPort)
1330
878
  @urls.push(url)
1331
- p "rip-server: #{url}" unless @flags.quiet or @flags.hideUrls
1332
879
 
1333
880
  controlFetch: (req) ->
1334
881
  url = new URL(req.url)
@@ -1366,13 +913,14 @@ class Server
1366
913
  new Response('not-found', { status: 404 })
1367
914
 
1368
915
  maybeAddSecurityHeaders: (headers) ->
1369
- edgeMaybeAddSecurityHeaders(@httpsActive, @flags.hsts, headers)
916
+ addSecurityHeaders(@httpsActive, @flags.hsts, headers)
1370
917
 
1371
918
  loadTlsMaterial: ->
1372
- edgeLoadTlsMaterial(@flags, import.meta.dir, (domain) -> acmeLoadCert(defaultCertDir(), domain))
919
+ certDir = @servingRuntime?.configInfo?.certDir or defaultCertDir()
920
+ loadTls(@flags, import.meta.dir, (domain) -> acmeLoadCert(certDir, domain))
1373
921
 
1374
922
  printCertSummary: (certPem) ->
1375
- edgePrintCertSummary(certPem)
923
+ printCertSummary(certPem)
1376
924
 
1377
925
  getLanIP: ->
1378
926
  controlGetLanIP(networkInterfaces)
@@ -1392,77 +940,23 @@ class Server
1392
940
  proxyRealtimeToWorker: (headers, frameType, body) ->
1393
941
  app = getAppState(@appRegistry, @defaultAppId)
1394
942
  return null unless app
1395
- sock = @getNextAvailableSocket(app)
1396
- return null unless sock
1397
- proxyHeaders = new Headers(headers)
1398
- proxyHeaders.set('Sec-WebSocket-Frame', frameType)
1399
- proxyHeaders.set('Content-Type', 'text/plain')
1400
- proxyHeaders.delete('Upgrade')
1401
- proxyHeaders.delete('Connection')
1402
- proxyHeaders.delete('Sec-WebSocket-Key')
1403
- proxyHeaders.delete('Sec-WebSocket-Version')
1404
- proxyHeaders.delete('Sec-WebSocket-Extensions')
1405
- try
1406
- res = fetch! "http://localhost/v1/realtime",
1407
- method: 'POST'
1408
- headers: proxyHeaders
1409
- body: body or ''
1410
- unix: sock.socket
1411
- decompress: false
1412
- res.text!
1413
- catch e
1414
- console.error "realtime: worker proxy failed:", e.message
1415
- null
1416
- finally
1417
- @releaseWorker(sock, app)
943
+ realtimeProxyToWorker(headers, frameType, body,
944
+ (=> @getNextAvailableSocket(app)),
945
+ ((sock) => @releaseWorker(sock, app))
946
+ )
1418
947
 
1419
948
  buildWebSocketHandlers: ->
1420
- hub = @realtimeHub
1421
- server = @
1422
-
1423
- websocket:
1424
- idleTimeout: 120 # seconds evict idle/stale connections
1425
- sendPings: true # Bun auto-sends pings to detect dead connections
1426
-
1427
- open: (ws) ->
1428
- if ws.data?.kind is 'edge-proxy'
1429
- server.retainEdgeRuntimeWs(ws.data.runtime)
1430
- ws.data.passthrough = createWsPassthrough(ws, ws.data.upstreamUrl, ws.data.protocols or [])
1431
- return
1432
-
1433
- { clientId, headers } = ws.data
1434
- addClient(hub, clientId, ws)
1435
- server.metrics.wsConnections++
1436
- logEvent 'ws_open', { clientId }
1437
- response = server.proxyRealtimeToWorker!(headers, 'open', '')
1438
- processResponse(hub, response, clientId) if response
1439
-
1440
- message: (ws, message) ->
1441
- if ws.data?.kind is 'edge-proxy'
1442
- ws.data.passthrough?.sendToUpstream(message)
1443
- return
1444
-
1445
- { clientId, headers } = ws.data
1446
- server.metrics.wsMessages++
1447
- isBinary = typeof message isnt 'string'
1448
- msg = if isBinary then Buffer.from(message) else message
1449
- frameType = if isBinary then 'binary' else 'text'
1450
- response = server.proxyRealtimeToWorker!(headers, frameType, msg)
1451
- processResponse(hub, response, clientId) if response
1452
-
1453
- close: (ws) ->
1454
- if ws.data?.kind is 'edge-proxy'
1455
- ws.data.passthrough?.close()
1456
- unless ws.data.released
1457
- releaseTarget(ws.data.upstreamTarget, 0, true, ws.data.runtime.upstreamPool)
1458
- server.releaseEdgeRuntimeWs(ws.data.runtime)
1459
- ws.data.released = true
1460
- return
1461
-
1462
- { clientId, headers } = ws.data
1463
- logEvent 'ws_close', { clientId }
1464
- removeClient(hub, clientId)
1465
- server.proxyRealtimeToWorker!(headers, 'close', '')
949
+ buildWsHandlers(@realtimeHub,
950
+ metrics: @metrics
951
+ retainRuntimeWs: @retainRuntimeWs.bind(@)
952
+ releaseRuntimeWs: @releaseRuntimeWs.bind(@)
953
+ createWsPassthrough: (ws, url, protocols) -> createWsPassthrough(ws, url, protocols)
954
+ releaseProxyTarget: (data) =>
955
+ releaseTarget(data.upstreamTarget, 0, true, data.runtime.upstreamPool)
956
+ @releaseRuntimeWs(data.runtime)
957
+ logEvent: logEvent
958
+ proxyRealtimeToWorker: @proxyRealtimeToWorker.bind(@)
959
+ )
1466
960
 
1467
961
 
1468
962
  # ==============================================================================
@@ -1481,8 +975,14 @@ main = ->
1481
975
  if flags.checkConfig
1482
976
  runCheckConfig!(flags)
1483
977
  return
978
+ if flags.showInfo
979
+ showInfo!(fetch, exit)
980
+ return
1484
981
  if flags.reloadConfig
1485
- runReloadSubcommand!(flags.socketPrefix, getControlSocketPath, fetch, exit)
982
+ reloadConfig!(flags.socketPrefix, getControlSocketPath, fetch, exit)
983
+ return
984
+ if flags.listApps
985
+ listApps!(flags.socketPrefix, getControlSocketPath, fetch, exit)
1486
986
  return
1487
987
  if flags.stopServer
1488
988
  stopServer(flags.socketPrefix, getPidFilePath, existsSync, readFileSync, process.kill.bind(process), Bun.spawnSync, import.meta.path)
@@ -1492,7 +992,7 @@ main = ->
1492
992
  writeFileSync(pidFile, String(process.pid))
1493
993
 
1494
994
  svr = new Server(flags)
1495
- mgr = new Manager(flags)
995
+ mgr = new Manager(flags, import.meta.path)
1496
996
  svr.manager = mgr
1497
997
  mgr.server = svr
1498
998
 
@@ -1507,26 +1007,42 @@ main = ->
1507
1007
 
1508
1008
  svr.start!
1509
1009
 
1510
- # ACME auto-TLS: obtain cert if needed, start renewal loop
1511
- if (flags.acme or flags.acmeStaging) and flags.acmeDomain
1010
+ # ACME auto-TLS: obtain certs for all domains, start renewal loop
1011
+ unless flags.noAcme
1012
+ acmeDomains = []
1013
+ acmeDomains.push(flags.acmeDomain) if flags.acmeDomain
1014
+ configAcme = svr.servingRuntime?.configInfo?.acmeDomains or []
1015
+ for d in configAcme
1016
+ acmeDomains.push(d) unless acmeDomains.includes(d)
1017
+
1018
+ if not flags.noAcme and acmeDomains?.length > 0
1512
1019
  acmeMgr = createAcmeManager
1513
- certDir: defaultCertDir()
1020
+ certDir: svr.servingRuntime?.configInfo?.certDir or defaultCertDir()
1514
1021
  staging: flags.acmeStaging
1515
1022
  challengeStore: svr.challengeStore
1516
1023
  onCertRenewed: (domain, result) ->
1517
- p "rip-acme: cert renewed for #{domain}, graceful restart needed"
1024
+ p "rip-acme: cert renewed for #{domain}"
1025
+ try
1026
+ svr.tlsMaterial = svr.loadTlsMaterial!
1027
+ if svr.servingRuntime?.configInfo?.certs and Object.keys(svr.servingRuntime.configInfo.certs).length > 0
1028
+ svr.tlsMaterial = buildTlsArray(svr.tlsMaterial, svr.servingRuntime.configInfo.certs)
1029
+ svr.httpsServer?.reload({ tls: svr.tlsMaterial }) if svr.httpsServer
1030
+ svr.internalHttpsServer?.reload({ tls: svr.tlsMaterial }) if svr.internalHttpsServer
1031
+ p "rip-acme: TLS reloaded with renewed cert for #{domain}"
1032
+ catch e
1033
+ console.error "rip-acme: TLS reload failed after renewal: #{e.message}"
1518
1034
  svr.acmeManager = acmeMgr
1519
- try acmeMgr.obtainCert!(flags.acmeDomain)
1520
- catch e
1521
- console.error "rip-acme: initial cert obtain failed: #{e.message}"
1522
- console.error "rip-acme: will retry during renewal loop"
1523
- acmeMgr.startRenewalLoop([flags.acmeDomain])
1035
+ for domain in acmeDomains
1036
+ try acmeMgr.obtainCert!(domain)
1037
+ catch e
1038
+ console.error "rip-acme: cert obtain failed for #{domain}: #{e.message}"
1039
+ console.error "rip-acme: will retry during renewal loop"
1040
+ acmeMgr.startRenewalLoop(acmeDomains)
1524
1041
 
1525
1042
  process.env.RIP_URLS = svr.urls.join(',') # exact URLs the Server just built
1526
1043
  mgr.start!
1527
1044
 
1528
- logStartupSummary(svr, flags, edgeBuildRipdevUrl, formatPort)
1529
- logEvent('server_start', { app: flags.appName, workers: flags.workers })
1045
+ logStartupSummary(svr, flags, buildRipdevUrl, formatPort)
1530
1046
 
1531
1047
  # ==============================================================================
1532
1048
  # Entry Point