M Dockerfile => Dockerfile +42 -1
@@ 6,4 6,45 @@ WORKDIR /app
COPY . /app
-EXPOSE 80>
\ No newline at end of file
+EXPOSE 80
+
+RUN echo "include /app/nginx/conf/nginx.conf;" > /etc/nginx/nginx.conf \
+ && echo "include /app/nginx/conf/nginx.conf;" > /usr/local/openresty/nginx/conf/nginx.conf
+
+RUN DEBIAN_FRONTEND=noninteractive apt-get update \
+ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
+ apt-utils make openresty-opm rlwrap wget
+
+RUN printf '#!/usr/bin/env bash\nexec rlwrap /app/modules/fennel/fennel "$@"\n' | tee /usr/local/bin/fennel
+
+# RUN mkdir /var/log/postgres \
+# && echo "deb http://apt.postgresql.org/pub/repos/apt/ stretch-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list \
+# && wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | DEBIAN_FRONTEND=noninteractive apt-key add - \
+# && DEBIAN_FRONTEND=noninteractive apt-get update \
+# && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends postgresql-11
+
+ENV NVM_DIR /usr/local/nvm
+ENV NODE_VERSION 10.15.1
+RUN mkdir $NVM_DIR \
+ && curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bash \
+ && . $NVM_DIR/nvm.sh \
+ && nvm install $NODE_VERSION \
+ && nvm alias default $NODE_VERSION \
+ && nvm use default
+ENV NODE_PATH $NVM_DIR/v$NODE_VERSION/lib/node_modules
+ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH
+
+RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | DEBIAN_FRONTEND=noninteractive apt-key add - \
+ && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
+ && DEBIAN_FRONTEND=noninteractive apt-get remove -y cmdtest \
+ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends apt-transport-https \
+ && DEBIAN_FRONTEND=noninteractive apt-get update \
+ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends yarn
+
+RUN opm get leafo/pgmoon
+
+RUN ln -sf /usr/local/openresty/luajit/bin/luajit /usr/local/bin/lua \
+ && make
+
+RUN ln -sf /dev/stdout /app/nginx/logs/access.log \
+ && ln -sf /dev/stderr /app/nginx/logs/error.log<
\ No newline at end of file
M app/makefile => app/makefile +8 -6
@@ 1,5 1,5 @@
.PHONY: all
-all: dist/main.js
+all: dist/app.js dist/app.html
.PHONY: clean
clean:
@@ 7,9 7,11 @@ clean:
rm -f src/*.lua
rm -rf node_modules
-dist/main.js: src/react-helpers.lua src/app.lua src/index.js
+dist/app.html: src/app.html.fnl
+ modules/fennel/fennel $< > $@
+dist/app.js: src/react-helpers.lua src/app.lua src/index.js src/set-function-name.js
yarn && yarn run build
-src/app.lua: src/app.fnl
- modules/fennel/fennel --compile $^ > $@
-src/react-helpers.lua: src/react-helpers.fnl
- modules/fennel/fennel --compile $^ > $@
+src/app.lua: src/app.fnl src/react-macros.fnl
+ modules/fennel/fennel --compile $< > $@
+src/react-helpers.lua: src/react-helpers.fnl src/react-macros.fnl
+ modules/fennel/fennel --compile $< > $@
M app/package.json => app/package.json +1 -1
@@ 1,7 1,7 @@
{
"name": "fennel-openresty",
"version": "0.0.1",
- "main": "dist/main.js",
+ "main": "dist/app.js",
"author": "Benaiah Mischenko",
"license": "AGPL",
"scripts": {
M app/src/app.fnl => app/src/app.fnl +72 -25
@@ 1,35 1,73 @@
(require-macros :src.react-macros)
-
(local React (require "react"))
(local ReactDOM (require "react-dom"))
+(local <> React.Fragment)
(local {:use-state use-state
+ :use-effect use-effect
+ :use-layout-effect use-layout-effect
:use-ref use-ref
+ :use-previous use-previous
:create-context create-context
:use-context use-context
:get-children-as-array get-children-as-array}
(require "./react-helpers.lua"))
+(local console {:log (fn [...] (: js.global.console :log ...))})
+
(component! LogLine [{:children message}]
- (c! :div {:className :log-message} message))
+ (c! :pre {:className :log-message} message))
(component! Log [{:children maybe-children}]
- (let [children (get-children-as-array maybe-children)]
- (: children :map (fn [_ child i] (c! LogLine {:key i} child)))))
+ (let [children (get-children-as-array maybe-children)
+ container-ref (use-ref nil)]
+ ;; scroll to bottom on change
+ (use-effect
+ (fn []
+ (when (and container-ref.current (not (= container-ref.current js.null))
+ container-ref.current.lastChild (not (= container-ref.current.lastChild js.null)))
+ (: container-ref.current.lastChild :scrollIntoView (js! {}))))
+ (js! [children.length]))
+ (c! :div {:style {:color :#EEEEEE
+ :backgroundColor :#222222
+ :overflowY :scroll
+ :position :fixed
+ :left :0px
+ :right :0px
+ :top :100px
+ :height "calc(100vh - 200px)"}
+ :ref container-ref}
+ (: children :map (fn [_ child i] (c! LogLine {:key i} child))))))
(component! WebSocketDebugForm
[{:connect connect :disconnect disconnect :on-message on-message}]
- (let [(current-message set-current-message) (use-state "")]
+ (let [input-element (use-ref nil)
+ (current-message set-current-message) (use-state "")
+ maybe-prev-message (use-previous current-message)
+ prev-message (if maybe-prev-message maybe-prev-message "")]
+ (use-effect
+ (fn [] (when input-element.current (: input-element.current :focus)) nil))
(c! [:div {}
- [:form {:onSubmit (fn []
- (on-message current-message)
- (set-current-message ""))}
+ [:form {:onSubmit
+ (fn [_ e]
+ (: e :preventDefault)
+ (on-message current-message)
+ (set-current-message ""))}
[:button {:type :button :onClick connect} :Connect]
[:button {:type :button :onClick disconnect} :Disconnect]
[:input {:id :text
:type :text
:value current-message
- :onChange (fn [_ e] (set-current-message e.target.value))}]
+ :ref input-element
+ :onChange
+ (fn []
+ ;; (print (.. "Changing current-message '"
+ ;; (if current-message current-message "")
+ ;; "' to '"
+ ;; input-element.current.value
+ ;; "'."))
+ (set-current-message input-element.current.value))
+ }]
[:button {:type :submit} :Send]]])))
(fn create-websocket
@@ 48,32 86,41 @@
(local ws-url "ws://localhost:8090/wss")
(component! App []
(let [(log-messages set-log-messages) (use-state (js! []))
+ (pending-messages set-pending-messages) (use-state (js! []))
(ws set-ws) (use-state nil)
log (fn [message]
- (: js.global.console :log message)
+ (console.log message)
(set-log-messages
(fn [_ prev-messages] (: prev-messages :concat message))))
ws-on-open (fn [] (log "connected"))
- ws-on-error (fn [_ err] (log err))
+ ws-on-error (fn [] (log "websocket error!"))
ws-on-message (fn [_ e] (log (.. "recv: " e.data)))
- ws-on-close (fn [] (log "disconnected") (set-ws nil))
+ ws-on-close (fn [_ e]
+ (log (.."disconnected: " (if e.reason e.reason "")))
+ (set-ws nil))
connect
(fn []
(if ws (log "already connected")
- (set-ws (create-websocket
- {:ws-url ws-url
- :on-open ws-on-open
- :on-error ws-on-error
- :on-message ws-on-message
- :on-close ws-on-close}))))
- disconnect (fn [] (if ws (log "already disconnected") (: ws :close)))
- send-message (fn [message]
+ (set-ws (fn [_ ws]
+ (if ws ws
+ (create-websocket
+ {:ws-url ws-url
+ :on-open ws-on-open
+ :on-error ws-on-error
+ :on-message ws-on-message
+ :on-close ws-on-close}))))))
+ disconnect (fn [] (print "App.disconnect")
+ (if (not ws) (log "already disconnected") (: ws :close)))
+ send-message (fn [message] (print "App.send-message")
(if (not ws) (log "please connect first")
- (log (.. "send: " message) (: ws :send message))))]
- (c! [React.Fragment {}
- [WebSocketDebugForm {:connect connect
- :disconnect disconnect
- :on-message send-message}]
+ (log (.. "send: " message) (: ws :send message))))
+ debug-form (c! [WebSocketDebugForm {:connect connect
+ :disconnect disconnect
+ :on-message send-message}])]
+ (set js.global.fennelLog (fn [_ message] (log message)))
+ (c! [<> {}
+ [:div {} (if ws "WebSocket connected" "Websocket not connected")]
+ debug-form
[Log {} log-messages]])))
(: ReactDOM :render
A app/src/app.html.fnl => app/src/app.html.fnl +41 -0
@@ 0,0 1,41 @@
+(local map (fn [f tbl]
+ (let [out {}]
+ (each [i v (ipairs tbl)]
+ (tset out i (f v)))
+ out)))
+
+(local map-kv (fn [f tbl]
+ (let [out {}]
+ (each [k v (pairs tbl)]
+ (table.insert out (f k v)))
+ out)))
+
+(local to-attr (fn [k v]
+ (if (= v true) k
+ (.. k "=\"" v"\""))))
+
+(local tag (fn [tag-name attrs]
+ (assert (= (type attrs) "table") "Missing attrs table")
+ (let [attr-str (table.concat (map-kv to-attr attrs) " ")]
+ (.. "<" tag-name " " attr-str">"))))
+
+(fn html [doc]
+ (if (= (type doc) "string")
+ doc
+ (let [[tag-name attrs & body] doc]
+ (.. (tag tag-name attrs)
+ (table.concat (map html body) " ")
+ "</" tag-name ">"))))
+
+(local ret
+ (.. "<!doctype html>\n"
+ (html [:html {}
+ [:head {}
+ [:meta {:charset "UTF-8"}]]
+ [:body {}
+ [:div {:id :react-root}]
+ [:script {:src "app.js"}]]])))
+
+(print ret)
+
+ret
M app/src/react-helpers.fnl => app/src/react-helpers.fnl +13 -0
@@ 1,3 1,5 @@
+(require-macros :src.react-macros)
+
(local React (require "react"))
(fn use-state [initial]
@@ 14,13 16,24 @@
(fn use-effect [effect dependencies]
(React.useEffect nil effect dependencies))
+(fn use-layout-effect [effect dependencies]
+ (React.useLayoutEffect nil effect dependencies))
+
+(fn use-previous [value]
+ (let [ref (use-ref)]
+ (use-effect (fn [] (set ref.current value)))
+ ref.current))
+
(fn get-children-as-array [maybe-children]
(if (: js.global.Array :isArray maybe-children)
maybe-children
(js! [maybe-children])))
{:use-state use-state
+ :use-effect use-effect
+ :use-layout-effect use-layout-effect
:use-ref use-ref
+ :use-previous use-previous
:create-context create-context
:use-context use-context
:get-children-as-array get-children-as-array}
M app/src/react-macros.fnl => app/src/react-macros.fnl +20 -2
@@ 52,8 52,26 @@
(create-component arg1 ...)))
(fn component! [name args ...]
- (let [padded-args `[_ @(unpack args)]]
- `(fn @name @padded-args @...)))
+ (let [tab (gensym)
+ something (gensym)
+ child-tab (gensym)
+ key (gensym)
+ val (gensym)
+ fun (gensym)
+ index-fun (gensym)
+ new-index-fun (gensym)]
+ `(local @name
+ (let [@fun (fn [_ _ @(unpack args)] @...)
+ @tab {:displayName @(tostring name)
+ :name @(tostring name)
+ :length @(# args)}
+ @index-fun (fn [_ @key] (rawget @tab @key))
+ @new-index-fun (fn [_ @key @val] (rawset @tab @key @val))]
+ (setmetatable
+ @tab {:__index @index-fun
+ :__newindex @new-index-fun
+ :__call @fun})
+ (js.createproxy @tab :arrow_function)))))
{:js! js!
M app/webpack.config.js => app/webpack.config.js +3 -0
@@ 1,5 1,8 @@
module.exports = {
entry: './src/index.js',
+ output: {
+ filename: 'app.js'
+ },
module: {
rules: [
{
A db/.gitignore => db/.gitignore +2 -0
@@ 0,0 1,2 @@
+migrations/*.lua
+config.lua<
\ No newline at end of file
A db/config.fnl => db/config.fnl +5 -0
@@ 0,0 1,5 @@
+{:host :db
+ :port 5432
+ :database :fennel-openresty
+ :user :fennel-openresty
+ :password "go fennel!"}
A db/makefile => db/makefile +15 -0
@@ 0,0 1,15 @@
+.PHONY: all
+all: config.lua migrations/create-users-table.lua migrations/create-messages-table.lua
+
+.PHONY: clean
+clean:
+ rm -f ./config.lua
+ rm -f ./migrations/*.lua
+
+config.lua: config.fnl
+ ../modules/fennel/fennel --compile $< > $@
+
+migrations/create-users-table.lua: migrations/create-users-table.fnl
+ ../modules/fennel/fennel --compile $< > $@
+migrations/create-messages-table.lua: migrations/create-messages-table.fnl
+ ../modules/fennel/fennel --compile $< > $@
A db/migrations/create-messages-table.fnl => db/migrations/create-messages-table.fnl +18 -0
@@ 0,0 1,18 @@
+(local pgmoon (require :pgmoon))
+(local dbconfig (require :db.config))
+(local pg (pgmoon.new dbconfig))
+
+(local pg-status (assert (: pg :connect)))
+
+(local messages-created (assert (: pg :query "
+create table messages (
+ id bigserial primary key,
+ message text,
+ response text,
+ timestamp timestamp with time zone default current_timestamp
+);
+")))
+
+(print (.. "creating messages table - success?: " messages-created))
+
+messages-created
A db/migrations/create-users-table.fnl => db/migrations/create-users-table.fnl +21 -0
@@ 0,0 1,21 @@
+(local pgmoon (require :pgmoon))
+(local dbconfig (require :db.config))
+(local fennelview (require :modules.fennel.fennelview))
+(local pg (pgmoon.new dbconfig))
+
+(local pg-status (assert (: pg :connect)))
+
+(local users-table-created (assert (: pg :query "
+create extension if not exists pgcrypto;
+create table if not exists users (
+ id uuid not null default gen_random_uuid() primary key,
+ email text not null,
+ password text not null
+);
+")))
+
+# hello world
+
+(print (.. "creating users table: " (fennelview users-table-created)))
+
+users-table-created
A docker-compose.yml => docker-compose.yml +34 -0
@@ 0,0 1,34 @@
+version: '3'
+services:
+ web:
+ build: .
+ depends_on:
+ - db
+ ports:
+ - "8090:80"
+ volumes:
+ - .:/app
+ networks:
+ nw:
+ ipv4_address: 172.28.0.101
+ db:
+ image: postgres
+ restart: always
+ ports:
+ - "5443:5432"
+ volumes:
+ - ./db/data:/var/lib/postgresql/data
+ environment:
+ POSTGRES_USER: fennel-openresty
+ POSGRES_PASSWORD: go fennel!
+ POSTGRES_DB: fennel-openresty
+ networks:
+ nw:
+ ipv4_address: 172.28.0.102
+
+networks:
+ nw:
+ driver: bridge
+ ipam:
+ config:
+ - subnet: 172.28.0.0/16
M makefile => makefile +9 -1
@@ 1,5 1,5 @@
.PHONY: all
-all: server app
+all: server app db
.PHONY: clean
clean: clean-server clean-app
@@ 22,3 22,11 @@ app:
.PHONY: clean-app
clean-app:
$(MAKE) -C app clean
+
+.PHONY: db
+db:
+ $(MAKE) -C db all
+
+.PHONY: clean-db
+clean-db:
+ $(MAKE) -C db clean
M nginx/conf/nginx.conf => nginx/conf/nginx.conf +10 -7
@@ 1,22 1,25 @@
-worker_processes 1;
-error_log logs/error.log;
+worker_processes 1;
events {
worker_connections 1024;
}
http {
include /usr/local/openresty/nginx/conf/mime.types;
server {
+ resolver 127.0.0.1;
listen 80;
location / {
- default_type text/html;
- content_by_lua_file server/server.lua;
- lua_code_cache off;
+ index app.html;
+ alias /app/app/dist/;
}
location /app.js {
- alias app/dist/main.js;
+ alias /app/app/dist/app.js;
+ }
+ location /api {
+ content_by_lua_file /app/server/server.lua;
+ lua_code_cache off;
}
location /wss {
- content_by_lua_file server/wss.lua;
+ content_by_lua_file /app/server/wss.lua;
lua_code_cache off;
}
}
M server/html.fnl => server/html.fnl +1 -1
@@ 30,4 30,4 @@
(let [[tag-name attrs & body] doc]
(.. (tag tag-name attrs)
(table.concat (map html body) " ")
-"</" tag-name ">"))))
+ "</" tag-name ">"))))
M server/server.fnl => server/server.fnl +5 -10
@@ 1,13 1,8 @@
(global ngx ngx)
+(local pgmoon (require :pgmoon))
(local html (require :server.html))
+(global _G (. (getmetatable _G) :__index))
+(local fennel (require :modules.fennel.fennel))
+(local fennelview (require :modules.fennel.fennelview))
-(local homepage
- [:html {}
- [:head {}
- [:meta {:charset "UTF-8"}]
- [:script {} script]]
- [:body {}
- [:div {:id :react-root}]
- [:script {:src "app.js"}]]])
-
-(ngx.say (.. "<!doctype html>\n" (html homepage)))
+(ngx.say (.. "<!doctype html>\n" (html [:div {} :WTF])))
M server/wss.fnl => server/wss.fnl +84 -38
@@ 1,40 1,86 @@
(global ngx ngx)
(local server (require :resty.websocket.server))
-(local (wb, err) (: server :new {:timeout 60000 :max_payload_len 65535}))
-
-(if (not wb)
- (do (ngx.log ngx.ERR "failed to connect to new websocket: ", err)
- (ngx.exit 444))
-
- (while true
- (local (data typ err) (: wb :recv_frame))
- (if wb.fatal
- (do (ngx.log ngx.ERR "failed to receive frame: ", err)
- (ngx.exit 444))
-
- (not data)
- (let [(bytes err) (: wb :send_ping)]
- (when (not bytes)
- (ngx.log ngx.ERR "failed to send ping: " err)
- (ngx.exit 444)))
-
- (= typ :close)
- (: wb :send_close)
-
- (= typ :ping)
- (let [(bytes err) (: wb :send_pong)]
- (when (not bytes)
- (ngx.log ngx.ERR "failed to send pong: " err)
- (ngx.exit 444)))
-
- (= typ :pong)
- (do (ngx.log ngx.INFO "client ponged")
- (: wb :send_close))
-
- (= typ :text)
- (let [(bytes err) (: wb :send_text data)]
- (if (not bytes)
- (do (ngx.log ngx.ERR "failed to send text: " err)
- (ngx.exit 444))))
-
- (: wb :send_close))))
+
+(local pgmoon (require :pgmoon))
+
+(local pg
+ (pgmoon.new
+ {:host :172.28.0.102
+ :port :5432
+ :database :fennel-openresty
+ :user :fennel-openresty
+ :password "go fennel!"}))
+(local (pg-status pg-err) (: pg :connect))
+((fn [] (when (not pg-status)
+ (ngx.log ngx.ERR (.. "error connecting to db: " (tostring pg-err))))))
+
+(fn log-message-to-db [message response]
+ (when pg-status
+ (let [query (.. "insert into messages (message, response) values ("
+ (: pg :escape_literal message) ","
+ (: pg :escape_literal response) ");")
+ (result err) (: pg :query query)]
+ (when (not result)
+ (ngx.log ngx.ERR (.. "error logging message: " (tostring err) " / " (tostring result)))))))
+
+(global _G (. (getmetatable _G) :__index))
+(local fennel (require :modules.fennel.fennel))
+(local fennelview (require :modules.fennel.fennelview))
+
+(fn eval-message [message]
+ (let [(ok result) (xpcall fennel.eval fennel.traceback message)]
+ (if ok (fennelview result) result)))
+
+(fn wss [] ;; must be wrapped in a function to avoid a top-level vararg
+ (local (wb, err) (: server :new {:timeout 10000 :max_payload_len 65535}))
+ (var continue true)
+ (when (not wb)
+ (ngx.log ngx.ERR "failed to connect to new websocket: ", err)
+ (ngx.exit 444)
+ (set continue false))
+ (while continue
+ (local (data typ err) (: wb :recv_frame))
+ (if (and (not data) (not (string.find err :timeout 1 true)))
+ (do
+ (ngx.log ngx.ERR "failed to receive a frame: " err)
+ (set continue false))
+
+ (= typ :close)
+ (let [(bytes err) (: wb :send_close 1000 "close frame")]
+ (if (not bytes)
+ (ngx.log ngx.ERR "failed to send the close frame: " err)
+ (ngx.log ngx.INFO "closing with status code " err " and message " data))
+ (set continue false))
+
+ (= typ :ping)
+ (let [(bytes err) (: wb :send_pong data)]
+ (when (not bytes)
+ (ngx.log ngx.ERR "failed to send frame: " err)
+ (set continue false)))
+
+ (= typ :pong) nil
+
+ (= typ :text)
+ (let [evaled-message (eval-message data)
+ (bytes err) (: wb :send_text evaled-message)]
+ (ngx.log ngx.INFO data)
+ (ngx.log ngx.INFO evaled-message)
+ (log-message-to-db data evaled-message)
+ (when (not bytes)
+ (ngx.log ngx.ERR "failed to send text: " err)
+ (ngx.exit 444)
+ (set continue false)))
+
+ (not (string.find err :timeout 1 true))
+ (let [(bytes err) (: wb :send_text (.. "unknown frame type: " (tostring typ)
+ " data: " (tostring data)
+ " err: " (tostring err)))]
+ (when (not bytes)
+ (ngx.log ngx.ERR "failed to send a text frame: " err)
+ (ngx.exit 444)
+ (set continue false)))))
+
+ (let [(bytes err) (: wb :send_close 1000 "close frame")]
+ (when (not bytes) (ngx.log ngx.ERR "failed to send the close frame: " err))))
+
+(wss)