├── .gitignore ├── LICENSE ├── README.md ├── elm-package.json ├── example ├── .gitignore ├── Example.elm ├── elm-package.json ├── example.js └── package.json ├── package.json └── src └── elm ├── Script.elm └── Script ├── Supervisor.elm └── Worker.elm /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | elm-stuff 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Richard Feldman 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of elm-node-app nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elm-node-app 2 | Run Elm CLI applications on Node.js. 3 | 4 | # TODO 5 | 6 | * Literally everything 7 | -------------------------------------------------------------------------------- /elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "Run Elm applications on Node.js", 4 | "repository": "https://github.com/rtfeldman/elm-node-app.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "src/elm" 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "circuithub/elm-json-extra": "2.2.1 <= v < 3.0.0", 12 | "elm-lang/core": "3.0.0 <= v < 4.0.0" 13 | }, 14 | "elm-version": "0.16.0 <= v < 0.17.0" 15 | } -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | Elm.js 2 | -------------------------------------------------------------------------------- /example/Example.elm: -------------------------------------------------------------------------------- 1 | module Example (..) where 2 | 3 | import Signal exposing (Signal) 4 | import Json.Encode as Encode exposing (Value) 5 | import Json.Decode as Decode exposing ((:=)) 6 | import Script 7 | import Set exposing (Set) 8 | import Script.Supervisor as Supervisor exposing (WorkerId, SupervisorMsg(..)) 9 | import Script.Worker as Worker 10 | import String 11 | 12 | 13 | type alias WorkerModel = 14 | { id : String } 15 | 16 | 17 | type alias SupervisorModel = 18 | { messagesReceived : List String 19 | , workerIds : Set WorkerId 20 | } 21 | 22 | 23 | updateWorker : Value -> WorkerModel -> ( WorkerModel, Worker.Cmd ) 24 | updateWorker data model = 25 | case Decode.decodeValue Decode.string data of 26 | Ok id -> 27 | ( { model | id = id }, Worker.send (Encode.string ("Hi, my name is Worker " ++ id ++ "!")) ) 28 | 29 | Err err -> 30 | ( model, Worker.send (Encode.string ("Error on worker " ++ model.id ++ ": " ++ err)) ) 31 | 32 | 33 | updateSupervisor : SupervisorMsg -> SupervisorModel -> ( SupervisorModel, Supervisor.Cmd ) 34 | updateSupervisor supervisorMsg model = 35 | case supervisorMsg of 36 | FromWorker workerId data -> 37 | case Decode.decodeValue Decode.string data of 38 | Ok str -> 39 | ( model, Supervisor.emit (Encode.string ("worker[" ++ workerId ++ "] says: " ++ str)) ) 40 | 41 | Err err -> 42 | ( model, Supervisor.emit (Encode.string ("worker[" ++ workerId ++ "] sent malformed example data!")) ) 43 | 44 | FromOutside data -> 45 | case Decode.decodeValue (Decode.object2 (,) ("msgType" := Decode.string) ("data" := Decode.string)) data of 46 | Ok ( "echo", msg ) -> 47 | let 48 | newMessagesReceived = 49 | model.messagesReceived ++ [ msg ] 50 | 51 | output = 52 | "Here are all the messages I've received so far:\n" 53 | ++ (String.join "\n" newMessagesReceived) 54 | in 55 | ( { model | messagesReceived = newMessagesReceived }, Supervisor.emit (Encode.string output) ) 56 | 57 | Ok ( "spawn", workerId ) -> 58 | ( { model | workerIds = Set.insert workerId model.workerIds } 59 | , Supervisor.send workerId (Encode.string workerId) 60 | ) 61 | 62 | Ok ( msgType, msg ) -> 63 | Debug.crash ("Urecognized msgType: " ++ msgType ++ " with data: " ++ msg) 64 | 65 | Err err -> 66 | ( model, Supervisor.emit (Encode.string ("Error decoding message; error was: " ++ err)) ) 67 | 68 | 69 | port sendMessage : Signal Value 70 | port sendMessage = 71 | Script.start 72 | { worker = 73 | { update = updateWorker 74 | , init = ( (WorkerModel "0"), Worker.none ) 75 | } 76 | , supervisor = 77 | { update = updateSupervisor 78 | , init = ( (SupervisorModel [] Set.empty), Supervisor.none ) 79 | } 80 | , receiveMessage = receiveMessage 81 | } 82 | 83 | 84 | port receiveMessage : Signal Value 85 | -------------------------------------------------------------------------------- /example/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "elm-node-app example", 4 | "repository": "https://github.com/rtfeldman/elm-node-app.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | ".", 8 | "../src/elm" 9 | ], 10 | "exposed-modules": [], 11 | "dependencies": { 12 | "circuithub/elm-json-extra": "2.2.1 <= v < 3.0.0", 13 | "elm-lang/core": "3.0.0 <= v < 4.0.0" 14 | }, 15 | "elm-version": "0.16.0 <= v < 0.17.0" 16 | } 17 | -------------------------------------------------------------------------------- /example/example.js: -------------------------------------------------------------------------------- 1 | var Supervisor = require("elm-web-workers").Supervisor; 2 | var path = require("path"); 3 | var elmPath = path.join(__dirname, "Elm.js"); 4 | 5 | var supervisor = new Supervisor(elmPath, "Example", {receiveMessage: null}); 6 | 7 | 8 | supervisor.on("emit", function(msg) { 9 | console.log("[supervisor]:", msg); 10 | }); 11 | 12 | supervisor.on("close", function(msg) { 13 | console.log("Closed with message:", msg); 14 | }); 15 | 16 | supervisor.start(); 17 | 18 | supervisor.send({msgType: "echo", data: "Spawning some workers..."}); 19 | supervisor.send({msgType: "spawn", data: "" + Math.random()}); 20 | supervisor.send({msgType: "spawn", data: "" + Math.random()}); 21 | supervisor.send({msgType: "spawn", data: "" + Math.random()}); 22 | supervisor.send({msgType: "spawn", data: "" + Math.random()}); 23 | supervisor.send({msgType: "spawn", data: "" + Math.random()}); 24 | supervisor.send({msgType: "spawn", data: "" + Math.random()}); 25 | supervisor.send({msgType: "spawn", data: "" + Math.random()}); 26 | supervisor.send({msgType: "spawn", data: "" + Math.random()}); 27 | 28 | process.stdin.resume(); 29 | process.stdin.setEncoding('utf8'); 30 | 31 | var util = require("util"); 32 | 33 | process.stdin.on("data", function (text) { 34 | var val = util.inspect(text); 35 | 36 | supervisor.send({msgType: "echo", data: val}); 37 | 38 | if (text === "quit\n") { 39 | done(); 40 | } 41 | }); 42 | 43 | function done() { 44 | process.exit(); 45 | } 46 | 47 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "example.js", 6 | "scripts": { 7 | "example": "elm-make Example.elm --output=Elm.js && echo \"\nmodule.exports = Elm;\" >> Elm.js && node example.js" 8 | }, 9 | "author": "Richard Feldman", 10 | "license": "BSD-3-Clause", 11 | "dependencies": { 12 | "elm-node-app": "file:..", 13 | "elm-web-workers": "git+https://github.com/rtfeldman/elm-web-workers.git", 14 | "webworker-threads": "^0.7.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elm-node-app", 3 | "version": "0.0.1", 4 | "description": "Run node.js apps that can talk to Elm apps", 5 | "main": "src/js/supervisor.js", 6 | "directories": { 7 | "example": "example" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/rtfeldman/elm-node-app.git" 15 | }, 16 | "author": "Richard Feldman ", 17 | "license": "BSD-3-Clause", 18 | "bugs": { 19 | "url": "https://github.com/rtfeldman/elm-node-app/issues" 20 | }, 21 | "homepage": "https://github.com/rtfeldman/elm-node-app#readme" 22 | } 23 | -------------------------------------------------------------------------------- /src/elm/Script.elm: -------------------------------------------------------------------------------- 1 | module Script (..) where 2 | 3 | import Signal exposing (Signal) 4 | import Json.Decode as Decode exposing (Value, Decoder, (:=), decodeValue) 5 | import Json.Decode.Extra as Extra 6 | import Json.Encode as Encode 7 | import Script.Worker as Worker 8 | import Script.Supervisor as Supervisor exposing (WorkerId, SupervisorMsg(..)) 9 | 10 | 11 | type alias Distribute workerModel supervisorModel = 12 | { worker : 13 | { update : Value -> workerModel -> ( workerModel, Worker.Cmd ) 14 | , init : ( workerModel, Worker.Cmd ) 15 | } 16 | , supervisor : 17 | { update : SupervisorMsg -> supervisorModel -> ( supervisorModel, Supervisor.Cmd ) 18 | , init : ( supervisorModel, Supervisor.Cmd ) 19 | } 20 | , receiveMessage : Signal Value 21 | } 22 | 23 | 24 | messageDecoder : Decoder ( Bool, Maybe WorkerId, Value ) 25 | messageDecoder = 26 | Decode.object3 (,,) ("forWorker" := Decode.bool) ("workerId" := (Extra.maybeNull Decode.string)) ("data" := Decode.value) 27 | 28 | 29 | type Role workerModel supervisorModel 30 | = Supervisor workerModel supervisorModel 31 | | Worker workerModel supervisorModel 32 | | Uninitialized 33 | 34 | 35 | type Cmd 36 | = SupervisorCmd Supervisor.Cmd 37 | | WorkerCmd Worker.Cmd 38 | | None 39 | 40 | 41 | start : Distribute workerModel supervisorModel -> Signal Value 42 | start config = 43 | let 44 | --handleMessage : Value -> ( Role workerModel supervisorModel, Cmd ) -> ( Role workerModel supervisorModel, Cmd ) 45 | handleMessage msg ( role, _ ) = 46 | case ( role, Decode.decodeValue messageDecoder msg ) of 47 | ( _, Err err ) -> 48 | Debug.crash ("Malformed JSON received: " ++ err) 49 | 50 | ( Uninitialized, Ok ( False, _, data ) ) -> 51 | let 52 | -- We've received a supervisor message; we must be a supervisor! 53 | ( model, cmd ) = 54 | config.supervisor.init 55 | 56 | ( workerModel, _ ) = 57 | config.worker.init 58 | in 59 | case handleMessage msg ( (Supervisor workerModel model), None ) of 60 | ( newRole, SupervisorCmd newCmd ) -> 61 | ( newRole, SupervisorCmd (Supervisor.batch [ cmd, newCmd ]) ) 62 | 63 | ( _, WorkerCmd _ ) -> 64 | Debug.crash "On init, received a worker command instead of the expected supervisor command" 65 | 66 | ( _, None ) -> 67 | Debug.crash "On init, received a None command instead of the expected supervisor command" 68 | 69 | ( Uninitialized, Ok ( True, _, data ) ) -> 70 | let 71 | -- We've received a worker message; we must be a worker! 72 | ( model, cmd ) = 73 | config.worker.init 74 | 75 | ( supervisorModel, _ ) = 76 | config.supervisor.init 77 | in 78 | case handleMessage msg ( (Worker model supervisorModel), None ) of 79 | ( newRole, WorkerCmd newCmd ) -> 80 | ( newRole, WorkerCmd (Worker.batch [ cmd, newCmd ]) ) 81 | 82 | ( _, SupervisorCmd _ ) -> 83 | Debug.crash "On init, received a supervisor command instead of the expected worker command" 84 | 85 | ( _, None ) -> 86 | Debug.crash "On init, received a None command instead of the expected worker command" 87 | 88 | ( Supervisor workerModel model, Ok ( False, maybeWorkerId, data ) ) -> 89 | let 90 | -- We're a supervisor; process the message accordingly 91 | subMsg = 92 | case maybeWorkerId of 93 | Nothing -> 94 | FromOutside data 95 | 96 | Just workerId -> 97 | FromWorker workerId data 98 | 99 | ( newModel, cmd ) = 100 | config.supervisor.update subMsg model 101 | in 102 | ( Supervisor workerModel newModel, SupervisorCmd cmd ) 103 | 104 | ( Worker model supervisorModel, Ok ( True, Nothing, data ) ) -> 105 | let 106 | -- We're a worker; process the message accordingly 107 | ( newModel, cmd ) = 108 | config.worker.update data model 109 | in 110 | ( Worker newModel supervisorModel, WorkerCmd cmd ) 111 | 112 | ( Worker _ _, Ok ( True, Just _, data ) ) -> 113 | Debug.crash ("Received workerId message intended for a worker.") 114 | 115 | ( Worker _ _, Ok ( False, _, _ ) ) -> 116 | Debug.crash ("Received supervisor message while running as worker.") 117 | 118 | ( Supervisor _ _, Ok ( True, _, _ ) ) -> 119 | Debug.crash ("Received worker message while running as supervisor.") 120 | in 121 | Signal.foldp handleMessage ( Uninitialized, None ) config.receiveMessage 122 | |> Signal.filterMap (snd >> cmdToMsg) Encode.null 123 | 124 | 125 | cmdToMsg : Cmd -> Maybe Value 126 | cmdToMsg rawCmd = 127 | case rawCmd of 128 | SupervisorCmd cmd -> 129 | cmd 130 | |> Supervisor.encodeCmd 131 | |> Encode.list 132 | |> Just 133 | 134 | WorkerCmd cmd -> 135 | cmd 136 | |> Worker.encodeCmd 137 | |> Encode.list 138 | |> Just 139 | 140 | None -> 141 | Nothing 142 | -------------------------------------------------------------------------------- /src/elm/Script/Supervisor.elm: -------------------------------------------------------------------------------- 1 | module Script.Supervisor (Cmd, terminate, send, emit, batch, none, WorkerId, SupervisorMsg(..), encodeCmd) where 2 | 3 | import Json.Encode as Encode exposing (Value) 4 | 5 | 6 | type alias WorkerId = 7 | String 8 | 9 | 10 | type SupervisorMsg 11 | = FromWorker WorkerId Value 12 | | FromOutside Value 13 | 14 | 15 | {-| A command the supervisor can run. 16 | -} 17 | type Cmd 18 | = Terminate 19 | | Send WorkerId Value 20 | | Emit Value 21 | | Batch (List Cmd) 22 | 23 | 24 | {-| Serialize a `Cmd` into a list of `Json.Value` instances. 25 | -} 26 | encodeCmd : Cmd -> List Value 27 | encodeCmd cmd = 28 | case cmd of 29 | Terminate -> 30 | -- Sending a null workerId and null data terminates the supervisor. 31 | [ Encode.object 32 | [ ( "cmd", Encode.string "TERMINATE" ) 33 | , ( "workerId", Encode.null ) 34 | , ( "data", Encode.null ) 35 | ] 36 | ] 37 | 38 | Emit data -> 39 | -- Sending a null workerId with String data emits it. 40 | [ Encode.object 41 | [ ( "cmd", Encode.string "EMIT" ) 42 | , ( "workerId", Encode.null ) 43 | , ( "data", data ) 44 | ] 45 | ] 46 | 47 | Send workerId data -> 48 | [ Encode.object 49 | [ ( "cmd", Encode.string "SEND_TO_WORKER" ) 50 | , ( "workerId", Encode.string workerId ) 51 | , ( "data", data ) 52 | ] 53 | ] 54 | 55 | Batch cmds -> 56 | List.concatMap encodeCmd cmds 57 | 58 | 59 | {-| Terminate the supervisor and all workers. 60 | -} 61 | terminate : Cmd 62 | terminate = 63 | Terminate 64 | 65 | 66 | {-| Send a `Json.Value` to a particular worker. 67 | -} 68 | send : WorkerId -> Value -> Cmd 69 | send = 70 | Send 71 | 72 | 73 | {-| Combine several supervisor commands. 74 | -} 75 | batch : List Cmd -> Cmd 76 | batch = 77 | Batch 78 | 79 | 80 | emit : Value -> Cmd 81 | emit = 82 | Emit 83 | 84 | 85 | {-| Do nothing. 86 | -} 87 | none : Cmd 88 | none = 89 | batch [] 90 | -------------------------------------------------------------------------------- /src/elm/Script/Worker.elm: -------------------------------------------------------------------------------- 1 | module Script.Worker (Cmd, send, batch, none, encodeCmd) where 2 | 3 | import Json.Encode as Encode exposing (Value) 4 | 5 | 6 | {-| A command the worker can run. 7 | -} 8 | type Cmd 9 | = Send Value 10 | | Batch (List Cmd) 11 | 12 | 13 | {-| Serialize a `Cmd` into a list of `Json.Value` instances. 14 | -} 15 | encodeCmd : Cmd -> List Value 16 | encodeCmd cmd = 17 | case cmd of 18 | Send data -> 19 | [ data ] 20 | 21 | Batch cmds -> 22 | List.concatMap encodeCmd cmds 23 | 24 | 25 | {-| Send a `Json.Value` to the supervisor. 26 | -} 27 | send : Value -> Cmd 28 | send = 29 | Send 30 | 31 | 32 | {-| Combine several worker commands. 33 | -} 34 | batch : List Cmd -> Cmd 35 | batch = 36 | Batch 37 | 38 | 39 | {-| Do nothing. 40 | -} 41 | none : Cmd 42 | none = 43 | batch [] 44 | --------------------------------------------------------------------------------