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

The core, compiled to JavaScript 5ac0521 · on 5ac0521f0d5672348e423d2b6c58f6f16a4137ae · nandi · 12h ago
frq_web.nim · 157 lines · 6.1 KBNim Blame HistoryRaw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
## The core, for a browser.
##
## `frq_core.nim` is the other one. That file is the only thing that knows
## about C; this is the only thing that knows about JavaScript, and the two
## have the same job: own the state and the screens, and hand a widget tree
## to whatever draws it.
##
## The seam is different because the host is. Across FFI a string is a pointer
## somebody has to free, and `frq_free` says so; here a string is a string.
## What is the same is the shape — a tree out, an event id back — and the
## polling, because the socket is somebody else's and nothing calls in.
##
## The platform modules underneath are chosen by search path: `nim/web/frq`
## comes before `nim/src/frq`, so `frq/conn` is the WebSocket queue rather
## than the socket threads, `frq/store` is localStorage rather than files,
## and so on. The shared code imports the same names either way and never
## learns which host it is on.

import std/[json, strutils, tables]
import frq/[cells, model, reducer, rooms, trace, ui]
import frq/conn as tr
import frq/oauth as oa
import frq/screens/connect as scConnectScreen
import frq/screens/chats as scChatsScreen
import frq/screens/chat as scChatScreen
import frq/screens/settings as scSettingsScreen

proc currentTree(): string =
  ## Whichever screen the state says. `drain` first, so the tree the host gets
  ## is built after every line that had arrived when it asked.
  drain()
  let connected = app.status.startsWith("Connected")
  let node =
    case app.screen
    of scChat: scChatScreen.chatScreen(app, connected)
    of scChats: scChatsScreen.chatsScreen(app, connected)
    of scDiscover: scSettingsScreen.discoverScreen(app)
    of scSettings: scSettingsScreen.settingsScreen(app, connected, true)
    of scConnect: scConnectScreen.connectScreen(app)
  $node.toJson

# ----------------------------------------------------------------- the seam
#
# Everything below is called from JavaScript and nothing else. `exportc` with
# a `frq` prefix rather than Nim's mangled names, so the host can call them by
# the names written here.

proc frqInit(payload: cstring) {.exportc.} =
  ## Once, before anything else. `payload` is the URL fragment this page came
  ## back with, or empty — a browser catches the broker's answer by being the
  ## page that was redirected, so a sign-in finishes here rather than on a
  ## loopback socket.
  restore()
  let p = $payload
  if p.len > 0: oa.handoff(p)

proc frqRender(): cstring {.exportc.} = currentTree().cstring
  ## The current screen as a widget tree, in JSON.

proc frqDispatch(event: cstring): cstring {.exportc.} =
  ## Apply an event and answer with the tree it produced — one call, so there
  ## is no window in which the host could draw a state nothing asked for.
  try:
    dispatch(parseJson($event))
  except CatchableError as e:
    trace("dispatch", "!! " & e.msg)
  currentTree().cstring

# --------------------------------------------------------------- the socket
#
# The host owns it. These four are the whole of the transport seam, and they
# are the two the tests already use plus the two a real connection needs.

proc frqWanted(): cstring {.exportc.} =
  ## Where the core is asking to be connected, as JSON, or empty where it is
  ## not asking. The host reads this after a dispatch and opens the socket.
  let cfg = tr.wanted()
  if cfg.host.len == 0: return "".cstring
  ($(%*{"host": cfg.host, "port": cfg.port, "tls": cfg.tls})).cstring

proc frqFeed(line: cstring) {.exportc.} = tr.feed($line)
  ## A line the server sent.

proc frqSocketEvent(e: cstring) {.exportc.} = tr.event($e)
  ## "open", or "close: why", or "error: why".

proc frqTakeOutbound(): cstring {.exportc.} =
  ## Everything the core wants to say, newline-separated, taken as it is read.
  ## One call rather than one per line: a registration is four lines and this
  ## is a crossing each.
  var lines: seq[string]
  while true:
    let (ok, line) = tr.tryOutbound()
    if not ok: break
    lines.add line
  lines.join("\n").cstring

# ----------------------------------------------------------------- sign-in

proc frqBrokerToken(): cstring {.exportc.} = app.brokerToken.cstring
  ## The remembered token, for the host to spend against the broker — `fetch`
  ## is asynchronous, so the core cannot spend it itself.

proc frqHandoff(payload: cstring) {.exportc.} = oa.handoff($payload)
  ## What the broker answered, whether from a redirect or from the host's own
  ## call to `/session`.

proc frqSignInFailed(reason: cstring) {.exportc.} = oa.failed($reason)

# ------------------------------------------------------------------- the rest

proc frqTrace(on: bool) {.exportc.} = trace.enabled = on
  ## Tracing has no environment to be switched on from here, so the console
  ## switches it: `frqTrace(true)`.

proc frqDemo() {.exportc.} =
  ## The same representative room `frq_core.frq_ui_demo` fills, for looking at
  ## the screens without a server.
  app = initState()
  app.formNick = "me"
  app.rooms.ensureRoom("#test")
  var r = app.rooms["#test"]
  r.joined = true
  r.users = {"me": "", "alice": "@", "bob": ""}.toTable
  r.topic = "a room"
  let t0 = 1_700_000_000_000'i64
  r.messages = @[
    Message(id: "1", frm: "*", text: "me joined #test", at: t0, system: true),
    Message(id: "2", frm: "alice", text: "hello there", at: t0 + 1000),
    Message(id: "3", frm: "bob", text: "answering", at: t0 + 2000,
            replyTo: "2"),
    Message(id: "4", frm: "me", text: "a line of my own", at: t0 + 3000)]
  app.rooms["#test"] = r
  app.current = "#test"
  app.screen = scChat
  app.status = "Connected as me"

# The host reaches these by name, so they are put where a name can be reached
# from: a `<script>` tag's globals are the browser's, and a module wrapper's
# are nobody's. One object rather than a scattering of globals, and the same
# object under node, which is what the smoke test drives.
{.emit: """
globalThis.frq = {
  init: frqInit,
  render: frqRender,
  dispatch: frqDispatch,
  wanted: frqWanted,
  feed: frqFeed,
  socketEvent: frqSocketEvent,
  takeOutbound: frqTakeOutbound,
  brokerToken: frqBrokerToken,
  handoff: frqHandoff,
  signInFailed: frqSignInFailed,
  trace: frqTrace,
  demo: frqDemo,
};
""".}