A state-interface program that can run in node.js which is commonly used to create command line tools, scripts and servers.
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
Replace 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 = ...
Gracefully stop the process, completing all other current interface operations. Do not keep listens in the interface, otherwise the process will not die.
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)
Uses process.stdout.write
An Interface for writing text to standard err.
input file does not exits. Is the path correct?
Uses process.stderr.write
Read text from standard in.
For nice input parsing,
see for example gren-tui's stringToInput.
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
Does the path point to a directory or "content"-file
An Interface for getting names of the files
contained on the surface in the directory at the given path.
directorySubPathsRequest : String -> Node.Interface (List String)
directorySubPathsRequest path =
Node.fileInfoRequest path
|> Node.interfaceFutureRequest
(\subNames ->
subNames |> List.map (\subName -> path ++ "/" ++ subName)
)Uses fs.readdir
An Interface for creating or overwriting a file
at a given path with given string content.
Uses fs.writeFile
An Interface for reading the string 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 HTTP request for use in an Interface.
Use Node.httpAddHeaders to set custom headers as needed.
Use Node.timeOnceAt to add a timeout of how long you are willing to wait before giving up.
Data send in your http request.
HttpBodyEmpty: Create an empty body for your request.
This is useful for GET requests and POST requests where you are not sending any data.
HttpBodyString: Put a String in the body of your request. Defining Node.httpJsonBody looks like this:
import Json.Encode
httpJsonBody : Json.Encode.Value -> Node.HttpBody
httpJsonBody value =
Node.HttpBodyString "application/json" (Json.Encode.encode 0 value)The first argument is a MIME type of the body.
HttpBodyUnsignedInt8s is pretty much the same as HttpBodyString but for Bytes,
see Node.httpBodyBytes
Describe what you expect to be returned in an http response body.
A Request can fail in a couple ways:
BadUrl means you did not provide a valid URLNetworkError means 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 means you got a response back, but the status code indicates failure. Contains:Metadata.Json.Decode.Value.Extra information about the response:
Note: It is possible for a response to have the same header multiple times. In that case, all the values end up in a single entry in the headers. The values are separated by commas, following the rules outlined here.
An Interface for handling an HttpRequest
using the fetch API
Create a GET HttpRequest.
Use Node.httpAddHeaders to set custom headers as needed.
Use Node.timeOnceAt to add a timeout of how long you are willing to wait before giving up.
Create a POST HttpRequest.
Use Node.httpAddHeaders to set custom headers as needed.
Use Node.timeOnceAt to add a timeout of how long you are willing to wait before giving up.
Add custom headers to the Node.HttpRequest.
request
|> Node.httpAddHeaders
[ ( "X-Custom-Header", "ProcessThisImmediately" )
]Expect the response body to be a String.
Expect the response body to be JSON, decode it using the given decoder.
The result will either be
Err with an HttpError if it didn't succeedOk if there was a result with eitherOk with the decoded valueErr with a Json.Decode.Error
and the actual text responseDiscard the response body.
Put a given JSON value in the body of your request. This will automatically add the Content-Type: application/json header.
Put given Bytes in the body of your request.
The string argument should be a MIME type to be used in the Content-Type header
import Bytes exposing (Bytes)
import Bytes.Encode
import Node
import Time
import Zip
import Zip.Entry
exampleZipBody : Node.HttpBody
exampleZipBody =
Node.httpBodyBytes "application/zip"
(Zip.fromEntries
[ Bytes.Encode.string "Hello, World!"
|> Bytes.Encode.encode
|> Zip.Entry.store
{ path = "hello.txt"
, lastModified = ( Time.utc, Time.millisToPosix 0 )
, comment = Nothing
}
]
|> Zip.toBytes
)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
Note: uses crypto.getRandomValues
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
What InterfaceSingleEdits are needed to sync up
Individual message to js to sync up with the latest interface type, describing changes to an existing interface with the same identity
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