syntropy 0.15.1 → 0.17

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ef2840797122cef8e0f7f7675c10665c35f60915e091043416ffd0ed70e1cb5c
4
- data.tar.gz: 0d929bfbe0d629879b05c57b7c01c1e8c0ee1af148dc23570c30233f19e81156
3
+ metadata.gz: db440894351239f4fdc164cd3550d741919a8fed568f20620040cca54b7dbebd
4
+ data.tar.gz: 165d4a52b7d7ce7fb8377d7b0e96f108ef7c65cfc768cf59ec73b97a4f7c84e0
5
5
  SHA512:
6
- metadata.gz: 6375f3fb5cbab65d6b710b613c32d0eb9ebb714880f0eb3921fd0b07da9219e0ab94faec2398ae2f33a21fa4f21a8135416faf5b6f2ad0721a99afa067b854f7
7
- data.tar.gz: 715274f642dc3b0db72b0ec37526f26221b56930e19eb324f804408b18552a5c465d59f313cd8329a24a9caa0aa05abaa6907d523f15e1e7061e3e9e1bf1762c
6
+ metadata.gz: d2bb77e92b44ca5e66a46c4427a4705cb2316b42372a71ce3c1e250bf1f93bccbe9201fe9602c174a6315880f33d47338133c121871561ab31e98c5a095bb385
7
+ data.tar.gz: c1069d92fbdb6a4a9eb792fa26c16e3d3494c882eb8a8d09e211a0f16b7b880bab8e0004a74cf4d1969398c2185091282bc3a4eebbf37b325c31ae06d52d3cd0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ # 0.17 2025-09-11
2
+
3
+ - Move repo to [digital-fabric](https://github.com/digital-fabric/syntropy)
4
+
5
+ # 0.16 2025-09-11
6
+
7
+ - `syntropy` script:
8
+ - Remove trailing slash for root dir in syntropy script
9
+ - Rename `-w/--watch` option to `-d/--dev` for development mode
10
+ - Fix `--mount` option
11
+ - Add builtin `/.syntropy` applet for builtin features:
12
+ - auto refresh for web pages
13
+ - JSON API
14
+ - Template debugging frontend tools
15
+ - ping route
16
+ - home page with links to examples
17
+ - Implement applet mounting and loading
18
+ - Remove Papercraft dependency
19
+ - Add support for P2 XML templates, using `#template_xml`
20
+ - Update P2, TP2
21
+
1
22
  # 0.15 2025-08-31
2
23
 
3
24
  - Implement invalidation of reverse dependencies on module file change
data/README.md CHANGED
@@ -9,10 +9,10 @@
9
9
  <a href="http://rubygems.org/gems/syntropy">
10
10
  <img src="https://badge.fury.io/rb/syntropy.svg" alt="Ruby gem">
11
11
  </a>
12
- <a href="https://github.com/noteflakes/syntropy/actions">
13
- <img src="https://github.com/noteflakes/syntropy/actions/workflows/test.yml/badge.svg" alt="Tests">
12
+ <a href="https://github.com/digital-fabric/syntropy/actions">
13
+ <img src="https://github.com/digital-fabric/syntropy/actions/workflows/test.yml/badge.svg" alt="Tests">
14
14
  </a>
15
- <a href="https://github.com/noteflakes/syntropy/blob/master/LICENSE">
15
+ <a href="https://github.com/digital-fabric/syntropy/blob/master/LICENSE">
16
16
  <img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License">
17
17
  </a>
18
18
  </p>
@@ -38,7 +38,7 @@ Syntropy is based on:
38
38
 
39
39
  - [UringMachine](https://github.com/digital-fabric/uringmachine) - a lean mean
40
40
  [io_uring](https://unixism.net/loti/what_is_io_uring.html) machine for Ruby.
41
- - [TP2](https://github.com/noteflakes/tp2) - an io_uring-based web server for
41
+ - [TP2](https://github.com/digital-fabric/tp2) - an io_uring-based web server for
42
42
  concurrent Ruby apps.
43
43
  - [Qeweney](https://github.com/digital-fabric/qeweney) a uniform interface for
44
44
  working with HTTP requests and responses.
@@ -46,6 +46,16 @@ Syntropy is based on:
46
46
  - [Extralite](https://github.com/digital-fabric/extralite) a fast and innovative
47
47
  SQLite wrapper for Ruby.
48
48
 
49
+ ## Examples
50
+
51
+ To get a taste of some of Syntropy's capabilities, you can run the included
52
+ examples site inside the Syntropy repository:
53
+
54
+ ```bash
55
+ $ cd syntropy
56
+ $ bundle exec syntropy -d examples
57
+ ```
58
+
49
59
  ## Routing
50
60
 
51
61
  Syntropy routes request by following the tree structure of the Syntropy app. A
@@ -93,6 +103,28 @@ Some conventions employed in Syntropy-based web apps:
93
103
  - The Syntrpy router accepts clean URLs for Ruby modules and Markdown files. It
94
104
  also accepts clean URLs for `index.html` files.
95
105
 
106
+ ## Running Syntropy
107
+
108
+ Note: Syntropy runs exclusively on Linux and requires kernel version >= 6.4.
109
+
110
+ To start a web server on the working directory, use the `syntropy` command:
111
+
112
+ ```bash
113
+ $ # install syntropy:
114
+ $ gem install syntropy
115
+ $ # run syntropy
116
+ $ syntropy path/to/my_site
117
+ ```
118
+
119
+ To get help for the different options available, run `syntropy -h`.
120
+
121
+ ## Development mode
122
+
123
+ When developing and making changes to your site, you can run Syntropy in
124
+ development mode, which automatically reloads changed modules and provides tools
125
+ to automatically refresh open web pages and debug HTML templates. To start
126
+ Syntropy in development mode, run `syntropy -d path/to/my_site`.
127
+
96
128
  ## What does a Syntropic Ruby module look like?
97
129
 
98
130
  Consider `site/archive.rb` in the file tree above. We want to get a list of
@@ -121,7 +153,7 @@ But a module can also be something completely different:
121
153
 
122
154
  ```ruby
123
155
  # api/v1.rb
124
- class APIV1 < Syntropy::RPCAPI
156
+ class APIV1 < Syntropy::JSONAPI
125
157
  def initialize(db)
126
158
  @db = db
127
159
  end
data/TODO.md CHANGED
@@ -22,6 +22,8 @@
22
22
  article.layout #=>
23
23
  article.render_proc #=> (load layout, apply article)
24
24
  article.render #=> (render to HTML)
25
+
26
+ # there should also be methods for creating, updating and deleting of articles/items.
25
27
  ...
26
28
  ```
27
29
 
@@ -30,6 +32,149 @@
30
32
  - [ ] support for caching headers
31
33
  - [ ] add `Request#render_static_file(route, fn)
32
34
 
35
+ - [ ] Serving of built-in assets (mostly JS)
36
+ - [ ] JS lib for RPC API
37
+
38
+ ## Missing for a first public release
39
+
40
+ - [ ] Logo
41
+ - [ ] Website
42
+ - [ ] Frontend part of RPC API
43
+ - [v] Auto-refresh page when file changes
44
+ - [ ] Examples
45
+ - [ ] Reactive app - counter or some other simple app showing interaction with server
46
+ - [ ] ?
47
+
48
+ ## Counter example
49
+
50
+ Here's a react component (from https://fresh.deno.dev/):
51
+
52
+ ```jsx
53
+ // islands/Counter.tsx
54
+ import { useSignal } from "@preact/signals";
55
+
56
+ export default function Counter(props) {
57
+ const count = useSignal(props.start);
58
+
59
+ return (
60
+ <div>
61
+ <h3>Interactive island</h3>
62
+ <p>The server supplied the initial value of {props.start}.</p>
63
+ <div>
64
+ <button onClick={() => count.value -= 1}>-</button>
65
+ <div>{count}</div>
66
+ <button onClick={() => count.value += 1}>+</button>
67
+ </div>
68
+ </div>
69
+ );
70
+ }
71
+ ```
72
+
73
+ How do we do this with Syntropy? Can we make a component that does the
74
+ templating and the reactivity in a single file? Can we wrap reactivity in a
75
+ component that has its own state? And where does the state live?
76
+
77
+ ```ruby
78
+ class Counter < Syntropy::Component
79
+ def initialize(start:, **props)
80
+ @count = reactive()
81
+ end
82
+
83
+ def template
84
+ div {
85
+ h3 'Interactive island'
86
+ p "The server supplied the initial value of #props[:start]}"
87
+ div {
88
+ button '-', on_click: -> { @count.value -= 1 }
89
+ div @count.value
90
+ button '+', on_click: -> { @count.value += 1 }
91
+ }
92
+ }
93
+ end
94
+ end
95
+ ```
96
+
97
+ Hmm, don't know if the complexity is worth it. It's an abstraction that's very
98
+ costly - both in terms of complexity of computation, and in terms of having a
99
+ clear mental model of what's happening under the hood.
100
+
101
+ I think a more logical approach is to stay with well-defined boundaries between
102
+ computation on the frontend and computation on the backend, and a having a clear
103
+ understanding of where things happen:
104
+
105
+ ```ruby
106
+ class Counter < Syntropy::Component
107
+ def incr
108
+ update(value: @props[:value] + 1)
109
+ end
110
+
111
+ def decr
112
+ update(value: @props[:value] - 1)
113
+ end
114
+
115
+ def template
116
+ div {
117
+ h3 'Interactive island'
118
+ div {
119
+ button '-', syn_click: 'decr'
120
+ div @props[:value]
121
+ button '+', syn_click: 'incr'
122
+ }
123
+ }
124
+ end
125
+ end
126
+ ```
127
+
128
+ Now, we can do all kinds of wrapping with scripts and ids and stuff to make this
129
+ work, but still, it would be preferable for the interactivity to be expressed in
130
+ JS. Maybe we just need a way to include a script that acts on the local HTML
131
+ code. How can we do this without writing a web component etc?
132
+
133
+ One way is to assign a random id to the template, then have a script that works
134
+ on it locally.
135
+
136
+ ```ruby
137
+ export template { |**props|
138
+ id = SecureRandom.hex(4)
139
+ div(id: id) {
140
+ h3 'Interactive island'
141
+ div {
142
+ button '-', syn_action: "decr"
143
+ div props[:value], syn_value: "value"
144
+ button '+', syn_action: "incr"
145
+ }
146
+ script <<~JS
147
+ const state = { value: #{props[:value]} }
148
+ const root = document.querySelector('##{id}')
149
+ const decr = root.querySelector('[syn-action="decr"]')
150
+ const incr = root.querySelector('[syn-action="incr"]')
151
+ const value = root.querySelector('[syn-value="value"]')
152
+ const updateValue = (v) => { state.value = v; value.innerText = String(v) }
153
+
154
+ decr.addEventListener('click', (e) => { updateValue(state.value - 1) })
155
+ incr.addEventListener('click', (e) => { updateValue(state.value + 1) })
156
+ JS
157
+ }
158
+ }
159
+ ```
160
+
161
+ How can we make this less verbose, less painful, less error-prone?
162
+
163
+ One way is to say - we don't worry about this on the backend, we just write
164
+ normal JS for the frontend and forget about the whole thing. Another way is to
165
+ provide a set of tools for making this less painful:
166
+
167
+ - Add some fancier abstractions on top of the JS RPC lib
168
+ - Add some template extensions that inject JS into the generated HTML
169
+
170
+ ## Testing facilities
171
+
172
+ - What do we need to test?
173
+ - Routes
174
+ - Route responses
175
+ - Changes to state / DB
176
+ -
177
+
33
178
  ## Support for applets
34
179
 
35
180
  - can be implemented as separate gems
data/bin/syntropy CHANGED
@@ -7,7 +7,8 @@ require 'optparse'
7
7
  env = {
8
8
  mount_path: '/',
9
9
  banner: Syntropy::BANNER,
10
- logger: true
10
+ logger: true,
11
+ builtin_applet_path: '/.syntropy'
11
12
  }
12
13
 
13
14
  parser = OptionParser.new do |o|
@@ -24,7 +25,8 @@ parser = OptionParser.new do |o|
24
25
  env[:logger] = nil
25
26
  end
26
27
 
27
- o.on('-w', '--watch', 'Watch for changed files') do
28
+ o.on('-d', '--dev', 'Development mode') do
29
+ env[:dev_mode] = true
28
30
  env[:watch_files] = 0.1
29
31
  end
30
32
 
@@ -33,8 +35,14 @@ parser = OptionParser.new do |o|
33
35
  exit
34
36
  end
35
37
 
36
- o.on('-m', '--mount', 'Set mount path (default: /)') do
37
- env[:mount_path] = it
38
+ o.on('-m', '--mount PATH', 'Set mount path (default: /)') do |path|
39
+ p mount: path
40
+ env[:mount_path] = path
41
+ env[:builtin_applet_path] = File.join(path, '.syntropy')
42
+ end
43
+
44
+ o.on('--no-builtin-applet', 'Do not mount builtin applet') do
45
+ env[:builtin_applet_path] = nil
38
46
  end
39
47
 
40
48
  o.on('-v', '--version', 'Show version') do
@@ -54,7 +62,7 @@ rescue StandardError => e
54
62
  exit
55
63
  end
56
64
 
57
- env[:root_dir] = ARGV.shift || '.'
65
+ env[:root_dir] = (ARGV.shift || '.').gsub(/\/$/, '')
58
66
 
59
67
  if !File.directory?(env[:root_dir])
60
68
  puts "#{File.expand_path(env[:root_dir])} Not a directory"
@@ -70,6 +78,7 @@ env[:machine] = Syntropy.machine = UM.new
70
78
  env[:logger] = env[:logger] && TP2::Logger.new(env[:machine], **env)
71
79
 
72
80
  require 'syntropy/version'
81
+ require 'syntropy/dev_mode' if env[:dev_mode]
73
82
 
74
83
  env[:logger]&.info(message: "Running Syntropy version #{Syntropy::VERSION}")
75
84
  app = Syntropy::App.load(env)
data/examples/card.rb ADDED
@@ -0,0 +1,12 @@
1
+ export template {
2
+ card {
3
+ h2 'Some card'
4
+ h3 'Some subtitle'
5
+
6
+ markdown <<~MARKDOWN
7
+ - Foo
8
+ - Bar
9
+ - Baz
10
+ MARKDOWN
11
+ }
12
+ }
@@ -0,0 +1,20 @@
1
+ import JSONAPI from '/.syntropy/json_api.js'
2
+
3
+ (() => {
4
+ const api = new JSONAPI('/counter_api');
5
+
6
+ const value = document.querySelector('#value');
7
+ const decr = document.querySelector('#decr');
8
+ const incr = document.querySelector('#incr');
9
+
10
+ decr.addEventListener('click', async () => {
11
+ const result = await api.post('decr')
12
+ value.innerText = String(result);
13
+ });
14
+
15
+ incr.addEventListener('click', async () => {
16
+ const result = await api.post('incr')
17
+ value.innerText = String(result);
18
+ });
19
+
20
+ })()
@@ -0,0 +1,23 @@
1
+ CounterAPI = import 'counter_api'
2
+
3
+ export template {
4
+ html5 {
5
+ body {
6
+ p { a '< Home', href: '/' }
7
+
8
+ h1 'Counter'
9
+
10
+ div {
11
+ button '-', id: 'decr'
12
+ value CounterAPI.value, id: 'value'
13
+ button '+', id: 'incr'
14
+ }
15
+ }
16
+ script src: '/counter.js', type: 'module'
17
+ style <<~CSS
18
+ div { font-weight: bold; font-size: 1.3em }
19
+ value { display: inline-block; padding: 0 1em; color: blue; width: 1em }
20
+ CSS
21
+ auto_refresh_watch!
22
+ }
23
+ }
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CounterAPI < Syntropy::JSONAPI
4
+ def initialize(env)
5
+ @env = env
6
+ @value = env[:counter_value] || 0
7
+ end
8
+
9
+ def value(req = nil)
10
+ @value
11
+ end
12
+
13
+ def incr!(req)
14
+ @value += 1
15
+ end
16
+
17
+ def decr!(req)
18
+ @value -= 1
19
+ end
20
+ end
21
+
22
+ export CounterAPI
File without changes
data/examples/index.md ADDED
@@ -0,0 +1,8 @@
1
+ ---
2
+ title: Syntropy Examples
3
+ ---
4
+
5
+ # Syntropy Examples
6
+
7
+ - [Template composition](/templates)
8
+ - [JSON API](/counter)
@@ -0,0 +1,40 @@
1
+ Card = import 'card'
2
+
3
+ export template {
4
+ html5 {
5
+ head {
6
+ style {
7
+ raw <<~CSS
8
+ div {
9
+ display: grid;
10
+ grid-template-columns: 1fr 1fr;
11
+ }
12
+ span.foo {
13
+ color: white;
14
+ background-color: blue;
15
+ padding: 1em;
16
+ }
17
+ span.bar {
18
+ color: white;
19
+ background-color: green;
20
+ padding: 1em;
21
+ }
22
+ CSS
23
+ }
24
+ }
25
+ body {
26
+ p { a '< Home', href: '/' }
27
+
28
+ h1 'Testing'
29
+
30
+ div {
31
+ span 'foo', class: 'foo'
32
+ span 'bar', class: 'bar'
33
+ }
34
+
35
+ Card()
36
+ }
37
+ auto_refresh_watch!
38
+ debug_template!
39
+ }
40
+ }
data/lib/syntropy/app.rb CHANGED
@@ -43,6 +43,7 @@ module Syntropy
43
43
  @root_dir = File.expand_path(env[:root_dir])
44
44
  @mount_path = env[:mount_path]
45
45
  @env = env
46
+ @logger = env[:logger]
46
47
 
47
48
  @module_loader = Syntropy::ModuleLoader.new(app: self, **env)
48
49
  setup_routing_tree
@@ -70,15 +71,22 @@ module Syntropy
70
71
  proc = route[:proc] ||= compute_route_proc(route)
71
72
  proc.(req)
72
73
  rescue StandardError => e
73
- @env[:logger]&.error(
74
- message: "Error while serving request",
74
+ @logger&.error(
75
+ message: "Error while serving request: #{e.message}",
75
76
  method: req.method,
76
- path: req.path
77
+ path: req.path,
78
+ error: e
77
79
  )
78
80
  error_handler = get_error_handler(route)
79
81
  error_handler.(req, e)
80
82
  end
81
83
 
84
+ def route(path, params = {}, compute_proc: false)
85
+ route = @router_proc.(path, params)
86
+ route[:proc] ||= compute_route_proc(route) if compute_proc
87
+ route
88
+ end
89
+
82
90
  private
83
91
 
84
92
  # Instantiates a routing tree with the app settings, and generates a router
@@ -89,9 +97,16 @@ module Syntropy
89
97
  @routing_tree = Syntropy::RoutingTree.new(
90
98
  root_dir: @root_dir, mount_path: @mount_path, **@env
91
99
  )
100
+ mount_builtin_applet if @env[:builtin_applet_path]
92
101
  @router_proc = @routing_tree.router_proc
93
102
  end
94
103
 
104
+ def mount_builtin_applet
105
+ path = @env[:builtin_applet_path]
106
+ @builtin_applet ||= Syntropy.builtin_applet(@env, mount_path: path)
107
+ @routing_tree.mount_applet(path, @builtin_applet)
108
+ end
109
+
95
110
  # Computes the route proc for the given route, wrapping it in hooks found up
96
111
  # the routing tree.
97
112
  #
@@ -146,9 +161,9 @@ module Syntropy
146
161
  if (layout = atts[:layout])
147
162
  route[:applied_layouts] ||= {}
148
163
  proc = route[:applied_layouts][layout] ||= markdown_layout_proc(layout)
149
- html = proc.render(md: md, **atts)
164
+ html = proc.render(md:, **atts)
150
165
  else
151
- html = P2.markdown(md)
166
+ html = default_markdown_layout_proc.render(md:, **atts)
152
167
  end
153
168
  html
154
169
  end
@@ -160,9 +175,22 @@ module Syntropy
160
175
  @layouts[layout] = template.apply { |md:, **| markdown(md) }
161
176
  end
162
177
 
178
+ def default_markdown_layout_proc
179
+ @default_markdown_layout ||= ->(md:, **atts) {
180
+ html5 {
181
+ head {
182
+ title atts[:title]
183
+ }
184
+ body {
185
+ markdown md
186
+ auto_refresh_watch! if @env[:dev_mode]
187
+ }
188
+ }
189
+ }
190
+ end
191
+
163
192
  def module_route_proc(route)
164
193
  ref = @routing_tree.fn_to_rel_path(route[:target][:fn])
165
- # ref = route[:target][:fn].sub(@mount_path, '')
166
194
  mod = @module_loader.load(ref)
167
195
  compute_module_proc(mod)
168
196
  end
@@ -171,31 +199,23 @@ module Syntropy
171
199
  case mod
172
200
  when P2::Template
173
201
  p2_template_proc(mod)
174
- when Papercraft::Template
175
- papercraft_template_proc(mod)
176
202
  else
177
203
  mod
178
204
  end
179
205
  end
180
206
 
181
207
  def p2_template_proc(template)
208
+ xml_mode = template.mode == :xml
182
209
  template = template.proc
183
- headers = { 'Content-Type' => 'text/html' }
210
+ mime_type = xml_mode ? 'text/xml; charset=UTF-8' : 'text/html; charset=UTF-8'
211
+ headers = { 'Content-Type' => mime_type }
184
212
 
185
- ->(req) {
186
- req.respond_by_http_method(
187
- 'head' => [nil, headers],
188
- 'get' => -> { [template.render, headers] }
189
- )
190
- }
191
- end
213
+ get_proc = xml_mode ? -> { [template.render_xml, headers] } : -> { [template.render, headers] }
192
214
 
193
- def papercraft_template_proc(template)
194
- headers = { 'Content-Type' => template.mime_type }
195
215
  ->(req) {
196
216
  req.respond_by_http_method(
197
217
  'head' => [nil, headers],
198
- 'get' => -> { [template.render, headers] }
218
+ 'get' => get_proc
199
219
  )
200
220
  }
201
221
  end
@@ -232,7 +252,7 @@ module Syntropy
232
252
  DEFAULT_ERROR_HANDLER = ->(req, err) {
233
253
  msg = err.message
234
254
  msg = nil if msg.empty? || (req.method == 'head')
235
- req.respond(msg, ':status' => Syntropy::Error.http_status(err))
255
+ req.respond(msg, ':status' => Syntropy::Error.http_status(err)) rescue nil
236
256
  }
237
257
 
238
258
  # Returns an error handler for the given route. If route is nil, looks up
@@ -286,7 +306,7 @@ module Syntropy
286
306
  # setup tasks
287
307
  @machine.sleep 0.2
288
308
  route_count = @routing_tree.static_map.size + @routing_tree.dynamic_map.size
289
- @env[:logger]&.info(
309
+ @logger&.info(
290
310
  message: "Serving from #{@root_dir} (#{route_count} routes found)"
291
311
  )
292
312
 
@@ -302,7 +322,7 @@ module Syntropy
302
322
  wf = @env[:watch_files]
303
323
  period = wf.is_a?(Numeric) ? wf : 0.1
304
324
  Syntropy.file_watch(@machine, @root_dir, period: period) do |event, fn|
305
- @env[:logger]&.info(message: "File change detected", fn: fn)
325
+ @logger&.info(message: 'File change detected', fn: fn)
306
326
  @module_loader.invalidate_fn(fn)
307
327
  debounce_file_change
308
328
  end
@@ -324,7 +344,23 @@ module Syntropy
324
344
  @machine.sleep(0.1)
325
345
  setup_routing_tree
326
346
  @routing_tree_reloader = nil
347
+ signal_auto_refresh_watchers!
327
348
  end
328
349
  end
350
+
351
+ def signal_auto_refresh_watchers!
352
+ return if !@builtin_applet
353
+
354
+ watcher_route_path = File.join(@env[:builtin_applet_path], 'auto_refresh/watch.sse')
355
+ watcher_route = @builtin_applet.route(watcher_route_path, compute_proc: true)
356
+
357
+ watcher_mod = watcher_route[:proc]
358
+ watcher_mod.signal!
359
+ rescue => e
360
+ @logger&.error(
361
+ message: 'Unexpected error while signalling auto refresh watcher',
362
+ error: e
363
+ )
364
+ end
329
365
  end
330
366
  end
@@ -0,0 +1,12 @@
1
+ (() => {
2
+ const jsURL = import.meta.url;
3
+ const sseURL = jsURL.replace(/\.js$/, '.sse');
4
+ const eventSource = new EventSource(sseURL);
5
+
6
+ eventSource.addEventListener('message', (msg) => {
7
+ if (msg.data != '') window.location.reload();
8
+ })
9
+ eventSource.addEventListener('error', () => {
10
+ console.log(`Failed to connect to auto refresh watcher (${sseURL})`);
11
+ })
12
+ })()