@rip-lang/server 1.1.6 → 1.1.8
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/package.json +1 -1
- package/server.rip +79 -2
package/package.json
CHANGED
package/server.rip
CHANGED
|
@@ -485,6 +485,8 @@ class Manager
|
|
|
485
485
|
@nextWorkerId = -1
|
|
486
486
|
@retiringIds = new Set()
|
|
487
487
|
@currentVersion = 1
|
|
488
|
+
@server = null
|
|
489
|
+
@appWatchers = new Map()
|
|
488
490
|
|
|
489
491
|
process.on 'SIGTERM', => @shutdown!
|
|
490
492
|
process.on 'SIGINT', => @shutdown!
|
|
@@ -512,20 +514,21 @@ class Manager
|
|
|
512
514
|
@rollingRestart().finally => @isRolling = false
|
|
513
515
|
, 50
|
|
514
516
|
|
|
515
|
-
# Watch files in app directory
|
|
517
|
+
# Watch files in app directory (opt-in via -w/--watch)
|
|
516
518
|
if @flags.watchGlob
|
|
517
519
|
entryFile = @flags.appEntry
|
|
518
520
|
entryBase = basename(entryFile)
|
|
519
521
|
watchExt = if @flags.watchGlob.startsWith('*.') then @flags.watchGlob.slice(1) else null
|
|
522
|
+
debounceMs = @flags.debounce or 250
|
|
520
523
|
try
|
|
521
524
|
watch @flags.appBaseDir, { recursive: true }, (event, filename) =>
|
|
522
525
|
return unless filename
|
|
523
|
-
# Match by extension (e.g., *.rip) or exact glob
|
|
524
526
|
if watchExt
|
|
525
527
|
return unless filename.endsWith(watchExt)
|
|
526
528
|
else
|
|
527
529
|
return unless filename is @flags.watchGlob or filename.endsWith("/#{@flags.watchGlob}")
|
|
528
530
|
return if filename is entryBase or filename.endsWith("/#{entryBase}")
|
|
531
|
+
# Touch entry file to trigger rolling restart
|
|
529
532
|
try
|
|
530
533
|
now = new Date()
|
|
531
534
|
utimesSync(entryFile, now, now)
|
|
@@ -540,6 +543,25 @@ class Manager
|
|
|
540
543
|
try unlinkSync(w.socketPath) catch then null
|
|
541
544
|
@workers = []
|
|
542
545
|
|
|
546
|
+
watchDirs: (prefix, dirs) ->
|
|
547
|
+
return if @appWatchers.has(prefix)
|
|
548
|
+
timer = null
|
|
549
|
+
watchers = []
|
|
550
|
+
broadcast = =>
|
|
551
|
+
clearTimeout(timer) if timer
|
|
552
|
+
timer = setTimeout =>
|
|
553
|
+
timer = null
|
|
554
|
+
@server?.broadcastChange(prefix)
|
|
555
|
+
, 250
|
|
556
|
+
for dir in dirs
|
|
557
|
+
try
|
|
558
|
+
w = watch dir, { recursive: true }, (event, filename) ->
|
|
559
|
+
broadcast() if filename?.endsWith('.rip')
|
|
560
|
+
watchers.push(w)
|
|
561
|
+
catch e
|
|
562
|
+
console.warn "rip-server: watch failed for #{dir}: #{e.message}"
|
|
563
|
+
@appWatchers.set prefix, { watchers, timer }
|
|
564
|
+
|
|
543
565
|
spawnWorker: (version) ->
|
|
544
566
|
workerId = ++@nextWorkerId
|
|
545
567
|
socketPath = getWorkerSocketPath(@flags.socketPrefix, workerId)
|
|
@@ -673,6 +695,8 @@ class Server
|
|
|
673
695
|
@httpsActive = false
|
|
674
696
|
@hostRegistry = new Set(['localhost', '127.0.0.1', 'rip.local'])
|
|
675
697
|
@mdnsProcesses = new Map()
|
|
698
|
+
@watchGroups = new Map()
|
|
699
|
+
@manager = null
|
|
676
700
|
try
|
|
677
701
|
pkg = JSON.parse(readFileSync(import.meta.dir + '/package.json', 'utf8'))
|
|
678
702
|
@serverVersion = pkg.version
|
|
@@ -763,6 +787,12 @@ class Server
|
|
|
763
787
|
|
|
764
788
|
return @status() if url.pathname is '/status'
|
|
765
789
|
|
|
790
|
+
# SSE hot-reload: intercept /{prefix}/watch for registered watch groups
|
|
791
|
+
path = url.pathname
|
|
792
|
+
if path.endsWith('/watch')
|
|
793
|
+
watchPrefix = path.slice(0, -6) # strip '/watch'
|
|
794
|
+
return @handleWatch(watchPrefix) if @watchGroups.has(watchPrefix)
|
|
795
|
+
|
|
766
796
|
if url.pathname is '/server'
|
|
767
797
|
headers = new Headers({ 'content-type': 'text/plain' })
|
|
768
798
|
@maybeAddSecurityHeaders(headers)
|
|
@@ -806,6 +836,40 @@ class Server
|
|
|
806
836
|
@maybeAddSecurityHeaders(headers)
|
|
807
837
|
new Response(body, { headers })
|
|
808
838
|
|
|
839
|
+
registerWatch: (prefix) ->
|
|
840
|
+
return if @watchGroups.has(prefix)
|
|
841
|
+
@watchGroups.set prefix, { sseClients: new Set() }
|
|
842
|
+
|
|
843
|
+
handleWatch: (prefix) ->
|
|
844
|
+
group = @watchGroups.get(prefix)
|
|
845
|
+
return new Response('not-found', { status: 404 }) unless group
|
|
846
|
+
encoder = new TextEncoder()
|
|
847
|
+
client = null
|
|
848
|
+
new Response new ReadableStream(
|
|
849
|
+
start: (controller) ->
|
|
850
|
+
send = (event) ->
|
|
851
|
+
try controller.enqueue encoder.encode("event: #{event}\n\n")
|
|
852
|
+
catch then null
|
|
853
|
+
client = { send }
|
|
854
|
+
group.sseClients.add(client)
|
|
855
|
+
send('connected')
|
|
856
|
+
cancel: ->
|
|
857
|
+
group.sseClients.delete(client) if client
|
|
858
|
+
),
|
|
859
|
+
headers:
|
|
860
|
+
'Content-Type': 'text/event-stream'
|
|
861
|
+
'Cache-Control': 'no-cache'
|
|
862
|
+
'Connection': 'keep-alive'
|
|
863
|
+
|
|
864
|
+
broadcastChange: (prefix) ->
|
|
865
|
+
group = @watchGroups.get(prefix)
|
|
866
|
+
return unless group
|
|
867
|
+
dead = []
|
|
868
|
+
for client as group.sseClients
|
|
869
|
+
try client.send('reload')
|
|
870
|
+
catch then dead.push(client)
|
|
871
|
+
group.sseClients.delete(c) for c in dead
|
|
872
|
+
|
|
809
873
|
getNextAvailableSocket: ->
|
|
810
874
|
while @availableWorkers.length > 0
|
|
811
875
|
worker = @availableWorkers.pop()
|
|
@@ -941,6 +1005,17 @@ class Server
|
|
|
941
1005
|
null
|
|
942
1006
|
return new Response(JSON.stringify({ ok: false }), { status: 400, headers: { 'content-type': 'application/json' } })
|
|
943
1007
|
|
|
1008
|
+
if req.method is 'POST' and url.pathname is '/watch'
|
|
1009
|
+
try
|
|
1010
|
+
j = req.json!
|
|
1011
|
+
if j?.op is 'watch' and typeof j.prefix is 'string' and Array.isArray(j.dirs)
|
|
1012
|
+
@registerWatch(j.prefix)
|
|
1013
|
+
@manager?.watchDirs(j.prefix, j.dirs) if j.dirs.length > 0
|
|
1014
|
+
return new Response(JSON.stringify({ ok: true }), { headers: { 'content-type': 'application/json' } })
|
|
1015
|
+
catch
|
|
1016
|
+
null
|
|
1017
|
+
return new Response(JSON.stringify({ ok: false }), { status: 400, headers: { 'content-type': 'application/json' } })
|
|
1018
|
+
|
|
944
1019
|
if url.pathname is '/registry' and req.method is 'GET'
|
|
945
1020
|
return new Response(JSON.stringify({ ok: true, hosts: Array.from(@hostRegistry.values()) }), { headers: { 'content-type': 'application/json' } })
|
|
946
1021
|
|
|
@@ -1176,6 +1251,8 @@ main = ->
|
|
|
1176
1251
|
|
|
1177
1252
|
svr = new Server(flags)
|
|
1178
1253
|
mgr = new Manager(flags)
|
|
1254
|
+
svr.manager = mgr
|
|
1255
|
+
mgr.server = svr
|
|
1179
1256
|
|
|
1180
1257
|
cleanup = ->
|
|
1181
1258
|
console.log 'rip-server: shutting down...'
|