nandi/frqpublic Fork 0
0e7cd322e9fe4d04d215501ceb97171001c09cc2
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.

frq_web.nim · 157 lines · 6.1 KBNim Blame HistoryRaw
The core, compiled to JavaScript 5ac0521 nandi 11h ago1## 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
19import std/[json, strutils, tables]
20import frq/[cells, model, reducer, rooms, trace, ui]
21import frq/conn as tr
22import frq/oauth as oa
23import frq/screens/connect as scConnectScreen
24import frq/screens/chats as scChatsScreen
25import frq/screens/chat as scChatScreen
26import frq/screens/settings as scSettingsScreen
27
28proc 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
48proc 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
57proc frqRender(): cstring {.exportc.} = currentTree().cstring
58 ## The current screen as a widget tree, in JSON.
59
60proc 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
74proc 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
81proc frqFeed(line: cstring) {.exportc.} = tr.feed($line)
82 ## A line the server sent.
83
84proc frqSocketEvent(e: cstring) {.exportc.} = tr.event($e)
85 ## "open", or "close: why", or "error: why".
86
87proc 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
100proc 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
104proc 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
108proc frqSignInFailed(reason: cstring) {.exportc.} = oa.failed($reason)
109
110# ------------------------------------------------------------------- the rest
111
112proc 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
116proc 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: """
143globalThis.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""".}