nandi/frqpublic Fork 0
3836fee
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

A screen asks what is known, not for it to be found out

`screens/chat` imported `profile`, which imported `atproto`, which
imported `httpclient` and `std/net`. So the UI could not be compiled for
a target without sockets — and it was a chain nothing needed: a screen
reads profiles, it never fetches one.

`profile` keeps the types, the cache and the lookups the screens use.
`profilefetch` does the going and getting, and is imported by the
reducer alone. The cache stays on the reading side, with `remember` and
`known` for the fetching side to reach it through.

`trace` writes through `echo` where there is no stderr, and has no
environment variable to read when there is no environment.

Both are the first steps of a web build — the whole of the UI layer
compiles to JavaScript once this chain is cut — but neither is only
that. The seam was in the wrong place on its own merits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-19T20:59:38-07:00 Browse files
3836fee parent: 07a23ef
modified nim/src/frq/profile.nim +10 -104
@@ -18,9 +18,9 @@
1818 ## The Clojure has a fetch seam here because the two compilers disagreed about
1919 ## HTTP. Nim has one client, so the seam is gone and `fetch` simply asks.
2020
21-import std/[exitprocs, json, strutils, tables]
21+import std/[json, strutils, tables]
2222 from std/unicode import runeLen, runeSubStr
23-import frq/[atproto, trace]
23+
2424
2525 type
2626 ProfileStatus* = enum
@@ -113,111 +113,19 @@ proc parseProfile*(body: JsonNode): Profile =
113113 follows: body{"followsCount"}.getInt(),
114114 posts: body{"postsCount"}.getInt())
115115
116+proc remember*(actor: string, p: Profile) =
117+ ## Put a profile in the cache. For `profilefetch`, which is the only thing
118+ ## that has one to put — the cache lives here because this is the side the
119+ ## screens read, and a screen must never reach the side that fetches.
120+ cache[actor] = p
121+
122+proc known*(actor: string): bool = cache.hasKey(actor)
123+
116124 proc entry*(actor: string): (Profile, bool) =
117125 ## What is known about this person right now, and whether anything is.
118126 if cache.hasKey(actor): (cache[actor], true)
119127 else: (Profile(), false)
120128
121-proc fetch*(actor: string) =
122- ## Ensure this person's profile is on its way.
123- ##
124- ## Blocking, and called from the reducer — which is the one place in this
125- ## program that can afford it: the render path must not, and the socket
126- ## threads have their own work. A profile is opened by a press, so the
127- ## reader is already waiting.
128- if actor.len == 0 or cache.hasKey(actor): return
129- cache[actor] = Profile(status: psLoading)
130- try:
131- let body = getProfile(actor)
132- cache[actor] = if body{"did"}.getStr().len > 0: parseProfile(body)
133- else: Profile(status: psFailed)
134- except CatchableError as e:
135- trace("profile", "could not fetch " & actor & ": " & e.msg)
136- cache[actor] = Profile(status: psFailed)
137-
138-# ------------------------------------------------------------ in the background
139-
140-# Faces are wanted for everyone in a room at once, and a profile is an HTTPS
141-# round trip each. Blocking was affordable for the panel — a reader presses a
142-# face and waits — and is not affordable for a room of twelve, on the thread
143-# that also answers every keystroke.
144-#
145-# So: a worker takes actors off one channel and puts answers on another, and
146-# `collect` folds them into the cache on the thread that owns it. The cache
147-# itself is never touched from two threads; only the channels are.
148-
149-var
150- requests: Channel[string]
151- answers: Channel[string] ## "<actor>\x1f<json>", the json empty on failure
152- fetcher: Thread[void]
153- fetching: bool
154-
155-var stopping: bool
156-
157-proc fetcherBody() {.thread.} =
158- {.gcsafe.}:
159- while true:
160- let actor = requests.recv()
161- if actor.len == 0 or stopping: break
162- var body = ""
163- try:
164- body = $getProfile(actor)
165- except CatchableError as e:
166- trace("profile", "could not fetch " & actor & ": " & e.msg)
167- answers.send(actor & "\x1f" & body)
168-
169-proc want*(actor: string) =
170- ## Ask for a profile, without waiting for it.
171- ##
172- ## Once per identity for the run: `psLoading` goes in the cache here, so a
173- ## second ask for the same person while the first is in flight is not a
174- ## second round trip.
175- if actor.len == 0 or cache.hasKey(actor) or isAgent(actor): return
176- cache[actor] = Profile(status: psLoading)
177- if not fetching:
178- fetching = true
179- createThread(fetcher, fetcherBody)
180- requests.send(actor)
181-
182-proc stopFetching() =
183- ## Stop the worker and wait for it, at exit.
184- ##
185- ## Not tidiness: a thread that is still running when the process tears down
186- ## is a thread calling `newContext` after OpenSSL has been unloaded under
187- ## it, which is a SIGSEGV inside `net.newContext` reported against whatever
188- ## ran last. The test suite found it the first time this landed.
189- ##
190- ## The sentinel is an empty actor. A request already in flight finishes
191- ## first — at worst the HTTP timeout, and in practice the round trip that
192- ## was already nearly done.
193- if not fetching: return
194- stopping = true
195- requests.send("")
196- joinThread(fetcher)
197- fetching = false
198-
199-addExitProc(stopFetching)
200-
201-proc collect*(): bool =
202- ## Fold whatever has come back into the cache. True where anything did, so
203- ## a caller knows the screen has something new on it.
204- while true:
205- let (ok, msg) = answers.tryRecv()
206- if not ok: break
207- let sep = msg.find('\x1f')
208- if sep < 0: continue
209- let actor = msg[0 ..< sep]
210- let body = msg[sep + 1 .. ^1]
211- cache[actor] =
212- if body.len == 0: Profile(status: psFailed)
213- else:
214- try:
215- let j = parseJson(body)
216- if j{"did"}.getStr().len > 0: parseProfile(j)
217- else: Profile(status: psFailed)
218- except CatchableError: Profile(status: psFailed)
219- result = true
220-
221129 proc avatarFor*(actor: string, alsoKnownAs = ""): string =
222130 ## The face to paint for this identity, or "" where there is not one yet.
223131 ## A lookup and never a fetch: this is called from the render path.
@@ -266,5 +174,3 @@ proc truncate*(s: string, max: int): string =
266174 let t = s.strip()
267175 if t.runeLen <= max: t else: t.runeSubStr(0, max - 1) & ""
268176
269-requests.open()
270-answers.open()
@@ -18,9 +18,9 @@
18 ## The Clojure has a fetch seam here because the two compilers disagreed about18 ## The Clojure has a fetch seam here because the two compilers disagreed about
19 ## HTTP. Nim has one client, so the seam is gone and `fetch` simply asks.19 ## HTTP. Nim has one client, so the seam is gone and `fetch` simply asks.
20 20
21-import std/[exitprocs, json, strutils, tables]21+import std/[json, strutils, tables]
22 from std/unicode import runeLen, runeSubStr22 from std/unicode import runeLen, runeSubStr
23-import frq/[atproto, trace]23+
24 24
25 type25 type
26 ProfileStatus* = enum26 ProfileStatus* = enum
@@ -113,111 +113,19 @@ proc parseProfile*(body: JsonNode): Profile =
113 follows: body{"followsCount"}.getInt(),113 follows: body{"followsCount"}.getInt(),
114 posts: body{"postsCount"}.getInt())114 posts: body{"postsCount"}.getInt())
115 115
116+proc remember*(actor: string, p: Profile) =
117+ ## Put a profile in the cache. For `profilefetch`, which is the only thing
118+ ## that has one to put — the cache lives here because this is the side the
119+ ## screens read, and a screen must never reach the side that fetches.
120+ cache[actor] = p
121+
122+proc known*(actor: string): bool = cache.hasKey(actor)
123+
116 proc entry*(actor: string): (Profile, bool) =124 proc entry*(actor: string): (Profile, bool) =
117 ## What is known about this person right now, and whether anything is.125 ## What is known about this person right now, and whether anything is.
118 if cache.hasKey(actor): (cache[actor], true)126 if cache.hasKey(actor): (cache[actor], true)
119 else: (Profile(), false)127 else: (Profile(), false)
120 128
121-proc fetch*(actor: string) =
122- ## Ensure this person's profile is on its way.
123- ##
124- ## Blocking, and called from the reducer — which is the one place in this
125- ## program that can afford it: the render path must not, and the socket
126- ## threads have their own work. A profile is opened by a press, so the
127- ## reader is already waiting.
128- if actor.len == 0 or cache.hasKey(actor): return
129- cache[actor] = Profile(status: psLoading)
130- try:
131- let body = getProfile(actor)
132- cache[actor] = if body{"did"}.getStr().len > 0: parseProfile(body)
133- else: Profile(status: psFailed)
134- except CatchableError as e:
135- trace("profile", "could not fetch " & actor & ": " & e.msg)
136- cache[actor] = Profile(status: psFailed)
137-
138-# ------------------------------------------------------------ in the background
139-
140-# Faces are wanted for everyone in a room at once, and a profile is an HTTPS
141-# round trip each. Blocking was affordable for the panel — a reader presses a
142-# face and waits — and is not affordable for a room of twelve, on the thread
143-# that also answers every keystroke.
144-#
145-# So: a worker takes actors off one channel and puts answers on another, and
146-# `collect` folds them into the cache on the thread that owns it. The cache
147-# itself is never touched from two threads; only the channels are.
148-
149-var
150- requests: Channel[string]
151- answers: Channel[string] ## "<actor>\x1f<json>", the json empty on failure
152- fetcher: Thread[void]
153- fetching: bool
154-
155-var stopping: bool
156-
157-proc fetcherBody() {.thread.} =
158- {.gcsafe.}:
159- while true:
160- let actor = requests.recv()
161- if actor.len == 0 or stopping: break
162- var body = ""
163- try:
164- body = $getProfile(actor)
165- except CatchableError as e:
166- trace("profile", "could not fetch " & actor & ": " & e.msg)
167- answers.send(actor & "\x1f" & body)
168-
169-proc want*(actor: string) =
170- ## Ask for a profile, without waiting for it.
171- ##
172- ## Once per identity for the run: `psLoading` goes in the cache here, so a
173- ## second ask for the same person while the first is in flight is not a
174- ## second round trip.
175- if actor.len == 0 or cache.hasKey(actor) or isAgent(actor): return
176- cache[actor] = Profile(status: psLoading)
177- if not fetching:
178- fetching = true
179- createThread(fetcher, fetcherBody)
180- requests.send(actor)
181-
182-proc stopFetching() =
183- ## Stop the worker and wait for it, at exit.
184- ##
185- ## Not tidiness: a thread that is still running when the process tears down
186- ## is a thread calling `newContext` after OpenSSL has been unloaded under
187- ## it, which is a SIGSEGV inside `net.newContext` reported against whatever
188- ## ran last. The test suite found it the first time this landed.
189- ##
190- ## The sentinel is an empty actor. A request already in flight finishes
191- ## first — at worst the HTTP timeout, and in practice the round trip that
192- ## was already nearly done.
193- if not fetching: return
194- stopping = true
195- requests.send("")
196- joinThread(fetcher)
197- fetching = false
198-
199-addExitProc(stopFetching)
200-
201-proc collect*(): bool =
202- ## Fold whatever has come back into the cache. True where anything did, so
203- ## a caller knows the screen has something new on it.
204- while true:
205- let (ok, msg) = answers.tryRecv()
206- if not ok: break
207- let sep = msg.find('\x1f')
208- if sep < 0: continue
209- let actor = msg[0 ..< sep]
210- let body = msg[sep + 1 .. ^1]
211- cache[actor] =
212- if body.len == 0: Profile(status: psFailed)
213- else:
214- try:
215- let j = parseJson(body)
216- if j{"did"}.getStr().len > 0: parseProfile(j)
217- else: Profile(status: psFailed)
218- except CatchableError: Profile(status: psFailed)
219- result = true
220-
221 proc avatarFor*(actor: string, alsoKnownAs = ""): string =129 proc avatarFor*(actor: string, alsoKnownAs = ""): string =
222 ## The face to paint for this identity, or "" where there is not one yet.130 ## The face to paint for this identity, or "" where there is not one yet.
223 ## A lookup and never a fetch: this is called from the render path.131 ## A lookup and never a fetch: this is called from the render path.
@@ -266,5 +174,3 @@ proc truncate*(s: string, max: int): string =
266 let t = s.strip()174 let t = s.strip()
267 if t.runeLen <= max: t else: t.runeSubStr(0, max - 1) & ""175 if t.runeLen <= max: t else: t.runeSubStr(0, max - 1) & ""
268 176
269-requests.open()
270-answers.open()
added nim/src/frq/profilefetch.nim +99 -0
new file mode 100644
@@ -0,0 +1,99 @@
1+## Going and getting a profile, off the thread that draws it.
2+##
3+## Split from `profile` because the screens read profiles and must not, by
4+## importing them, drag in an HTTP client: `screens/chat` reached `profile`,
5+## which reached `atproto`, which reached `httpclient` and `std/net`. That
6+## chain is why the UI could not be compiled for a target without sockets,
7+## and it was a chain nothing needed — a screen asks what is known, never for
8+## it to be found out.
9+##
10+## So this side does the finding out, and `profile` keeps the cache the
11+## screens read. On the web there is no thread and no `httpclient`; the host
12+## fetches and hands the answer to `profile.remember`.
13+
14+import std/[exitprocs, json, strutils]
15+import frq/[atproto, profile, trace]
16+
17+# Faces are wanted for everyone in a room at once, and a profile is an HTTPS
18+# round trip each. Blocking was affordable for the panel — a reader presses a
19+# face and waits — and is not affordable for a room of twelve, on the thread
20+# that also answers every keystroke.
21+#
22+# So: a worker takes actors off one channel and puts answers on another, and
23+# `collect` folds them into the cache on the thread that owns it. The cache
24+# itself is never touched from two threads; only the channels are.
25+
26+var
27+ requests: Channel[string]
28+ answers: Channel[string] ## "<actor>\x1f<json>", the json empty on failure
29+ fetcher: Thread[void]
30+ fetching: bool
31+
32+var stopping: bool
33+
34+proc fetcherBody() {.thread.} =
35+ {.gcsafe.}:
36+ while true:
37+ let actor = requests.recv()
38+ if actor.len == 0 or stopping: break
39+ var body = ""
40+ try:
41+ body = $getProfile(actor)
42+ except CatchableError as e:
43+ trace("profile", "could not fetch " & actor & ": " & e.msg)
44+ answers.send(actor & "\x1f" & body)
45+
46+proc want*(actor: string) =
47+ ## Ask for a profile, without waiting for it.
48+ ##
49+ ## Once per identity for the run: `psLoading` goes in the cache here, so a
50+ ## second ask for the same person while the first is in flight is not a
51+ ## second round trip.
52+ if actor.len == 0 or known(actor) or isAgent(actor): return
53+ remember(actor, Profile(status: psLoading))
54+ if not fetching:
55+ fetching = true
56+ createThread(fetcher, fetcherBody)
57+ requests.send(actor)
58+
59+proc stopFetching() =
60+ ## Stop the worker and wait for it, at exit.
61+ ##
62+ ## Not tidiness: a thread that is still running when the process tears down
63+ ## is a thread calling `newContext` after OpenSSL has been unloaded under
64+ ## it, which is a SIGSEGV inside `net.newContext` reported against whatever
65+ ## ran last. The test suite found it the first time this landed.
66+ ##
67+ ## The sentinel is an empty actor. A request already in flight finishes
68+ ## first — at worst the HTTP timeout, and in practice the round trip that
69+ ## was already nearly done.
70+ if not fetching: return
71+ stopping = true
72+ requests.send("")
73+ joinThread(fetcher)
74+ fetching = false
75+
76+addExitProc(stopFetching)
77+
78+proc collect*(): bool =
79+ ## Fold whatever has come back into the cache. True where anything did, so
80+ ## a caller knows the screen has something new on it.
81+ while true:
82+ let (ok, msg) = answers.tryRecv()
83+ if not ok: break
84+ let sep = msg.find('\x1f')
85+ if sep < 0: continue
86+ let actor = msg[0 ..< sep]
87+ let body = msg[sep + 1 .. ^1]
88+ remember(actor,
89+ if body.len == 0: Profile(status: psFailed)
90+ else:
91+ try:
92+ let j = parseJson(body)
93+ if j{"did"}.getStr().len > 0: parseProfile(j)
94+ else: Profile(status: psFailed)
95+ except CatchableError: Profile(status: psFailed))
96+ result = true
97+
98+requests.open()
99+answers.open()
new file mode 100644
@@ -0,0 +1,99 @@
1+## Going and getting a profile, off the thread that draws it.
2+##
3+## Split from `profile` because the screens read profiles and must not, by
4+## importing them, drag in an HTTP client: `screens/chat` reached `profile`,
5+## which reached `atproto`, which reached `httpclient` and `std/net`. That
6+## chain is why the UI could not be compiled for a target without sockets,
7+## and it was a chain nothing needed — a screen asks what is known, never for
8+## it to be found out.
9+##
10+## So this side does the finding out, and `profile` keeps the cache the
11+## screens read. On the web there is no thread and no `httpclient`; the host
12+## fetches and hands the answer to `profile.remember`.
13+
14+import std/[exitprocs, json, strutils]
15+import frq/[atproto, profile, trace]
16+
17+# Faces are wanted for everyone in a room at once, and a profile is an HTTPS
18+# round trip each. Blocking was affordable for the panel — a reader presses a
19+# face and waits — and is not affordable for a room of twelve, on the thread
20+# that also answers every keystroke.
21+#
22+# So: a worker takes actors off one channel and puts answers on another, and
23+# `collect` folds them into the cache on the thread that owns it. The cache
24+# itself is never touched from two threads; only the channels are.
25+
26+var
27+ requests: Channel[string]
28+ answers: Channel[string] ## "<actor>\x1f<json>", the json empty on failure
29+ fetcher: Thread[void]
30+ fetching: bool
31+
32+var stopping: bool
33+
34+proc fetcherBody() {.thread.} =
35+ {.gcsafe.}:
36+ while true:
37+ let actor = requests.recv()
38+ if actor.len == 0 or stopping: break
39+ var body = ""
40+ try:
41+ body = $getProfile(actor)
42+ except CatchableError as e:
43+ trace("profile", "could not fetch " & actor & ": " & e.msg)
44+ answers.send(actor & "\x1f" & body)
45+
46+proc want*(actor: string) =
47+ ## Ask for a profile, without waiting for it.
48+ ##
49+ ## Once per identity for the run: `psLoading` goes in the cache here, so a
50+ ## second ask for the same person while the first is in flight is not a
51+ ## second round trip.
52+ if actor.len == 0 or known(actor) or isAgent(actor): return
53+ remember(actor, Profile(status: psLoading))
54+ if not fetching:
55+ fetching = true
56+ createThread(fetcher, fetcherBody)
57+ requests.send(actor)
58+
59+proc stopFetching() =
60+ ## Stop the worker and wait for it, at exit.
61+ ##
62+ ## Not tidiness: a thread that is still running when the process tears down
63+ ## is a thread calling `newContext` after OpenSSL has been unloaded under
64+ ## it, which is a SIGSEGV inside `net.newContext` reported against whatever
65+ ## ran last. The test suite found it the first time this landed.
66+ ##
67+ ## The sentinel is an empty actor. A request already in flight finishes
68+ ## first — at worst the HTTP timeout, and in practice the round trip that
69+ ## was already nearly done.
70+ if not fetching: return
71+ stopping = true
72+ requests.send("")
73+ joinThread(fetcher)
74+ fetching = false
75+
76+addExitProc(stopFetching)
77+
78+proc collect*(): bool =
79+ ## Fold whatever has come back into the cache. True where anything did, so
80+ ## a caller knows the screen has something new on it.
81+ while true:
82+ let (ok, msg) = answers.tryRecv()
83+ if not ok: break
84+ let sep = msg.find('\x1f')
85+ if sep < 0: continue
86+ let actor = msg[0 ..< sep]
87+ let body = msg[sep + 1 .. ^1]
88+ remember(actor,
89+ if body.len == 0: Profile(status: psFailed)
90+ else:
91+ try:
92+ let j = parseJson(body)
93+ if j{"did"}.getStr().len > 0: parseProfile(j)
94+ else: Profile(status: psFailed)
95+ except CatchableError: Profile(status: psFailed))
96+ result = true
97+
98+requests.open()
99+answers.open()
modified nim/src/frq/reducer.nim +2 -1
@@ -14,7 +14,8 @@
1414 import std/[json, options, sequtils, strutils, tables]
1515 import std/sets
1616 import frq/[cells, model, rooms, reactions, trace, ircparse, clock,
17- atproto, handshake, textruns, members, msgsig, profile, store]
17+ atproto, handshake, textruns, members, msgsig, profile, store,
18+ profilefetch]
1819 import frq/conn as tr
1920 import frq/oauth as oa
2021
@@ -14,7 +14,8 @@
14 import std/[json, options, sequtils, strutils, tables]14 import std/[json, options, sequtils, strutils, tables]
15 import std/sets15 import std/sets
16 import frq/[cells, model, rooms, reactions, trace, ircparse, clock,16 import frq/[cells, model, rooms, reactions, trace, ircparse, clock,
17- atproto, handshake, textruns, members, msgsig, profile, store]17+ atproto, handshake, textruns, members, msgsig, profile, store,
18+ profilefetch]
18 import frq/conn as tr19 import frq/conn as tr
19 import frq/oauth as oa20 import frq/oauth as oa
20 21
modified nim/src/frq/trace.nim +16 -4
@@ -13,18 +13,30 @@
1313 ## string building — which matters because the argument to a trace call is
1414 ## usually the expensive part.
1515
16-import std/[os, strutils, times]
16+import std/[strutils, times]
17+when not defined(js):
18+ import std/os
1719
18-let enabled* = getEnv("FRQ_TRACE").len > 0 and getEnv("FRQ_TRACE") != "0"
20+when defined(js):
21+ # No environment to read, and no stderr to write to. A web build traces
22+ # through the console, and turns it on from there rather than from a
23+ # variable set before the page loaded.
24+ var enabled* = false
25+else:
26+ let enabled* = getEnv("FRQ_TRACE").len > 0 and getEnv("FRQ_TRACE") != "0"
1927
2028 proc trace*(topic: string, msg: string) =
2129 ## One line: a timestamp, a topic, and the message.
2230 if not enabled: return
2331 let t = now().format("HH:mm:ss'.'fff")
24- stderr.writeLine("[frq " & t & "] " & topic.alignLeft(9) & " " & msg)
32+ let line = "[frq " & t & "] " & topic.alignLeft(9) & " " & msg
33+ when defined(js):
34+ echo line
35+ else:
36+ stderr.writeLine(line)
2537 # Flushed every line rather than at exit: a trace lost when the process dies
2638 # is worth nothing, and the process dying is the case most worth tracing.
27- stderr.flushFile()
39+ when not defined(js): stderr.flushFile()
2840
2941 template traced*(topic: string, body: untyped) =
3042 ## For a message that costs something to build. The body is not evaluated
@@ -13,18 +13,30 @@
13 ## string building — which matters because the argument to a trace call is13 ## string building — which matters because the argument to a trace call is
14 ## usually the expensive part.14 ## usually the expensive part.
15 15
16-import std/[os, strutils, times]16+import std/[strutils, times]
17+when not defined(js):
18+ import std/os
17 19
18-let enabled* = getEnv("FRQ_TRACE").len > 0 and getEnv("FRQ_TRACE") != "0"20+when defined(js):
21+ # No environment to read, and no stderr to write to. A web build traces
22+ # through the console, and turns it on from there rather than from a
23+ # variable set before the page loaded.
24+ var enabled* = false
25+else:
26+ let enabled* = getEnv("FRQ_TRACE").len > 0 and getEnv("FRQ_TRACE") != "0"
19 27
20 proc trace*(topic: string, msg: string) =28 proc trace*(topic: string, msg: string) =
21 ## One line: a timestamp, a topic, and the message.29 ## One line: a timestamp, a topic, and the message.
22 if not enabled: return30 if not enabled: return
23 let t = now().format("HH:mm:ss'.'fff")31 let t = now().format("HH:mm:ss'.'fff")
24- stderr.writeLine("[frq " & t & "] " & topic.alignLeft(9) & " " & msg)32+ let line = "[frq " & t & "] " & topic.alignLeft(9) & " " & msg
33+ when defined(js):
34+ echo line
35+ else:
36+ stderr.writeLine(line)
25 # Flushed every line rather than at exit: a trace lost when the process dies37 # Flushed every line rather than at exit: a trace lost when the process dies
26 # is worth nothing, and the process dying is the case most worth tracing.38 # is worth nothing, and the process dying is the case most worth tracing.
27- stderr.flushFile()39+ when not defined(js): stderr.flushFile()
28 40
29 template traced*(topic: string, body: untyped) =41 template traced*(topic: string, body: untyped) =
30 ## For a message that costs something to build. The body is not evaluated42 ## For a message that costs something to build. The body is not evaluated