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>
3836fee parent: 07a23ef modified
nim/src/frq/profile.nim +10 -104 | @@ -18,9 +18,9 @@ | ||
| 18 | 18 | ## The Clojure has a fetch seam here because the two compilers disagreed about |
| 19 | 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 | 22 | from std/unicode import runeLen, runeSubStr |
| 23 | -import frq/[atproto, trace] | |
| 23 | + | |
| 24 | 24 | |
| 25 | 25 | type |
| 26 | 26 | ProfileStatus* = enum |
| @@ -113,111 +113,19 @@ proc parseProfile*(body: JsonNode): Profile = | ||
| 113 | 113 | follows: body{"followsCount"}.getInt(), |
| 114 | 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 | 124 | proc entry*(actor: string): (Profile, bool) = |
| 117 | 125 | ## What is known about this person right now, and whether anything is. |
| 118 | 126 | if cache.hasKey(actor): (cache[actor], true) |
| 119 | 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 | 129 | proc avatarFor*(actor: string, alsoKnownAs = ""): string = |
| 222 | 130 | ## The face to paint for this identity, or "" where there is not one yet. |
| 223 | 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 | 174 | let t = s.strip() |
| 267 | 175 | if t.runeLen <= max: t else: t.runeSubStr(0, max - 1) & "…" |
| 268 | 176 | |
| 269 | -requests.open() | |
| 270 | -answers.open() | |
| @@ -18,9 +18,9 @@ | |||
| 18 | ## The Clojure has a fetch seam here because the two compilers disagreed about | 18 | ## 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, runeSubStr | 22 | from std/unicode import runeLen, runeSubStr |
| 23 | -import frq/[atproto, trace] | 23 | + |
| 24 | 24 | ||
| 25 | type | 25 | type |
| 26 | ProfileStatus* = enum | 26 | 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 @@ | ||
| 14 | 14 | import std/[json, options, sequtils, strutils, tables] |
| 15 | 15 | import std/sets |
| 16 | 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 | 19 | import frq/conn as tr |
| 19 | 20 | import frq/oauth as oa |
| 20 | 21 | |
| @@ -14,7 +14,8 @@ | |||
| 14 | import std/[json, options, sequtils, strutils, tables] | 14 | import std/[json, options, sequtils, strutils, tables] |
| 15 | import std/sets | 15 | 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 tr | 19 | import frq/conn as tr |
| 19 | import frq/oauth as oa | 20 | import frq/oauth as oa |
| 20 | 21 | ||
modified
nim/src/frq/trace.nim +16 -4 | @@ -13,18 +13,30 @@ | ||
| 13 | 13 | ## string building — which matters because the argument to a trace call is |
| 14 | 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 | 28 | proc trace*(topic: string, msg: string) = |
| 21 | 29 | ## One line: a timestamp, a topic, and the message. |
| 22 | 30 | if not enabled: return |
| 23 | 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 | 37 | # Flushed every line rather than at exit: a trace lost when the process dies |
| 26 | 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 | 41 | template traced*(topic: string, body: untyped) = |
| 30 | 42 | ## 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 is | 13 | ## 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: return | 30 | 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 dies | 37 | # 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 evaluated | 42 | ## For a message that costs something to build. The body is not evaluated |