@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/server.rip +79 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rip-lang/server",
3
- "version": "1.1.6",
3
+ "version": "1.1.8",
4
4
  "description": "Pure Rip application server — multi-worker, hot reload, HTTPS, mDNS",
5
5
  "type": "module",
6
6
  "main": "server.rip",
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 - touch entry file on changes (opt-in via -w/--watch)
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...'