~technomancy/fennel

migrating repl to a web worker v1 PROPOSED

gbaptista: 1
 migrating repl to a web worker

 5 files changed, 161 insertions(+), 44 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~technomancy/fennel/patches/12080/mbox | git am -3
Learn more about email & git
View this thread in the archives

[PATCH] migrating repl to a web worker Export this patch

Hi!

This patch suggests using a Web Worker for the web REPL. I tried to
modify as little code as possible.

Tested on: Chromium, Firefox, Brave, and Opera.

Included in this patch:

repl-worker.js:

Fengari does not have a straightforward way to deal with Web Workers;
This file evaluates the Worker written in Lua and makes it accessible
in pure js for the Web Worker.

repl-worker.lua:

The Web Worker source code; It's responsible for receiving messages
from the UI and sending the evaluated responses.

repl.fnl:

The Lua pane toggle was removed to make possible to run the full REPL
inside a Web Worker.

shim.lua:

A small adjustment to avoid breaking the test.

init.lua:

Instead of using REPL directly, it communicates via messages with the
Web Worker to avoid freezing the page.

I introduced a "fake loader" that gives the feeling that something is
happening to avoid let the user think that something is broken. This
loader simulates percentage progress for 10 seconds waiting time.

The Lua pane toggle was moved to this file.

Also, there is a loadReplWithoutWebWorker function to keep the shim.lua
working.

---
 init.lua        | 123 ++++++++++++++++++++++++++++++++++--------------
 repl-worker.js  |   6 +++
 repl-worker.lua |  66 ++++++++++++++++++++++++++
 repl.fnl        |   8 ----
 shim.lua        |   2 +-
 5 files changed, 161 insertions(+), 44 deletions(-)
 create mode 100644 repl-worker.js
 create mode 100644 repl-worker.lua

diff --git a/init.lua b/init.lua
index 94fbc55..9aa64fd 100644
--- a/init.lua
+++ b/init.lua
@@ -11,21 +11,6 @@ local welcome = nil
_G.os.exit = function() end
_G.os.getenv = function() return nil end

--- require-macros depends on io.open; we splice in a hacky replacement
io={open=function(filename)
       return {
          read = function(_, all)
             assert(all=="*all", "Can only read *all.")
             local xhr = js.new(js.global.XMLHttpRequest)
             xhr:open("GET", filename, false)
             xhr:send()
             assert(xhr.status == 200, xhr.status .. ": " .. xhr.statusText)
             return tostring(xhr.response)
          end,
          close = function() end,
       }
end}

package.preload.fennelview = assert(loadfile("fennelview.lua"))

-- Save references to lua baselib functions used
@@ -38,7 +23,9 @@ local output = document:getElementById("fengari-console")
local prompt = document:getElementById("fengari-prompt")
local input = document:getElementById("fengari-input")
local luacode = document:getElementById("compiled-lua")
assert(output and prompt and input and luacode)
local luapane = document:getElementById("lua-pane")
local togglebtn = document:getElementById("toggle-compiled-code")
assert(output and prompt and input and luacode and luapane and togglebtn)

local function triggerEvent(el, type)
    local e = document:createEvent("HTMLEvents")
@@ -121,27 +108,91 @@ _G.printError = function(...)
   triggerEvent(output, "change")
end

local repl
local replWorker = { webWorkerNotStarted = true }
local replWorkerLoaded = false
local replStack = {}

function replWorker.loadReplWithoutWebWorker()
    local fennel = require("fennel/fennel")
    package.loaded.fennel = fennel
    return coroutine.create(fennel.dofile("repl.fnl"))
end

-- loading Fennel at the top level breaks scrolling because browsers
-- are terrible; so we load when the input element gets focus

