@rip-lang/server 1.1.7 → 1.1.9
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 +36 -13
- package/package.json +1 -1
- package/server.rip +91 -21
package/README.md
CHANGED
|
@@ -311,7 +311,12 @@ The server uses a single-file, self-spawning architecture:
|
|
|
311
311
|
└─────────────────────────────────────────────────┘
|
|
312
312
|
```
|
|
313
313
|
|
|
314
|
-
When `
|
|
314
|
+
When `RIP_SETUP_MODE=1` is set, the same file runs the one-time setup phase. When `RIP_WORKER_MODE=1` is set, it runs as a worker.
|
|
315
|
+
|
|
316
|
+
### Startup Lifecycle
|
|
317
|
+
|
|
318
|
+
1. **Setup** — If `setup.rip` exists next to the entry file, it runs once in a temporary process before any workers spawn. Use this for database migrations, table creation, and seeding.
|
|
319
|
+
2. **Workers** — N worker processes are spawned, each loading the entry file and serving requests.
|
|
315
320
|
|
|
316
321
|
### Request Flow
|
|
317
322
|
|
|
@@ -323,12 +328,10 @@ When `RIP_WORKER_MODE=1` is set, the same `server.rip` file runs as a worker ins
|
|
|
323
328
|
|
|
324
329
|
### Hot Reloading
|
|
325
330
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
- **Default**: Only the entry file is watched
|
|
329
|
-
- **With `-w`**: All matching files in the app directory are watched
|
|
331
|
+
Two layers of hot reload work together in development:
|
|
330
332
|
|
|
331
|
-
|
|
333
|
+
- **API changes** (`-w` flag) — The Manager watches for `.rip` file changes in the API directory and triggers rolling worker restarts (zero downtime, server-side).
|
|
334
|
+
- **UI changes** (`watch: true` in `ripUI`) — Workers register their app's component directories with the Manager via the control socket. The Manager watches those directories and broadcasts SSE reload events to connected browsers (client-side). SSE connections are held by the long-lived Server process, not by workers.
|
|
332
335
|
|
|
333
336
|
Use `--static` in production to disable hot reload entirely.
|
|
334
337
|
|
|
@@ -409,6 +412,24 @@ export default
|
|
|
409
412
|
fetch: (req) -> new Response('Hello!')
|
|
410
413
|
```
|
|
411
414
|
|
|
415
|
+
## One-Time Setup
|
|
416
|
+
|
|
417
|
+
If a `setup.rip` file exists next to your entry file, rip-server runs it
|
|
418
|
+
automatically **once** before spawning any workers. This is ideal for database
|
|
419
|
+
migrations, table creation, and seeding.
|
|
420
|
+
|
|
421
|
+
```coffee
|
|
422
|
+
# setup.rip — runs once before workers start
|
|
423
|
+
export setup = ->
|
|
424
|
+
await createTables()
|
|
425
|
+
await seedData()
|
|
426
|
+
console.log 'Database ready'
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
The setup function can export as `setup` or `default`. If the file doesn't
|
|
430
|
+
exist, the setup phase is skipped entirely (no overhead). If setup fails,
|
|
431
|
+
the server exits immediately.
|
|
432
|
+
|
|
412
433
|
## Environment Variables
|
|
413
434
|
|
|
414
435
|
Most settings are configured via CLI flags, but environment variables provide an alternative for containers, CI/CD, or system-wide defaults.
|
|
@@ -501,14 +522,16 @@ This gives you:
|
|
|
501
522
|
|
|
502
523
|
When running with `-w`, two layers of hot reload work together:
|
|
503
524
|
|
|
504
|
-
1. **
|
|
505
|
-
and triggers rolling worker restarts (server-side
|
|
506
|
-
2. **
|
|
507
|
-
directories
|
|
525
|
+
1. **API hot reload** (`-w` flag) — The Manager watches for `.rip` file changes
|
|
526
|
+
in the API directory and triggers rolling worker restarts (server-side).
|
|
527
|
+
2. **UI hot reload** (`watch: true`) — Workers register their component
|
|
528
|
+
directories with the Manager via the control socket. The Manager watches
|
|
529
|
+
those directories and tells the Server to broadcast SSE reload events to
|
|
530
|
+
connected browsers (client-side).
|
|
508
531
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
532
|
+
SSE connections are held by the long-lived Server process, not by recyclable
|
|
533
|
+
workers, ensuring stable hot-reload connections. Each app prefix gets its own
|
|
534
|
+
SSE pool for multi-app isolation.
|
|
512
535
|
|
|
513
536
|
## Comparison with Other Servers
|
|
514
537
|
|
package/package.json
CHANGED
package/server.rip
CHANGED
|
@@ -352,6 +352,18 @@ parseFlags = (argv) ->
|
|
|
352
352
|
# Worker Mode
|
|
353
353
|
# ==============================================================================
|
|
354
354
|
|
|
355
|
+
runSetup = ->
|
|
356
|
+
setupFile = process.env.RIP_SETUP_FILE
|
|
357
|
+
try
|
|
358
|
+
mod = import!(setupFile)
|
|
359
|
+
await Promise.resolve()
|
|
360
|
+
fn = mod?.setup or mod?.default
|
|
361
|
+
if typeof fn is 'function'
|
|
362
|
+
await fn()
|
|
363
|
+
catch e
|
|
364
|
+
console.error "rip-server: setup failed:", e
|
|
365
|
+
process.exit(1)
|
|
366
|
+
|
|
355
367
|
runWorker = ->
|
|
356
368
|
workerId = parseInt(process.env.WORKER_ID or '0')
|
|
357
369
|
maxRequests = parseInt(process.env.MAX_REQUESTS or '10000')
|
|
@@ -485,14 +497,32 @@ class Manager
|
|
|
485
497
|
@nextWorkerId = -1
|
|
486
498
|
@retiringIds = new Set()
|
|
487
499
|
@currentVersion = 1
|
|
488
|
-
@
|
|
489
|
-
@
|
|
500
|
+
@server = null
|
|
501
|
+
@appWatchers = new Map()
|
|
490
502
|
|
|
491
503
|
process.on 'SIGTERM', => @shutdown!
|
|
492
504
|
process.on 'SIGINT', => @shutdown!
|
|
493
505
|
|
|
494
506
|
start: ->
|
|
495
507
|
@stop!
|
|
508
|
+
|
|
509
|
+
# Run one-time setup if setup.rip exists next to the entry file
|
|
510
|
+
setupFile = join(dirname(@flags.appEntry), 'setup.rip')
|
|
511
|
+
if existsSync(setupFile)
|
|
512
|
+
setupEnv = Object.assign {}, process.env,
|
|
513
|
+
RIP_SETUP_MODE: '1'
|
|
514
|
+
RIP_SETUP_FILE: setupFile
|
|
515
|
+
proc = Bun.spawn ['rip', import.meta.path],
|
|
516
|
+
stdout: 'inherit'
|
|
517
|
+
stderr: 'inherit'
|
|
518
|
+
stdin: 'ignore'
|
|
519
|
+
cwd: process.cwd()
|
|
520
|
+
env: setupEnv
|
|
521
|
+
code = await proc.exited
|
|
522
|
+
if code isnt 0
|
|
523
|
+
console.error "rip-server: setup exited with code #{code}"
|
|
524
|
+
process.exit(1)
|
|
525
|
+
|
|
496
526
|
@workers = []
|
|
497
527
|
for i in [0...@flags.workers]
|
|
498
528
|
w = @spawnWorker!(@currentVersion)
|
|
@@ -534,13 +564,6 @@ class Manager
|
|
|
534
564
|
utimesSync(entryFile, now, now)
|
|
535
565
|
catch
|
|
536
566
|
null
|
|
537
|
-
# Debounced SSE broadcast to connected clients
|
|
538
|
-
if @onFileChange
|
|
539
|
-
clearTimeout(@_broadcastTimer) if @_broadcastTimer
|
|
540
|
-
@_broadcastTimer = setTimeout =>
|
|
541
|
-
@_broadcastTimer = null
|
|
542
|
-
@onFileChange()
|
|
543
|
-
, debounceMs
|
|
544
567
|
catch e
|
|
545
568
|
console.warn "rip-server: directory watch failed: #{e.message}"
|
|
546
569
|
|
|
@@ -550,6 +573,25 @@ class Manager
|
|
|
550
573
|
try unlinkSync(w.socketPath) catch then null
|
|
551
574
|
@workers = []
|
|
552
575
|
|
|
576
|
+
watchDirs: (prefix, dirs) ->
|
|
577
|
+
return if @appWatchers.has(prefix)
|
|
578
|
+
timer = null
|
|
579
|
+
watchers = []
|
|
580
|
+
broadcast = =>
|
|
581
|
+
clearTimeout(timer) if timer
|
|
582
|
+
timer = setTimeout =>
|
|
583
|
+
timer = null
|
|
584
|
+
@server?.broadcastChange(prefix)
|
|
585
|
+
, 250
|
|
586
|
+
for dir in dirs
|
|
587
|
+
try
|
|
588
|
+
w = watch dir, { recursive: true }, (event, filename) ->
|
|
589
|
+
broadcast() if filename?.endsWith('.rip')
|
|
590
|
+
watchers.push(w)
|
|
591
|
+
catch e
|
|
592
|
+
console.warn "rip-server: watch failed for #{dir}: #{e.message}"
|
|
593
|
+
@appWatchers.set prefix, { watchers, timer }
|
|
594
|
+
|
|
553
595
|
spawnWorker: (version) ->
|
|
554
596
|
workerId = ++@nextWorkerId
|
|
555
597
|
socketPath = getWorkerSocketPath(@flags.socketPrefix, workerId)
|
|
@@ -683,7 +725,8 @@ class Server
|
|
|
683
725
|
@httpsActive = false
|
|
684
726
|
@hostRegistry = new Set(['localhost', '127.0.0.1', 'rip.local'])
|
|
685
727
|
@mdnsProcesses = new Map()
|
|
686
|
-
@
|
|
728
|
+
@watchGroups = new Map()
|
|
729
|
+
@manager = null
|
|
687
730
|
try
|
|
688
731
|
pkg = JSON.parse(readFileSync(import.meta.dir + '/package.json', 'utf8'))
|
|
689
732
|
@serverVersion = pkg.version
|
|
@@ -773,7 +816,12 @@ class Server
|
|
|
773
816
|
return new Response Bun.file(import.meta.dir + '/server.html')
|
|
774
817
|
|
|
775
818
|
return @status() if url.pathname is '/status'
|
|
776
|
-
|
|
819
|
+
|
|
820
|
+
# SSE hot-reload: intercept /{prefix}/watch for registered watch groups
|
|
821
|
+
path = url.pathname
|
|
822
|
+
if path.endsWith('/watch')
|
|
823
|
+
watchPrefix = path.slice(0, -6) # strip '/watch'
|
|
824
|
+
return @handleWatch(watchPrefix) if @watchGroups.has(watchPrefix)
|
|
777
825
|
|
|
778
826
|
if url.pathname is '/server'
|
|
779
827
|
headers = new Headers({ 'content-type': 'text/plain' })
|
|
@@ -818,31 +866,39 @@ class Server
|
|
|
818
866
|
@maybeAddSecurityHeaders(headers)
|
|
819
867
|
new Response(body, { headers })
|
|
820
868
|
|
|
821
|
-
|
|
869
|
+
registerWatch: (prefix) ->
|
|
870
|
+
return if @watchGroups.has(prefix)
|
|
871
|
+
@watchGroups.set prefix, { sseClients: new Set() }
|
|
872
|
+
|
|
873
|
+
handleWatch: (prefix) ->
|
|
874
|
+
group = @watchGroups.get(prefix)
|
|
875
|
+
return new Response('not-found', { status: 404 }) unless group
|
|
822
876
|
encoder = new TextEncoder()
|
|
823
877
|
client = null
|
|
824
878
|
new Response new ReadableStream(
|
|
825
|
-
start: (controller)
|
|
879
|
+
start: (controller) ->
|
|
826
880
|
send = (event) ->
|
|
827
881
|
try controller.enqueue encoder.encode("event: #{event}\n\n")
|
|
828
882
|
catch then null
|
|
829
883
|
client = { send }
|
|
830
|
-
|
|
884
|
+
group.sseClients.add(client)
|
|
831
885
|
send('connected')
|
|
832
|
-
cancel:
|
|
833
|
-
|
|
886
|
+
cancel: ->
|
|
887
|
+
group.sseClients.delete(client) if client
|
|
834
888
|
),
|
|
835
889
|
headers:
|
|
836
890
|
'Content-Type': 'text/event-stream'
|
|
837
891
|
'Cache-Control': 'no-cache'
|
|
838
892
|
'Connection': 'keep-alive'
|
|
839
893
|
|
|
840
|
-
broadcastChange: ->
|
|
894
|
+
broadcastChange: (prefix) ->
|
|
895
|
+
group = @watchGroups.get(prefix)
|
|
896
|
+
return unless group
|
|
841
897
|
dead = []
|
|
842
|
-
for client as
|
|
898
|
+
for client as group.sseClients
|
|
843
899
|
try client.send('reload')
|
|
844
900
|
catch then dead.push(client)
|
|
845
|
-
|
|
901
|
+
group.sseClients.delete(c) for c in dead
|
|
846
902
|
|
|
847
903
|
getNextAvailableSocket: ->
|
|
848
904
|
while @availableWorkers.length > 0
|
|
@@ -979,6 +1035,17 @@ class Server
|
|
|
979
1035
|
null
|
|
980
1036
|
return new Response(JSON.stringify({ ok: false }), { status: 400, headers: { 'content-type': 'application/json' } })
|
|
981
1037
|
|
|
1038
|
+
if req.method is 'POST' and url.pathname is '/watch'
|
|
1039
|
+
try
|
|
1040
|
+
j = req.json!
|
|
1041
|
+
if j?.op is 'watch' and typeof j.prefix is 'string' and Array.isArray(j.dirs)
|
|
1042
|
+
@registerWatch(j.prefix)
|
|
1043
|
+
@manager?.watchDirs(j.prefix, j.dirs) if j.dirs.length > 0
|
|
1044
|
+
return new Response(JSON.stringify({ ok: true }), { headers: { 'content-type': 'application/json' } })
|
|
1045
|
+
catch
|
|
1046
|
+
null
|
|
1047
|
+
return new Response(JSON.stringify({ ok: false }), { status: 400, headers: { 'content-type': 'application/json' } })
|
|
1048
|
+
|
|
982
1049
|
if url.pathname is '/registry' and req.method is 'GET'
|
|
983
1050
|
return new Response(JSON.stringify({ ok: true, hosts: Array.from(@hostRegistry.values()) }), { headers: { 'content-type': 'application/json' } })
|
|
984
1051
|
|
|
@@ -1214,7 +1281,8 @@ main = ->
|
|
|
1214
1281
|
|
|
1215
1282
|
svr = new Server(flags)
|
|
1216
1283
|
mgr = new Manager(flags)
|
|
1217
|
-
|
|
1284
|
+
svr.manager = mgr
|
|
1285
|
+
mgr.server = svr
|
|
1218
1286
|
|
|
1219
1287
|
cleanup = ->
|
|
1220
1288
|
console.log 'rip-server: shutting down...'
|
|
@@ -1243,7 +1311,9 @@ main = ->
|
|
|
1243
1311
|
# Entry Point
|
|
1244
1312
|
# ==============================================================================
|
|
1245
1313
|
|
|
1246
|
-
if process.env.
|
|
1314
|
+
if process.env.RIP_SETUP_MODE
|
|
1315
|
+
runSetup()
|
|
1316
|
+
else if process.env.RIP_WORKER_MODE
|
|
1247
1317
|
runWorker()
|
|
1248
1318
|
else
|
|
1249
1319
|
main!
|