# How to add a task and event to the example chat client ## Background At the time of writing (commit [#0e4fd872](https://github.com/status-im/nim-status/commit/0e4fd872fa31e8cf9486be9ad506379fd483772c)), `nim-status` has several main features: 1. Wraps two `nim-sqlcipher` databases (one is unencrypted) with CRUD APIs for database objects 2. Handles communication with mailservers for messaging functionality 3. Wraps calls to an upstream web3 provider for Ethereum-centric functionality Internal to the library, `nim-status` handles all three of these features internally in a single thread, with plans to incorporate async operations via `nim-chronos`'s event loop. Consuming these functions synchronously is not a good idea due to the CPU-intense and heavy use of cryptographic mechanisms, long response times due to network I/O, and the synchronous nature of sqlcipher database operations. Therefore, consumers of this library should **not** consume `nim-status` in a synchronous manner. Consumers of `nim-status` should instead consume `nim-status` in a completely asychronous manner, ideally using threads in a thread pool for each task. This can be done using `nim-task-runner` as a tool for executing tasks in a thread pool, or for long-running tasks that should reside in a separate thread. Note that consumers of `nim-task-runner` must use `nim-chronos` as their async event loop. ## Example Chat Client The [Example Chat Client in `nim-status`](https://github.com/status-im/nim-status/tree/master/examples/chat) show cases how to use `nim-task-runner` to run `nim-status` tasks in their own thread, and how to receive the results of these tasks asynchronously. The main purpose of the chat client is to serve as an example of how consumers of `nim-status` should consume `nim-status`. Having an example chat client also helps to dogfood `nim-status`. This chat client example has a TUI (Terminal User Interface) that allows an end user to run commands, such as `/create`, which creates a new Status account. These commands are turned in to tasks for use with the thread pool in`nim-task-runner`, which then executes the tasks. The goal for these tasks is to execute the task off the calling thread (the "host"), so as not to block that thread. Once the task is complete, an `Event` is created, and sent back to the TUI (the "host") via the task runner, executing the event on the TUI's thread, which is listening for task runner messages. For each `nim-status` call that needs to be made in the Example Chat Client, the operation must be packaged up in to a task, sent to the task runner for execution in a thread in the thread pool (or in an existing long-running task). If there needs to be a response, or if there is communication that needs to occur from the task runner back to the chat client, such as a notice from long-running operation, a `ClientEvent` needs to be created and sent from the task runner to the client. These events will be executed on the TUI's thread. ### Example Chat Client - Creating a TUI "command" and an "action" For each `nim-status` operation, we need to create a task to be executed in the task runner. Given that the Example Chat Client is a TUI, these tasks are called "commands", each of which inherits from `Command`. Typically, each task/command has a response, so we can do things like display results of a `Command` on screen for the user. Therefore, we need to communicate back from the task runner to the TUI. To do so, we need to create an "action" that the TUI responds, each of which inherits from `ClientEvent`. In summary, to execute a full-circle input from the TUI that consumes `nim-status` and displays a result on screen, we need to create a `Command` and a `ClientEvent` and run them via the task runner. Here, we will walk through how the `/createaccount <password>` command was created. This command is responsible for creating a Status account in `nim-status` and displays the created account on screen after it's complete. The password is used to encrypt the keystore stored on disk. The command is aliased to `/create` for convenience. #### Creating the `/createaccount <password>` "command" First, a `CreateAccount` type has been created in [`examples/chat/tui/common.nim`](https://github.com/status-im/nim-status/blob/0e4fd872fa31e8cf9486be9ad506379fd483772c/examples/chat/tui/common.nim#L46-L47). ```nim type CreateAccount* = ref object of Command password*: string ``` Notice that `CreateAccount` inherits from the `Command` type. The `password` field is a TUI command line argument and needs to be included in the type. Each command line argument will need to be included as a field in the created command type. Next, we created four functions in [`examples/chat/tui/commands.nim`](https://github.com/status-im/nim-status/blob/0e4fd872fa31e8cf9486be9ad506379fd483772c/examples/chat/tui/commands.nim#L40-L53) that utilise the `CreateAccount` command. Note, at the time of writing, the `help()` proc hadn't yet made its way in to `master`, though that will likely change shortly (hopefully by the time you read this). | Function | Parameters | Returns | Description | | -------- | ---------- | ------- | ----------- | | `split` | `T: type CreateAccount` - creates a unique signature for `split()`<br>`argsRaw: string` - raw, unsplit arguments typed in after `/createaccount` in the TUI | `seq[string]` - sequence of command line arguments, which should only contain one item for the entered password | Handles parsing the text after `/createaccount` and turning it in to an array of arguments to be used in the `new` function. | | `new` | `T: type CreateAccount` - creates a unique signature for `new()`<br>`args: varargs[string]` - arguments typed in after `/createaccount` in the TUI, as split in the `split` proc | `CreateAccount` - newly instantiated `CreateAccount` object | Handles instatiating the `CreateAccount` object. | | `command` | `self: ChatTUI` - the instantiated `ChatTUI` object `split`<br>`command: CreateAccount` - contains the instantiated `CreateAccount` object, created in the `new()` proc | `void` | Executes the `/createaccount` command. | | `help` | `T: type Print` - creates a unique signature for `help()` | `HelpText` - name and description for the command to be displayed when the `/help` command is issues. Also contains arguments names and descriptions | Returns a `HelpText` object with command name and description, as well as the name and description of each command line arguments used in this command. | The first three procs are automatically called by the TUI parser, while the `help()` proc is called by a macro executed during the `/help` command's routine. They look like the following: ```nim # CreateAccount ---------------------------------------------------------------- proc help*(T: type CreateAccount): HelpText = let command = "createaccount" HelpText(command: command, aliases: aliased[command], parameters: @[ CommandParameter(name: "password", description: "Password for the new " & "account.") ], description: "Creates a new Status account.") proc new*(T: type CreateAccount, args: varargs[string]): T = T(password: args[0]) proc split*(T: type CreateAccount, argsRaw: string): seq[string] = @[argsRaw] proc command*(self: ChatTUI, command: CreateAccount) {.async, gcsafe, nimcall.} = if command.password == "": self.wprintFormatError(epochTime().int64, "password cannot be blank, please provide a password as the first argument.") else: asyncSpawn self.client.generateMultiAccount(command.password) ``` Notice in the `command` proc that we are printing an error on the screen in the case of invalid command line args passed in. It is worth noting that commands that do not need to make CPU- or IO-intense calls can simply print to the screen here. Anything that executes in under 16ms should be OK here. However, as a general rule of thumb, calls to `nim-status` should make their way in to the task runner so they avoid blocking the calling thread. Here, we call `self.client.generateMultiAccount(command.password)` which needs to be created in [`examples/chat/client.nim`](https://github.com/status-im/nim-status/blob/0e4fd872fa31e8cf9486be9ad506379fd483772c/examples/chat/client.nim#L54): ```nim proc generateMultiAccount*(self: ChatClient, password: string) {.async.} = asyncSpawn generateMultiAccount(self.taskRunner, status, password) ``` This function is run asynchronously spawns a call to `generateMultiAccount`, which is a task defined in [`chat/client/tasks.nim`](https://github.com/status-im/nim-status/blob/0e4fd872fa31e8cf9486be9ad506379fd483772c/examples/chat/client/tasks.nim#L81-L111). `asyncSpawn` is used here as an asychronous "fire and forget", where we do not need to `await` a result of `generateMultiAccount`. If we did need to wait for a result, we would ensure that we would use the `kind=rts` parameter in the task pragma `{.task(kind=rts).}` on our task definition (see the task definition below). Keep in mind that this function is run on the TUI ("host") thread, so be careful not to introduce blocking code here that would effectively block the host thread. We want to keep it responsive. The parameter `status` lives in [the task runner context](https://github.com/status-im/nim-status/blob/0e4fd872fa31e8cf9486be9ad506379fd483772c/examples/chat/client/tasks.nim#L47) of the client. It is a type of `StatusObject` and serves "an instantiated `nim-status` object". We can use this value from the context (or any value in the context) in our task definitions. Next, we wrote the task definition that creates the account in `nim-status` and then fires a `ClientEvent` which is the TUI is listening for and will serve to print on screen. It's suprisingly very easy. First, create a `CreateAccountResult` type in [`chat/client/events`](https://github.com/status-im/nim-status/blob/0e4fd872fa31e8cf9486be9ad506379fd483772c/examples/chat/client/events.nim#L12-L14): ```nim type CreateAccountResult* = ref object of ClientEvent account*: Account timestamp*: int64 ``` Notice that `CreateAccountResult` inherits from `ClientEvent`. The first field, `Account` is a `nim-status` account that contains the public account information that we want to display on screen after the task is executed. Additionally, we needed to add the `CreateAccountResult` in the form of a string [to the `clientEvents` array](https://github.com/status-im/nim-status/blob/0e4fd872fa31e8cf9486be9ad506379fd483772c/examples/chat/client/events.nim#L29): ```nim const clientEvents* = [ "CreateAccountResult", # <==== added here "ListAccountsResult", "NetworkStatus", "UserMessage" ] ``` There is a macro that uses this array to generate a `case of` statement to execute the correct `action()` which will get to at the end. Next, we created the task defintion. ```nim proc generateMultiAccount*(password: string) {.task(kind=no_rts, stoppable=false).} = let paths = @[PATH_WALLET_ROOT, PATH_EIP_1581, PATH_WHISPER, PATH_DEFAULT_WALLET] multiAccounts = status.multiAccountGenerateAndDeriveAddresses(12, 1, password, paths) multiAccount = multiAccounts[0] dir = status.dataDir / "keystore" dir.createDir() status.multiAccountStoreDerivedAccounts(multiAccount, password, dir) let timestamp = epochTime().int64 whisperAcct = multiAccount.accounts[2] account = accounts.Account( name: whisperAcct.publicKey.generateAlias(), identicon: whisperAcct.publicKey.identicon(), keycardPairing: "", keyUid: whisperAcct.keyUid # whisper key-uid ) status.saveAccount(account) let event = CreateAccountResult(account: account, timestamp: timestamp) eventEnc = event.encode task = taskArg.taskName trace "task sent event to host", event=eventEnc, task asyncSpawn chanSendToHost.send(eventEnc.safe) ``` Notice that this proc is decorated with the `{.task.}` prama. This pragma rewrites the task proc in a way that is compatible with the task runner. This proc is being run within the context of the task runner. It is run on a thread inside of the thread pool. Everything this task touches must be completely thread safe. Our job as developers of this task, is to execute what we need to execute in the task (i.e. execute a routine in `nim-status`) and then create a `ClientEvent` to return to the host (which is the example client) through the task runner. `status.saveAccount` and `status.multiAccountStoreDerivedAccounts` executes the procs on the `status` object, which is the instantiated `nim-status` `StatusObject` that is in our task context. After the `nim-satus` tasks are done, we instantiated a `CreateAccountResult` object which conains information we want to pass back to the "host", which is the client in our case. Once the host receives this event, we can print it on screen. #### Create the `CreateAccountResult` "action" Next, [we created an `action` that accepts the `CreateAccountResult` created in our task](https://github.com/status-im/nim-status/blob/0e4fd872fa31e8cf9486be9ad506379fd483772c/examples/chat/tui/actions.nim#L99-L109) and prints our desired output in the TUI. Keep in mind, this action is run on the TUI ("host") thread, so care should be taken not to introduce blocking operations here. It is outside the context of the task runner. ```nim # CreateAccountResult ---------------------------------------------------------- proc action*(self: ChatTUI, event: CreateAccountResult) {.async, gcsafe, nimcall.} = # if TUI is not ready for output then ignore it if self.outputReady: let account = event.account timestamp = event.timestamp name = account.name keyUid = account.keyUid abbrev = keyUid[0..5] & "..." & keyUid[^4..^1] self.printResult("Created account:", timestamp) self.printResult(fmt"{2.indent()}{name} ({abbrev})", timestamp) ``` The last step is to wire up up the `/createaccount` (and `/create` alias) command such that the TUI can parse the command entered in to the input prompt. This is done in [`example/chat/tui/common`](https://github.com/status-im/nim-status/blob/0e4fd872fa31e8cf9486be9ad506379fd483772c/examples/chat/tui/common.nim#L71-L93). First, we added `/createaccount` to the `commands` table: ```nim commands* = { DEFAULT_COMMAND: "SendMessage", "connect": "Connect", "createaccount": "CreateAccount", # <===== added this "disconnect": "Disconnect", "help": "Help", "listaccounts": "ListAccounts", "login": "Login", "logout": "Logout", "quit": "Quit" }.toTable ``` This table aids the macro responsible for parsing commands and instatiated the associated objects, ie matching `/createaccount` to the `CreateAccount` object, so that the first four procs we created can be called (`split()`, `new()`, `command()`, and `help()`). Next, we want to alias `/createaccount` to `/create` so that we can simply enter `/create <password>` in the TUI input prompt as a convenience method, instead of having to write out the longer form of `/createaccount <password>`. ```nim aliases* = { "?": "help", "create": "createaccount", <==== added this "list": "listaccounts", "send": DEFAULT_COMMAND }.toTable aliased* = { DEFAULT_COMMAND: @["send"], "createaccount": @["create"], <==== added this "help": @["?"], "listaccounts": @["list"] }.toTable ``` The `aliases` table maps `/create` to the `/createaccount` command, and `aliased` does the opposite. ## Conclusion We were able to create a `nim-status` task that is executed off the "host" thread, or the thread that the TUI is running on. It's important to note that even though the TUI is a simple application for now, we do not want to block the thread that it runs on as it may "freeze" the TUI and cause undesirable effects like stutter during scrolling. Therefore, we want to be very careful that we do not run any blocking code, such as CPU/IO-intensive tasks in `nim-status`, on the TUI thread. The goal is to keep operations under 16ms so we can retain a smoooth 60 FPS. This would allow for the TUI to become a more complex graphical UI at some point in time if desired. Any `nim-status` operation should therefore be run as a task in a task runner thread pool, and the TUI should listen for events coming back from the task runner to display results on screen. The Example Chat Client shown above serves as an example for how this could be implemented.