function initReplWorker()
    if input == nil then
      js.global.console:log('input not exists...')
      -- not running in a real web browser
      return
    end

    input:setAttribute("disabled", "disabled")
    input:setAttribute("placeholder", "0%")

    _G.print("Loading...")

    local percentage = 0

    local loader = js.global:setInterval(function()
      if not replWorkerLoaded and percentage < 99 then
        percentage = percentage + 1
        input:setAttribute("placeholder", percentage .. "%")
      else
        js.global:clearInterval(loader);
      end
    end, (10 * (1000)) / 100) -- Maximum expected time on a slow computer: 10s

    replWorker = js.new(js.global.Worker, '/repl-worker.js')

    replWorker.onmessage = function(_, message)
        local content = message.data

        local pipePosition = content:find('|')
        local functionName = content:sub(1, pipePosition-1)

        content = content:sub(pipePosition+1, #content)

        pipePosition = content:find('|')

        local command = content:sub(1, pipePosition-1)

        content = content:sub(pipePosition+1, #content)

        if command == 'append' then
            if replStack[functionName] == nil then
                replStack[functionName] = {}
            end

          table.insert(replStack[functionName], content)
        elseif command == 'dispatch' then
            _G[functionName](table.unpack(replStack[functionName]))

            replStack[functionName] = {}
        elseif command == 'loaded' then
          js.global:clearInterval(loader);
          replWorkerLoaded = true
          input:removeAttribute("disabled")
          input:setAttribute("placeholder", "Type code here...")
          input:focus()
        end
    end
end

function input.onfocus()
    -- setting input.onfocus to nil has no effect, somehow
    if repl ~= nil then return end
    _G.print("Loading Fennel...")
    js.global:setTimeout(function()
        local fennel = require("fennel/fennel")
       package.loaded.fennel = fennel
       _G.print("Loading REPL...")
       js.global:setTimeout(function()
           repl = coroutine.create(fennel.dofile("repl.fnl"))
           assert(coroutine.resume(repl))
           welcome = "Welcome to Fennel " .. fennel.version ..
              ", running on Fengari (" .. _VERSION .. ")"
           _G.print(welcome)
           _G.printLuacode("Compiled Lua code")
        end)
    end)
    if replWorker.webWorkerNotStarted then
        replWorker.webWorkerNotStarted = false
        initReplWorker()
    end
end

togglebtn.onclick = function()
    if luapane.style.display == 'flex' then
        luapane.style.display = 'none'
    else
        luapane.style.display = 'flex'
    end
end

function input.onkeydown(_, e)
@@ -159,7 +210,9 @@ function input.onkeydown(_, e)
                 table.remove(history, 1)
              end
           end
           coroutine.resume(repl, input.value)

           replWorker:postMessage(input.value)

           input.value = ""
        end
        return false
@@ -201,4 +254,4 @@ function input.onkeydown(_, e)
    end
end

return repl
return replWorker
diff --git a/repl-worker.js b/repl-worker.js
new file mode 100644
index 0000000..fe0dafa
--- /dev/null
+++ b/repl-worker.js
@@ -0,0 +1,6 @@
const window = this;
importScripts('/fengari-web.js');

fetch('/repl-worker.lua')
    .then(response => response.text())
    .then(sourceCode => window.fengari.load(sourceCode)());
diff --git a/repl-worker.lua b/repl-worker.lua
new file mode 100644
index 0000000..961cfad
--- /dev/null
+++ b/repl-worker.lua
@@ -0,0 +1,66 @@
package.path = "./?.lua"
local js = require "js"

local _G = _G
local pack = table.pack
local tostring = tostring

-- just make a few things not blow up
_G.os.exit = function() end
_G.os.getenv = function() return nil end

-- require-macros depends on io.open; we splice in a hacky replacement
io={open=function(filename)
    return {
        read = function(_, all)
            assert(all=="*all", "Can only read *all.")
            local xhr = js.new(js.global.XMLHttpRequest)
            xhr:open("GET", filename, false)
            xhr:send()
            assert(xhr.status == 200, xhr.status .. ": " .. xhr.statusText)
            return tostring(xhr.response)
        end,
        close = function() end,
    }
end}

function postContent(functionName, lines)
    for i = 1, lines.n do
        js.global:postMessage(functionName .. "|append|" .. tostring(lines[i]))
    end
    js.global:postMessage(functionName .. "|dispatch|")
end

_G.printLuacode = function(...)
    postContent("printLuacode", pack(...))
end

_G.print = function(...)
    postContent("print", pack(...))
end

_G.narrate = function(...)
    postContent("narrate", pack(...))
end

_G.printError = function(...)
    postContent("printError", pack(...))
end

local fennel = require("fennel/fennel")
package.loaded.fennel = fennel

repl = coroutine.create(fennel.dofile("repl.fnl"))

_G.print("Welcome to Fennel " .. fennel.version
    .. ", running on Fengari (" .. _VERSION .. ")")

_G.printLuacode("Compiled Lua code")

js.global:postMessage("|loaded|loaded|")

assert(coroutine.resume(repl))

js.global.onmessage = function(_, event)
    coroutine.resume(repl, event.data)
end
diff --git a/repl.fnl b/repl.fnl
index 0add6b4..1b92524 100644
--- a/repl.fnl
+++ b/repl.fnl
@@ -7,14 +7,6 @@

(set env._ENV env)

(local luapane (: js.global.document :getElementById "lua-pane"))
(local toggle-btn (: js.global.document :getElementById "toggle-compiled-code"))

(fn toggle-btn.onclick []
  (if (= luapane.style.display "flex")
      (set luapane.style.display "none")
      (set luapane.style.display "flex")))

(var last-input nil)
(var last-value nil)

diff --git a/shim.lua b/shim.lua
index 1e2695e..e891639 100644
--- a/shim.lua
+++ b/shim.lua
@@ -47,7 +47,7 @@ package.loaded.js = {
   }
}

local coro = require("init")
local coro = require("init").loadReplWithoutWebWorker()

coroutine.resume(coro, "(print :abc)")
coroutine.resume(coro, "(var x 1)")
-- 
2.28.0