~technomancy/fennel

migrating repl to a web worker with fallback v1 PROPOSED

gbaptista: 1
 migrating repl to a web worker with fallback

 5 files changed, 205 insertions(+), 27 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/12203/mbox | git am -3
Learn more about email & git
View this thread in the archives

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

Hello,

This patch suggests using a Web Worker for the web REPL. We had an issue
with the last one (https://lists.sr.ht/~technomancy/fennel/patches/12080).

The problem was that we had missing files in the upload process inside
the makefile. This patch solves these problems, and I improved it to
ensure a compatible fallback for browsers that do not support Web
Workers. Also, the shim.lua test still works.
I uploaded a version to make testing easier:
https://gbaptista.com/fennel/ (fennel / 123)

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

Included in this patch:

makefile:
Adds new files into the upload process.

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.

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 are compatible modes that make possible to use the REPL in
old browsers or to run tests without any browser.

---
 init.lua        | 148 ++++++++++++++++++++++++++++++++++++++++++------
 makefile        |   2 +-
 repl-worker.js  |   6 ++
 repl-worker.lua |  68 ++++++++++++++++++++++
 repl.fnl        |   8 ---
 5 files changed, 205 insertions(+), 27 deletions(-)
 create mode 100644 repl-worker.js
 create mode 100644 repl-worker.lua

diff --git a/init.lua b/init.lua
index 94fbc55..351a86b 100644
--- a/init.lua
+++ b/init.lua
@@ -38,7 +38,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,29 +123,135 @@ _G.printError = function(...)
   triggerEvent(output, "change")
end

local repl
local repl = { alreadyStarted = false, loaded = false, instance = nil, stack = {} }

--- loading Fennel at the top level breaks scrolling because browsers
--- are terrible; so we load when the input element gets focus
function input.onfocus()
    -- setting input.onfocus to nil has no effect, somehow
    if repl ~= nil then return end
function initReplForWebThroughWorker()
    input:setAttribute("disabled", "disabled")
    input:setAttribute("placeholder", "0%")
    input.value = ''

    _G.print("Loading...")

    local percentage = 0
    local dots = ''

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

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

    repl.instance.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 repl.stack[functionName] == nil then
                repl.stack[functionName] = {}
            end

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

            repl.stack[functionName] = {}
        elseif command == 'loaded' then
            welcome = content

            js.global:clearInterval(loader);
            repl.loaded = true
            input:removeAttribute("disabled")
            input:setAttribute("placeholder", "Type code here...")

            js.global:setTimeout(function()
            input = document:getElementById("fengari-input")
            input.value = '100%'
            input.value = ''
            end)
        end
    end
end

function initReplForWeb()
    _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")
        package.loaded.fennel = fennel
        _G.print("Loading REPL...")
        js.global:setTimeout(function()
            repl.instance = coroutine.create(fennel.dofile("repl.fnl"))
            assert(coroutine.resume(repl.instance))
            welcome = "Welcome to Fennel " .. fennel.version ..
                ", running on Fengari (" .. _VERSION .. ")"
            _G.print(welcome)
            _G.printLuacode("Compiled Lua code")
        end)
    end)
end

function initReplForTerminal()
    local fennel = require("fennel/fennel")
    package.loaded.fennel = fennel
    repl.instance = coroutine.create(fennel.dofile("repl.fnl"))
end

function initRepl()
    if repl.alreadyStarted then
        return
    end

    repl.alreadyStarted = true

    if js.global.Worker then
        initReplForWebThroughWorker()
    elseif js.global.setTimeout then
        initReplForWeb()
    else
        initReplForTerminal()
    end
end

function input.onfocus()
    initRepl()
end

function evaluateFennelInput(value)
    if js.global.Worker then
        repl.instance:postMessage(value)
    else
        coroutine.resume(repl.instance, value)
    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)
    if not e then
        e = js.global.event
@@ -159,7 +267,7 @@ function input.onkeydown(_, e)
                 table.remove(history, 1)
              end
           end
           coroutine.resume(repl, input.value)
           evaluateFennelInput(input.value)
           input.value = ""
        end
        return false
@@ -201,4 +309,8 @@ function input.onkeydown(_, e)
    end
end

return repl
if js.global.Worker or not js.global.setTimeout then
    initRepl()
end

return repl.instance
diff --git a/makefile b/makefile
index bcbb653..d15c5af 100644
--- a/makefile
+++ b/makefile
@@ -67,7 +67,7 @@ lua: $(LUA)
clean: cleantagdirs ; rm -f $(HTML) index.html $(LUA)

upload: $(HTML) $(LUA) $(TAGDIRS) index.html init.lua repl.fnl fennel.css \
		fengari-web.js .htaccess fennel
		fengari-web.js repl-worker.js repl-worker.lua .htaccess fennel
	rsync -r $^ fenneler@fennel-lang.org:fennel-lang.org/

conf/%.html: conf/%.fnl fennel/fennel ; fennel/fennel $< > $@
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..ac2016b
--- /dev/null
+++ b/repl-worker.lua
@@ -0,0 +1,68 @@
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"))

local welcome = "Welcome to Fennel " .. fennel.version
    .. ", running on Fengari (" .. _VERSION .. ")"

_G.print(welcome)

_G.printLuacode("Compiled Lua code")

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

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)

-- 
2.28.0
gbaptista <gbaptistas@protonmail.com> writes: