A state-interface program that can run in node.js which is commonly used to create command line tools, scripts and servers.
I recommend starting with the minimal setup, browsing the examples and picking parts you want to experiment with.
A Platform.Program
that elm can run,
produced by Node.program
You can also embed a state-interface program as part of an existing app that uses The Elm Architecture.
Incoming and outgoing effects. To create one, use the helpers in time, HTTP etc.
To combine multiple, use Node.interfaceBatch and Node.interfaceNone.
To change the value that comes back in the future, use Node.interfaceFutureMap
Combine multiple Interfaces into one
Doing nothing as an Interface. These two examples are equivalent:
Node.interfaceBatch [ a, Node.interfaceNone, b ]and
Node.interfaceBatch
(List.filterMap identity
[ a |> Just, Nothing, b |> Just ]
)Take what the Interface can come back with and return a different future value.
In practice, this is sometimes used like a kind of event-config pattern:
Node.timePosixRequest
|> Node.interfaceFutureMap (\timeNow -> TimeReceived timeNow)sometimes as a way to deal with all events (like update in The Elm Architecture)
...
|> Node.interfaceFutureMap
(\event ->
case event of
CursorMovedTo newCursorPoint ->
{ state | cursorPoint = newCursorPoint }
MinusEntered ->
{ state | counter = state.counter - 1 }
PlusEntered ->
{ state | counter = state.counter + 1 }
)In all these examples, you end up converting the narrow future representation of part of the interface to a broader representation for the parent interface
See elm/time
An Interface for getting the current POSIX time.
Replacement for elm/time's Time.now.
An Interface for getting a Time.Zone
based on the current UTC offset.
Replacement for elm/time's Time.here.
Intended for package authors.
An Interface for using Intl.DateTimeFormat().resolvedOptions().timeZone
to get names like Europe/Moscow or America/Havana.
From there you can look it up in any IANA data you loaded yourself.
Replacement for elm/time's Time.getZoneName.
An Interface for getting a reminder
once a given point in time has been reached.
This lets you for example wait until it's 15 minutes before the event, timeout a request or schedule a certain action to a specific time.
import Node
type RequestState result
= NotAsked
| BeforeTimeout { start : Time.Posix }
| TimedOut
| GotResult result
{ initialState = NotAsked
, interface =
\state ->
[ case state of
NotAsked ->
[ Node.timePosixRequest
|> Node.interfaceFutureMap BeforeTimeout
, ..request.. |> Node.futureMap GotResult
]
BeforeTimeout requestTime ->
-- timeout after 10 seconds
Node.timeOnceAt (Duration.addTo requestTime (Duration.seconds 10))
|> Node.interfaceFutureMap (\_ -> TimedOut)
TimedOut ->
...
GotResult result ->
...
]
}You can abstract this in various ways like adding a
withTimeout :
..Request result..
-> Node.Interface (RequestState result)where the result can be put into the "main state" and therefore cased on.
An Interface for getting the current working directory.
Uses process.cwd
An Interface for getting the command-line arguments passed when the Node.js process was launched.
For example, running
node process-args.js one two=three fourNode.launchArgumentsRequest
would give back
[ "/usr/local/bin/node"
, "/Users/mjr/work/node/process-args.js"
, "one"
, "two=three"
, "four"
]The first is always the absolute path to the executable that started the Node.js process, the second is always the path to the executed js file.
Uses process.argv
An Interface for getting the user's environment properties set for the process.
elmHomeRequest : Node.Interface (Maybe String)
elmHomeRequest =
Node.environmentVariablesRequest
|> Node.interfaceFutureMap
(Dict.get "ELM_HOME")Uses process.env
An Interface for replacing the title of the running process.
This will usually display in the title bar of your terminal emulator or in activity monitors.
Uses process.title = ...
An Interface for very gracefully stopping the process,
completing all other current interface operations.
Do not keep listens in the interface,
otherwise the process will not die.
Keep in mind that every process ends automatically
once no interfaces that expect future data are left
which means you only need Node.exit in case you want to exit
with a non-zero error code.
Uses process.exitCode = ...
instead of process.exit
to still make interfaces like writing an error come through before exiting.
An Interface for writing text to standard out, like
hit enter to apply changes or escape to cancel:
If you want to clear the screen, apply color or font effects, employ ansi codes instead (either use a library or go click on the declarations and copy source)
If you find that the last line gets eaten, append a \n
Uses process.stdout.write
An Interface for writing text to standard err.
input file does not exits. Is the path correct?
If you find that the last line gets eaten, append a \n
Uses process.stderr.write
An Interface for reading text from standard in.
If you want to intercept input before enter is sent,
use Node.standardInRawListen
An Interface for reading text from standard in, regardless of linebreaks.
When used as a terminal interface, user input is read character by character (except modifiers)
as StreamDataReceived events.
For nice input parsing,
see for example gren-tui's stringToInput.
As long as Node.standardInRawListen is part of the interface,
the terminal won't echo input characters.
However, ctrl+c will still exit the process (SIGINT).
Depending on how the final program is used,
input can also arrive from a source outside of the terminal (like a piped file),
in which case the full thing might arrive in multiple StreamDataReceived
chunks.
Once all data is send (when outside terminal interface) or CTRL+D is pressed (in terminal interface),
you'll receive a StreamDataEndReached event.
Uses process.stdin.addListener("data", ...)
in combination with (when used as a terminal interface) process.stdin.setRawMode(true)
When data is sent (possibly in chunks), you'll get a StreamDataReceived event.
Once it finished sending data, you'll get a StreamDataEndReached event
Used in Node.standardInListen and Node.subProcessSpawn
Uses the readable stream data event and end event
An Interface for getting the inner terminal window width and height in characters,
An Interface for detecting changes to the inner window width and height
An Interface for opening and interacting with a new process.
Node.subProcessSpawn
{ command = "elm"
, arguments = [ "make", "src/Main.elm", "--output=build/elm-output.js" ]
, workingDirectoryPath = "benchmarks"
, environmentVariables =
Dict.singleton "ELM_HOME" "elm-stuff/local-elm-home"
, writeToStandardIn = Nothing
}Even though the command will not be run in a shell, you should still tightly control user input to the command and arguments and in general, limit its use to local, small scripts.
If you want to inherit parts of the environment variables and working directory path,
store and explicitly pass from environmentVariablesRequest
and workingDirectoryPathRequest.
If you find that subProcessSpawn does not seem to do anything,
it's highly likely the command needs certain (default) environment variables you might not have thought of.
Uses child_process.spawn
Action a sub-process can perform that we can react to.
Used by subProcessSpawn
Does the path point to a directory or "content"-file
An Interface for getting relative paths of the files
contained in the directory at the given path (and any contained directory).
directoryFullSubPathsRequest : String -> Node.Interface (List String)
directoryFullSubPathsRequest directoryPath =
Node.directorySubPathsRequest directoryPath
|> Node.interfaceFutureRequest
(\subNamesOrError ->
subNamesOrError
|> Result.withDefault []
|> List.map (\subName -> directoryPath ++ "/" ++ subName)
)An Interface for creating a directory including its parent directories
at a given path unless it already exists.
An Interface for creating or overwriting a file
at a given path with given Bytes content.
Uses fs.writeFile
An Interface for reading the Bytes content
of the file at a given path.
Uses fs.readFile
An Interface for detecting changes to
the file at a given path or any of its deepest sub-files.
Did the file at a path get changed/created or moved away/removed?
An Interface for sending an HTTP request
to a given url
exampleHttpPost : Web.Interface (Result Web.HttpError Bytes)
exampleHttpPost =
Web.httpRequestSend
{ url = ...
, method = "POST"
, headers = [ { name = "Content-Type", value = "application/json" } ]
, body =
96
|> Json.Encode.int
|> Json.Encode.encode 0
|> Bytes.Encode.encode
|> Just
}
exampleHttpGet : Web.HttpRequest
exampleHttpGet =
Web.httpRequestSend
{ url = ...
, method = "GET"
, headers = [ { name = "X-Custom-Header", value = "ProcessThisImmediately" } ]
, body = Nothing -- empty bytes with HEAD or GET can lead to an HttpError
}
|> Web.interfaceFutureMap
(\responseOrError ->
case responseOrError of
Ok bytes ->
case bytes |> Bytes.Decode.decode (Bytes.Decode.string (bytes |> Bytes.width)) of
Just jsonString ->
case jsonString |> Json.Decode.decodeString ... of
Err jsonError ->
Err (jsonError |> Json.Decode.errorToString)
Ok parsed ->
Ok parsed
Nothing ->
Err "failed to decode bytes to a string"
Err _ ->
Err "HTTP response error"
)Usually, if you have a request body,
the headers include Content-Type with a MIME type
The response will either contain Bytes.
or an HttpError
If you for some reason need a timeout,
add a Web.timeOnceAt and remove the Web.httpRequestSend
once it has passed.
Uses the fetch API
A Request can fail in a couple ways:
BadUrl: you did not provide a valid URLNetworkError: the user turned off their wifi, went in a cave, etc.
or the server CORS is misconfigured.
Note: A 404 for example does not constitute a network errorBadStatus: you got a response back, but the status code indicates failure. Contains:BytesAn Interface for detecting when data has been sent from the server at a given address
or the connection has changed, see Web.SocketEvent.
You also have the ability to send data to the server whenever necessary.
Node.httpRequestListenAndMaybeRespond
{ portNumber = 1248
, response =
case state.pendingRequest of
Nothing ->
Nothing
Just pendingRequest ->
case pendingRequest.data |> Bytes.Decode.decode Bytes.Decode.signedInt32 Bytes.BE of
Nothing ->
Just
{ statusCode = 404
, headers = []
, data = Bytes.Encode.sequence [] |> Bytes.Encode.encode
}
Just requestNumber ->
Just
{ statusCode = 200
, headers = []
, data =
(requestNumber * 2)
|> Bytes.Encode.signedInt32 Bytes.BE
|> Bytes.Encode.encode
}
}
|> Node.interfaceFutureMap
(\socketEvent ->
case socketEvent of
Node.HttpRequestReceived pendingRequest ->
{ state | pendingRequest = Just pendingRequest }
Node.HttpResponseSent ->
{ state | pendingRequest = Nothing }
Node.HttpServerOpened ->
state
Node.HttpServerClosed ->
state
)"Removing" Node.httpRequestListenAndMaybeRespond
from your interface will close the server at that port.
It's common to send encoded data with Json.Encode.encode 0
(usually with the header { name = "Content-Type", value = "application/json" }).
How the connection has changed or what request has been sent.
Not familiar with random "generators"? elm/random
explains it nicely!
An Interface for generating a given count of cryptographically sound unsigned 32-bit Ints.
You can use these in all kinds of packages that allow creating an initial seed
from ints like NoRedInk/elm-random-pcg-extended
If you just want to replace a part of your elm app with this architecture. Make sure to wire in all 3:
What's needed to create a state-interface program
The "init" part for an embedded program
The "update" part for an embedded program
The "subscriptions" part for an embedded program
Under the hood, Node.program is then defined as just
program config =
Platform.worker
{ init = \() -> Node.programInit yourAppConfig
, update = Node.programUpdate yourAppConfig
, subscriptions = Node.programSubscriptions yourAppConfig
}Exposed so can for example simulate it more easily in tests, add a debugger etc.
The "model" in a Node.program
The "msg" in a Node.program
If you need more things like json encoders/decoders, open an issue
Create a
Program:The state is everything the program knows (what The Elm Architecture calls model). And it always starts with a given
initialStateThe
Interfaceis the face to the outside world and can be created using the helpers in time, HTTP etc. The giveninterfacefunction constructs these interfaces based on the current stateConnections to and from js