| The core, compiled to JavaScript 5ac0521 nandi 10h ago | 1 | ## The core, for a browser. |
| 2 | ## |
| 3 | ## `frq_core.nim` is the other one. That file is the only thing that knows |
| 4 | ## about C; this is the only thing that knows about JavaScript, and the two |
| 5 | ## have the same job: own the state and the screens, and hand a widget tree |
| 6 | ## to whatever draws it. |
| 7 | ## |
| 8 | ## The seam is different because the host is. Across FFI a string is a pointer |
| 9 | ## somebody has to free, and `frq_free` says so; here a string is a string. |
| 10 | ## What is the same is the shape — a tree out, an event id back — and the |
| 11 | ## polling, because the socket is somebody else's and nothing calls in. |
| 12 | ## |
| 13 | ## The platform modules underneath are chosen by search path: `nim/web/frq` |
| 14 | ## comes before `nim/src/frq`, so `frq/conn` is the WebSocket queue rather |
| 15 | ## than the socket threads, `frq/store` is localStorage rather than files, |
| 16 | ## and so on. The shared code imports the same names either way and never |
| 17 | ## learns which host it is on. |
| 18 | |
| 19 | import std/[json, strutils, tables] |
| 20 | import frq/[cells, model, reducer, rooms, trace, ui] |
| 21 | import frq/conn as tr |
| 22 | import frq/oauth as oa |
| 23 | import frq/screens/connect as scConnectScreen |
| 24 | import frq/screens/chats as scChatsScreen |
| 25 | import frq/screens/chat as scChatScreen |
| 26 | import frq/screens/settings as scSettingsScreen |
| 27 | |
| 28 | proc currentTree(): string = |
| 29 | ## Whichever screen the state says. `drain` first, so the tree the host gets |
| 30 | ## is built after every line that had arrived when it asked. |
| 31 | drain() |
| 32 | let connected = app.status.startsWith("Connected") |
| 33 | let node = |
| 34 | case app.screen |
| 35 | of scChat: scChatScreen.chatScreen(app, connected) |
| 36 | of scChats: scChatsScreen.chatsScreen(app, connected) |
| 37 | of scDiscover: scSettingsScreen.discoverScreen(app) |
| 38 | of scSettings: scSettingsScreen.settingsScreen(app, connected, true) |
| 39 | of scConnect: scConnectScreen.connectScreen(app) |
| 40 | $node.toJson |
| 41 | |
| 42 | # ----------------------------------------------------------------- the seam |
| 43 | # |
| 44 | # Everything below is called from JavaScript and nothing else. `exportc` with |
| 45 | # a `frq` prefix rather than Nim's mangled names, so the host can call them by |
| 46 | # the names written here. |
| 47 | |
| 48 | proc frqInit(payload: cstring) {.exportc.} = |
| 49 | ## Once, before anything else. `payload` is the URL fragment this page came |
| 50 | ## back with, or empty — a browser catches the broker's answer by being the |
| 51 | ## page that was redirected, so a sign-in finishes here rather than on a |
| 52 | ## loopback socket. |
| 53 | restore() |
| 54 | let p = $payload |
| 55 | if p.len > 0: oa.handoff(p) |
| 56 | |
| 57 | proc frqRender(): cstring {.exportc.} = currentTree().cstring |
| 58 | ## The current screen as a widget tree, in JSON. |
| 59 | |
| 60 | proc frqDispatch(event: cstring): cstring {.exportc.} = |
| 61 | ## Apply an event and answer with the tree it produced — one call, so there |
| 62 | ## is no window in which the host could draw a state nothing asked for. |
| 63 | try: |
| 64 | dispatch(parseJson($event)) |
| 65 | except CatchableError as e: |
| 66 | trace("dispatch", "!! " & e.msg) |
| 67 | currentTree().cstring |
| 68 | |
| 69 | # --------------------------------------------------------------- the socket |
| 70 | # |
| 71 | # The host owns it. These four are the whole of the transport seam, and they |
| 72 | # are the two the tests already use plus the two a real connection needs. |
| 73 | |
| 74 | proc frqWanted(): cstring {.exportc.} = |
| 75 | ## Where the core is asking to be connected, as JSON, or empty where it is |
| 76 | ## not asking. The host reads this after a dispatch and opens the socket. |
| 77 | let cfg = tr.wanted() |
| 78 | if cfg.host.len == 0: return "".cstring |
| 79 | ($(%*{"host": cfg.host, "port": cfg.port, "tls": cfg.tls})).cstring |
| 80 | |
| 81 | proc frqFeed(line: cstring) {.exportc.} = tr.feed($line) |
| 82 | ## A line the server sent. |
| 83 | |
| 84 | proc frqSocketEvent(e: cstring) {.exportc.} = tr.event($e) |
| 85 | ## "open", or "close: why", or "error: why". |
| 86 | |
| 87 | proc frqTakeOutbound(): cstring {.exportc.} = |
| 88 | ## Everything the core wants to say, newline-separated, taken as it is read. |
| 89 | ## One call rather than one per line: a registration is four lines and this |
| 90 | ## is a crossing each. |
| 91 | var lines: seq[string] |
| 92 | while true: |
| 93 | let (ok, line) = tr.tryOutbound() |
| 94 | if not ok: break |
| 95 | lines.add line |
| 96 | lines.join("\n").cstring |
| 97 | |
| 98 | # ----------------------------------------------------------------- sign-in |
| 99 | |
| 100 | proc frqBrokerToken(): cstring {.exportc.} = app.brokerToken.cstring |
| 101 | ## The remembered token, for the host to spend against the broker — `fetch` |
| 102 | ## is asynchronous, so the core cannot spend it itself. |
| 103 | |
| 104 | proc frqHandoff(payload: cstring) {.exportc.} = oa.handoff($payload) |
| 105 | ## What the broker answered, whether from a redirect or from the host's own |
| 106 | ## call to `/session`. |
| 107 | |
| 108 | proc frqSignInFailed(reason: cstring) {.exportc.} = oa.failed($reason) |
| 109 | |
| 110 | # ------------------------------------------------------------------- the rest |
| 111 | |
| 112 | proc frqTrace(on: bool) {.exportc.} = trace.enabled = on |
| 113 | ## Tracing has no environment to be switched on from here, so the console |
| 114 | ## switches it: `frqTrace(true)`. |
| 115 | |
| 116 | proc frqDemo() {.exportc.} = |
| 117 | ## The same representative room `frq_core.frq_ui_demo` fills, for looking at |
| 118 | ## the screens without a server. |
| 119 | app = initState() |
| 120 | app.formNick = "me" |
| 121 | app.rooms.ensureRoom("#test") |
| 122 | var r = app.rooms["#test"] |
| 123 | r.joined = true |
| 124 | r.users = {"me": "", "alice": "@", "bob": ""}.toTable |
| 125 | r.topic = "a room" |
| 126 | let t0 = 1_700_000_000_000'i64 |
| 127 | r.messages = @[ |
| 128 | Message(id: "1", frm: "*", text: "me joined #test", at: t0, system: true), |
| 129 | Message(id: "2", frm: "alice", text: "hello there", at: t0 + 1000), |
| 130 | Message(id: "3", frm: "bob", text: "answering", at: t0 + 2000, |
| 131 | replyTo: "2"), |
| 132 | Message(id: "4", frm: "me", text: "a line of my own", at: t0 + 3000)] |
| 133 | app.rooms["#test"] = r |
| 134 | app.current = "#test" |
| 135 | app.screen = scChat |
| 136 | app.status = "Connected as me" |
| 137 | |
| 138 | # The host reaches these by name, so they are put where a name can be reached |
| 139 | # from: a `<script>` tag's globals are the browser's, and a module wrapper's |
| 140 | # are nobody's. One object rather than a scattering of globals, and the same |
| 141 | # object under node, which is what the smoke test drives. |
| 142 | {.emit: """ |
| 143 | globalThis.frq = { |
| 144 | init: frqInit, |
| 145 | render: frqRender, |
| 146 | dispatch: frqDispatch, |
| 147 | wanted: frqWanted, |
| 148 | feed: frqFeed, |
| 149 | socketEvent: frqSocketEvent, |
| 150 | takeOutbound: frqTakeOutbound, |
| 151 | brokerToken: frqBrokerToken, |
| 152 | handoff: frqHandoff, |
| 153 | signInFailed: frqSignInFailed, |
| 154 | trace: frqTrace, |
| 155 | demo: frqDemo, |
| 156 | }; |
| 157 | """.} |