git.iko.soy/airlock-template
on master help sign in
Initial commit
commitd2a103f2ce1a844209e758c4a63071fc28e8852b authoriko <mail@iko.soy> dateFri 25 Aug 2023, 11:39 (3 years ago)
8 files changed+206
unifiedsplit
.gitignore+2 README.md+14 dev/elm-live.sh+7 elm.json+44 fetch-event-source.js+2 index.html+14 script.js+42 src/Main.elm+81
.gitignorenew file+2
@@ -0,0 +1,2 @@
1+elm-stuff
2+elm.js
README.mdnew file+14
@@ -0,0 +1,14 @@
1+# What is this
2+
3+This is a teamplate to create an Urbit frontend in Elm using [package.elm-lang.org/packages/ilyakooo0/airlock/](https://package.elm-lang.org/packages/ilyakooo0/airlock/latest/).
4+
5+# Usage
6+
7+## Prerequisites
8+
9+1. The elm compiler
10+2. [elm-live](https://www.elm-live.com/)
11+
12+## Running elm-live
13+
14+To create a live-reloading version of this Elm frontend.
dev/elm-live.shnew file+7
@@ -0,0 +1,7 @@
1+#!/bin/sh
2+
3+set -e
4+
5+cd "$(dirname "$0")/.."
6+
7+elm-live src/Main.elm --start-page=index.html -- --output=elm.js --debug "$@"
elm.jsonnew file+44
@@ -0,0 +1,44 @@
1+{
2+ "type": "application",
3+ "source-directories": [
4+ "src"
5+ ],
6+ "elm-version": "0.19.1",
7+ "dependencies": {
8+ "direct": {
9+ "elm/browser": "1.0.2",
10+ "elm/core": "1.0.5",
11+ "elm/html": "1.0.0",
12+ "elm/json": "1.1.3",
13+ "ilyakooo0/airlock": "1.1.0"
14+ },
15+ "indirect": {
16+ "AdrianRibao/elm-derberos-date": "1.2.3",
17+ "TSFoster/elm-bytes-extra": "1.3.0",
18+ "bitsoflogic/elm-radixint": "2.0.0",
19+ "chelovek0v/bbase64": "1.0.1",
20+ "cmditch/elm-bigint": "2.0.1",
21+ "elm/bytes": "1.0.8",
22+ "elm/file": "1.0.5",
23+ "elm/http": "2.0.0",
24+ "elm/parser": "1.1.0",
25+ "elm/random": "1.0.0",
26+ "elm/regex": "1.0.0",
27+ "elm/time": "1.0.0",
28+ "elm/url": "1.0.0",
29+ "elm/virtual-dom": "1.0.3",
30+ "elm-community/list-extra": "8.7.0",
31+ "elm-community/maybe-extra": "5.3.0",
32+ "figbus/elm-urbit-api": "4.0.1",
33+ "jxxcarlson/hex": "4.0.0",
34+ "ktonon/elm-word": "2.1.2",
35+ "robinheghan/murmur3": "1.0.0",
36+ "rtfeldman/elm-hex": "1.0.0",
37+ "toastal/either": "3.6.3"
38+ }
39+ },
40+ "test-dependencies": {
41+ "direct": {},
42+ "indirect": {}
43+ }
44+}
fetch-event-source.jsnew file+2
@@ -0,0 +1,2 @@
1+var m=class extends Error{};async function A(r,e,t){let n=r.getReader(),o={done:!1,value:new Uint8Array};for(;o&&!o.done;)o=await Promise.race([n.read(),new Promise((g,i)=>{setTimeout(()=>i(new Error("getBytes timed out")),t)})]),e(o.value)}function R(r){let e,t,n,o=!1;return function(i){e===void 0?(e=i,t=0,n=-1):e=L(e,i);let s=e.length,a=0;for(;t<s;){o&&(e[t]===10&&(a=++t),o=!1);let c=-1;for(;t<s&&c===-1;++t)switch(e[t]){case 58:n===-1&&(n=t-a);break;case 13:o=!0;case 10:c=t;break}if(c===-1)break;r(e.subarray(a,c),n),a=t,n=-1}a===s?e=void 0:a!==0&&(e=e.subarray(a),t-=a)}}function P(r,e,t){let n=k(),o=new TextDecoder;return function(i,s){if(i.length===0)r?.(n),n=k();else if(s>0){let a=o.decode(i.subarray(0,s)),c=s+(i[s+1]===32?2:1),p=o.decode(i.subarray(c));switch(a){case"data":n.data=n.data?n.data+`
2+`+p:p;break;case"event":n.event=p;break;case"id":e?.(n.id=p);break;case"retry":let l=parseInt(p,10);isNaN(l)||t?.(n.retry=l);break}}}}function L(r,e){let t=new Uint8Array(r.length+e.length);return t.set(r),t.set(e,r.length),t}function k(){return{data:"",event:"",id:"",retry:void 0}}var b="text/event-stream",N=1e3,T="last-event-id";function q(r,{signal:e,headers:t,onopen:n,onmessage:o,onclose:g,onerror:i,openWhenHidden:s,fetch:a,responseTimeout:c,...p}){return new Promise((l,M)=>{let y={...t};y.accept||(y.accept=b);let f;function w(){f.abort(),document.hidden||E()}typeof document<"u"&&!s&&document.addEventListener("visibilitychange",w);let S=N,h;function x(){typeof document<"u"&&!s&&document.removeEventListener("visibilitychange",w),clearTimeout(h),f.abort()}e?.addEventListener("abort",()=>{x(),l()});let I=a??fetch,U=n??O,v=!1;async function E(){f=new AbortController;try{let u=await Promise.race([I(r,{...p,headers:y,signal:f.signal}),new Promise((d,C)=>{setTimeout(()=>C(new Error("fetch timed out")),c)})]);if(u.status===404)throw new m("Channel reaped");if(u.status<200||u.status>=300)throw new Error(`Invalid server response: ${u.status}`);await U(u,v),v&&(v=!1),await A(u.body,R(P(o,d=>{d?y[T]=d:delete y[T]},d=>{S=d})),c),g?.(),x(),l()}catch(u){if(!f.signal.aborted)try{v=!0;let d=i?.(u)??S;clearTimeout(h),f.abort(),h=setTimeout(E,d)}catch(d){x(),M(d)}}}E()})}function O(r){let e=r.headers.get("content-type");if(!e?.startsWith(b))throw new Error(`Expected content-type to be ${b}, Actual: ${e}`)}export{b as EventStreamContentType,q as fetchEventSource};
index.htmlnew file+14
@@ -0,0 +1,14 @@
1+<!DOCTYPE html>
2+<html lang="en">
3+ <head>
4+ <meta charset="UTF-8" />
5+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+ </head>
7+ <body>
8+ <div id="elm"></div>
9+ <script src="/elm.js"></script>
10+ <script type="module" src="/script.js"></script>
11+ </body>
12+</html>
13+
14+
script.jsnew file+42
@@ -0,0 +1,42 @@
1+import { fetchEventSource } from "/fetch-event-source.js"
2+
3+const uid = `${Math.floor(Date.now() / 1000)}-${Math.random()}`;
4+
5+function searchForInit(obj) {
6+ if (obj.init) {
7+ return obj.init;
8+ } else {
9+ for (let key in obj) {
10+ if (obj.hasOwnProperty(key)) {
11+ const result = searchForInit(obj[key]);
12+ if (result) {
13+ return result;
14+ }
15+ }
16+ }
17+ }
18+}
19+
20+let app = searchForInit(Elm)({ node: document.getElementById("elm"), flags: { uid } });
21+
22+app.ports.createEventSource.subscribe((url) => {
23+ fetchEventSource(url, {
24+ headers: {
25+ Accept: 'application/x-urb-jam',
26+ "x-channel-format": 'application/x-urb-jam',
27+ "content-type": 'application/x-urb-jam'
28+ },
29+ credentials: 'include',
30+ responseTimeout: 25000,
31+ openWhenHidden: true,
32+ onmessage(ev) {
33+ console.log(ev)
34+ app.ports.onEventSourceMessage.send({ message: ev.data });
35+ },
36+ onerror(err) {
37+ console.log(err)
38+ app.ports.onEventSourceMessage.send({ error: err });
39+ }
40+ });
41+
42+});
src/Main.elmnew file+81
@@ -0,0 +1,81 @@
1+port module Main exposing (main)
2+
3+import Browser exposing (Document)
4+import Html exposing (..)
5+import Json.Decode as JD
6+import Ur
7+import Ur.Cmd
8+import Ur.Run
9+import Ur.Sub
10+
11+
12+url : String
13+url =
14+ "http://localhost:8080"
15+
16+
17+main : Ur.Run.Program Model Msg
18+main =
19+ Ur.Run.application
20+ { init =
21+ \_ _ ->
22+ ( { ship = Nothing }
23+ , Cmd.batch
24+ [ Ur.logIn url "lidlut-tabwed-pillex-ridrup"
25+ |> Cmd.map (always Noop)
26+ , Ur.getShipName url |> Cmd.map (result (always Noop) GotShipName)
27+ ]
28+ |> Ur.Cmd.cmd
29+ )
30+ , update = update
31+ , view = view
32+ , subscriptions = always Sub.none
33+ , createEventSource = createEventSource
34+ , urbitSubscriptions = \{} -> Ur.Sub.none
35+ , onEventSourceMsg = onEventSourceMessage
36+ , onUrlChange = \_ -> Noop
37+ , onUrlRequest = \_ -> Noop
38+ , urbitUrl = \_ -> url
39+ }
40+
41+
42+type alias Model =
43+ { ship : Maybe String }
44+
45+
46+type Msg
47+ = Noop
48+ | GotShipName String
49+
50+
51+update : Msg -> Model -> ( Model, Ur.Cmd.Cmd Msg )
52+update msg model =
53+ case msg of
54+ Noop ->
55+ ( model, Ur.Cmd.none )
56+
57+ GotShipName name ->
58+ ( { model | ship = Just name }, Ur.Cmd.none )
59+
60+
61+view : Model -> Document Msg
62+view {} =
63+ { body = [ text "Welcome to Urbit" ]
64+ , title = "Airlock"
65+ }
66+
67+
68+result : (a -> c) -> (b -> c) -> Result a b -> c
69+result f g res =
70+ case res of
71+ Ok b ->
72+ g b
73+
74+ Err a ->
75+ f a
76+
77+
78+port createEventSource : String -> Cmd msg
79+
80+
81+port onEventSourceMessage : (JD.Value -> msg) -> Sub msg