diff --git a/.tool-versions b/.tool-versions
new file mode 100644
index 0000000..5329811
--- /dev/null
+++ b/.tool-versions
@@ -0,0 +1,2 @@
+purescript 0.15.16-4
+bun 1.1.38
diff --git a/bun.lockb b/bun.lockb
new file mode 100755
index 0000000..357bea9
Binary files /dev/null and b/bun.lockb differ
diff --git a/jsconfig.json b/jsconfig.json
index 4567f8f..361136a 100644
--- a/jsconfig.json
+++ b/jsconfig.json
@@ -14,10 +14,6 @@
},
"include": [
"./scripts/*.js",
- "ui-serve/**/*.js",
- "api/**/*.js",
- "ui/**/*.js",
- "aws/**/*.js",
- "logging/**/*.js"
+ "src/**/*.js"
]
}
diff --git a/rename.js b/rename.js
new file mode 100644
index 0000000..cab94de
--- /dev/null
+++ b/rename.js
@@ -0,0 +1,14 @@
+import File from 'fs/promises'
+import Path from 'path'
+
+const contents = new Map()
+
+const files = (await File.readdir('./src', {recursive: true})).map(a => Path.resolve('./src', a)).filter(a => a.endsWith('.purs') || a.endsWith('.js'))
+for (const f of files) {
+ const fc = await File.readFile(f, 'utf8')
+ const fc_ = fc.replaceAll(/\bTower\b/g, 'Axon')
+ await File.writeFile(f, fc_)
+ const f_ = f.replace(/\bTower\b/, 'Axon')
+ await File.rename(f, f_)
+ console.log(`${f} -> ${f_}`)
+}
diff --git a/spago.lock b/spago.lock
new file mode 100644
index 0000000..e4b8bfa
--- /dev/null
+++ b/spago.lock
@@ -0,0 +1,1629 @@
+{
+ "workspace": {
+ "packages": {
+ "tower": {
+ "path": "./",
+ "core": {
+ "dependencies": [
+ "aff",
+ "argonaut-codecs",
+ "argonaut-core",
+ "console",
+ "effect",
+ "ezfetch",
+ "maybe",
+ "node-net",
+ "node-streams",
+ "nullable",
+ "prelude",
+ "strings",
+ "transformers",
+ "tuples",
+ "url-immutable",
+ "web-streams"
+ ],
+ "build_plan": [
+ "aff",
+ "aff-promise",
+ "argonaut-codecs",
+ "argonaut-core",
+ "arraybuffer-types",
+ "arrays",
+ "b64",
+ "bifunctors",
+ "console",
+ "const",
+ "contravariant",
+ "control",
+ "datetime",
+ "distributive",
+ "effect",
+ "either",
+ "encoding",
+ "enums",
+ "exceptions",
+ "exists",
+ "ezfetch",
+ "filterable",
+ "foldable-traversable",
+ "foreign",
+ "foreign-object",
+ "functions",
+ "functors",
+ "gen",
+ "identity",
+ "integers",
+ "invariant",
+ "js-date",
+ "js-promise",
+ "lazy",
+ "lists",
+ "maybe",
+ "media-types",
+ "newtype",
+ "node-buffer",
+ "node-event-emitter",
+ "node-fs",
+ "node-net",
+ "node-path",
+ "node-streams",
+ "nonempty",
+ "now",
+ "nullable",
+ "numbers",
+ "options",
+ "ordered-collections",
+ "orders",
+ "parallel",
+ "partial",
+ "prelude",
+ "profunctor",
+ "record",
+ "refs",
+ "safe-coerce",
+ "simple-json",
+ "st",
+ "strings",
+ "stringutils",
+ "tailrec",
+ "transformers",
+ "tuples",
+ "type-equality",
+ "typelevel-prelude",
+ "unfoldable",
+ "unsafe-coerce",
+ "url-immutable",
+ "variant",
+ "web-dom",
+ "web-events",
+ "web-file",
+ "web-streams"
+ ]
+ },
+ "test": {
+ "dependencies": [],
+ "build_plan": []
+ }
+ }
+ },
+ "package_set": {
+ "address": {
+ "registry": "61.2.0"
+ },
+ "compiler": ">=0.15.15 <0.16.0",
+ "content": {
+ "abc-parser": "2.0.1",
+ "ace": "9.1.0",
+ "address-rfc2821": "0.1.1",
+ "aff": "8.0.0",
+ "aff-bus": "6.0.0",
+ "aff-coroutines": "9.0.0",
+ "aff-promise": "4.0.0",
+ "aff-retry": "2.0.0",
+ "affjax": "13.0.0",
+ "affjax-node": "1.0.0",
+ "affjax-web": "1.0.0",
+ "ansi": "7.0.0",
+ "apexcharts": "0.5.0",
+ "applicative-phases": "1.0.0",
+ "argonaut": "9.0.0",
+ "argonaut-aeson-generic": "0.4.1",
+ "argonaut-codecs": "9.1.0",
+ "argonaut-core": "7.0.0",
+ "argonaut-generic": "8.0.0",
+ "argonaut-traversals": "10.0.0",
+ "argparse-basic": "2.0.0",
+ "array-builder": "0.1.2",
+ "array-search": "0.6.0",
+ "arraybuffer": "13.2.0",
+ "arraybuffer-builder": "3.1.0",
+ "arraybuffer-types": "3.0.2",
+ "arrays": "7.3.0",
+ "arrays-extra": "0.6.1",
+ "arrays-zipper": "2.0.1",
+ "ask": "1.0.0",
+ "assert": "6.0.0",
+ "assert-multiple": "0.4.0",
+ "avar": "5.0.0",
+ "b64": "0.0.8",
+ "barbies": "1.0.1",
+ "barlow-lens": "0.9.0",
+ "bifunctors": "6.0.0",
+ "bigints": "7.0.1",
+ "bolson": "0.3.9",
+ "bookhound": "0.1.7",
+ "bower-json": "3.0.0",
+ "call-by-name": "4.0.1",
+ "canvas": "6.0.0",
+ "canvas-action": "9.0.0",
+ "cartesian": "1.0.6",
+ "catenable-lists": "7.0.0",
+ "cbor-stream": "1.3.0",
+ "chameleon": "1.0.0",
+ "chameleon-halogen": "1.0.3",
+ "chameleon-react-basic": "1.1.0",
+ "chameleon-styled": "2.5.0",
+ "chameleon-transformers": "1.0.0",
+ "channel": "1.0.0",
+ "checked-exceptions": "3.1.1",
+ "choku": "1.0.2",
+ "classless": "0.1.1",
+ "classless-arbitrary": "0.1.1",
+ "classless-decode-json": "0.1.1",
+ "classless-encode-json": "0.1.3",
+ "classnames": "2.0.0",
+ "codec": "6.1.0",
+ "codec-argonaut": "10.0.0",
+ "codec-json": "2.0.0",
+ "colors": "7.0.1",
+ "concur-core": "0.5.0",
+ "concur-react": "0.5.0",
+ "concurrent-queues": "3.0.0",
+ "console": "6.1.0",
+ "const": "6.0.0",
+ "contravariant": "6.0.0",
+ "control": "6.0.0",
+ "convertable-options": "1.0.0",
+ "coroutines": "7.0.0",
+ "css": "6.0.0",
+ "css-frameworks": "1.0.1",
+ "csv-stream": "2.3.0",
+ "data-mvc": "0.0.2",
+ "datetime": "6.1.0",
+ "datetime-parsing": "0.2.0",
+ "debounce": "0.1.0",
+ "debug": "6.0.2",
+ "decimals": "7.1.0",
+ "default-values": "1.0.1",
+ "deku": "0.9.23",
+ "deno": "0.0.5",
+ "dissect": "1.0.0",
+ "distributive": "6.0.0",
+ "dom-filereader": "7.0.0",
+ "dom-indexed": "12.0.0",
+ "dom-simple": "0.4.0",
+ "dotenv": "4.0.3",
+ "droplet": "0.6.0",
+ "dts": "1.0.0",
+ "dual-numbers": "1.0.3",
+ "dynamic-buffer": "3.0.1",
+ "echarts-simple": "0.0.1",
+ "effect": "4.0.0",
+ "either": "6.1.0",
+ "elmish": "0.13.0",
+ "elmish-enzyme": "0.1.1",
+ "elmish-hooks": "0.10.3",
+ "elmish-html": "0.9.0",
+ "elmish-testing-library": "0.3.2",
+ "email-validate": "7.0.0",
+ "encoding": "0.0.9",
+ "enums": "6.0.1",
+ "env-names": "0.4.0",
+ "error": "2.0.0",
+ "eta-conversion": "0.3.2",
+ "exceptions": "6.1.0",
+ "exists": "6.0.0",
+ "exitcodes": "4.0.0",
+ "expect-inferred": "3.0.0",
+ "ezfetch": "1.1.0",
+ "fahrtwind": "2.0.0",
+ "fallback": "0.1.0",
+ "fast-vect": "1.2.0",
+ "fetch": "4.1.0",
+ "fetch-argonaut": "1.0.1",
+ "fetch-core": "5.1.0",
+ "fetch-yoga-json": "1.1.0",
+ "ffi-simple": "0.5.1",
+ "fft-js": "0.1.0",
+ "filterable": "5.0.0",
+ "fix-functor": "0.1.0",
+ "fixed-points": "7.0.0",
+ "fixed-precision": "5.0.0",
+ "flame": "1.3.0",
+ "float32": "2.0.0",
+ "fmt": "0.2.1",
+ "foldable-traversable": "6.0.0",
+ "foldable-traversable-extra": "0.0.6",
+ "foreign": "7.0.0",
+ "foreign-object": "4.1.0",
+ "foreign-readwrite": "3.4.0",
+ "forgetmenot": "0.1.0",
+ "fork": "6.0.0",
+ "form-urlencoded": "7.0.0",
+ "formatters": "7.0.0",
+ "framer-motion": "1.0.1",
+ "free": "7.1.0",
+ "freeap": "7.0.0",
+ "freer-free": "0.0.1",
+ "freet": "7.0.0",
+ "functions": "6.0.0",
+ "functor1": "3.0.0",
+ "functors": "5.0.0",
+ "fuzzy": "0.4.0",
+ "gen": "4.0.0",
+ "generate-values": "1.0.1",
+ "generic-router": "0.0.1",
+ "geojson": "0.0.5",
+ "geometria": "2.2.0",
+ "gojs": "0.1.1",
+ "grain": "3.0.0",
+ "grain-router": "3.0.0",
+ "grain-virtualized": "3.0.0",
+ "graphs": "8.1.0",
+ "group": "4.1.1",
+ "halogen": "7.0.0",
+ "halogen-bootstrap5": "5.3.2",
+ "halogen-canvas": "1.0.0",
+ "halogen-css": "10.0.0",
+ "halogen-echarts-simple": "0.0.4",
+ "halogen-formless": "4.0.3",
+ "halogen-helix": "1.0.1",
+ "halogen-hooks": "0.6.3",
+ "halogen-hooks-extra": "0.9.0",
+ "halogen-infinite-scroll": "1.1.0",
+ "halogen-store": "0.5.4",
+ "halogen-storybook": "2.0.0",
+ "halogen-subscriptions": "2.0.0",
+ "halogen-svg-elems": "8.0.0",
+ "halogen-typewriter": "1.0.4",
+ "halogen-vdom": "8.0.0",
+ "halogen-vdom-string-renderer": "0.5.0",
+ "halogen-xterm": "2.0.0",
+ "heckin": "2.0.1",
+ "heterogeneous": "0.6.0",
+ "homogeneous": "0.4.0",
+ "http-methods": "6.0.0",
+ "httpurple": "4.0.0",
+ "huffman": "0.4.0",
+ "humdrum": "0.0.1",
+ "hyrule": "2.3.8",
+ "identity": "6.0.0",
+ "identy": "4.0.1",
+ "indexed-db": "1.0.0",
+ "indexed-monad": "3.0.0",
+ "int64": "3.0.0",
+ "integers": "6.0.0",
+ "interpolate": "5.0.2",
+ "intersection-observer": "1.0.1",
+ "invariant": "6.0.0",
+ "jarilo": "1.0.1",
+ "jelly": "0.10.0",
+ "jelly-router": "0.3.0",
+ "jelly-signal": "0.4.0",
+ "jest": "1.0.0",
+ "js-abort-controller": "1.0.0",
+ "js-bigints": "2.2.1",
+ "js-date": "8.0.0",
+ "js-fetch": "0.2.1",
+ "js-fileio": "3.0.0",
+ "js-intl": "1.1.4",
+ "js-iterators": "0.1.1",
+ "js-maps": "0.1.2",
+ "js-promise": "1.0.0",
+ "js-promise-aff": "1.0.0",
+ "js-timers": "6.1.0",
+ "js-uri": "3.1.0",
+ "jsdom": "1.0.0",
+ "json": "1.1.0",
+ "json-codecs": "5.0.0",
+ "justifill": "0.5.0",
+ "jwt": "0.0.9",
+ "labeled-data": "0.2.0",
+ "language-cst-parser": "0.14.0",
+ "lazy": "6.0.0",
+ "lazy-joe": "1.0.0",
+ "lcg": "4.0.0",
+ "leibniz": "5.0.0",
+ "leveldb": "1.0.1",
+ "liminal": "1.0.1",
+ "linalg": "6.0.0",
+ "lists": "7.0.0",
+ "literals": "1.0.2",
+ "logging": "3.0.0",
+ "logging-journald": "0.4.0",
+ "lumi-components": "18.0.0",
+ "machines": "7.0.0",
+ "maps-eager": "0.5.0",
+ "marionette": "1.0.0",
+ "marionette-react-basic-hooks": "0.1.1",
+ "marked": "0.1.0",
+ "matrices": "5.0.1",
+ "matryoshka": "1.0.0",
+ "maybe": "6.0.0",
+ "media-types": "6.0.0",
+ "meowclient": "1.0.0",
+ "midi": "4.0.0",
+ "milkis": "9.0.0",
+ "minibench": "4.0.1",
+ "mmorph": "7.0.0",
+ "monad-control": "5.0.0",
+ "monad-logger": "1.3.1",
+ "monad-loops": "0.5.0",
+ "monad-unlift": "1.0.1",
+ "monoid-extras": "0.0.1",
+ "monoidal": "0.16.0",
+ "morello": "0.4.0",
+ "mote": "3.0.0",
+ "motsunabe": "2.0.0",
+ "mvc": "0.0.1",
+ "mysql": "6.0.1",
+ "n3": "0.1.0",
+ "nano-id": "1.1.0",
+ "nanoid": "0.1.0",
+ "naturals": "3.0.0",
+ "nested-functor": "0.2.1",
+ "newtype": "5.0.0",
+ "nextjs": "0.1.1",
+ "nextui": "0.2.0",
+ "node-buffer": "9.0.0",
+ "node-child-process": "11.1.0",
+ "node-event-emitter": "3.0.0",
+ "node-execa": "5.0.0",
+ "node-fs": "9.2.0",
+ "node-glob-basic": "1.3.0",
+ "node-http": "9.1.0",
+ "node-http2": "1.1.1",
+ "node-human-signals": "1.0.0",
+ "node-net": "5.1.0",
+ "node-os": "5.1.0",
+ "node-path": "5.0.0",
+ "node-process": "11.2.0",
+ "node-readline": "8.1.1",
+ "node-sqlite3": "8.0.0",
+ "node-stream-pipes": "2.1.6",
+ "node-streams": "9.0.0",
+ "node-tls": "0.3.1",
+ "node-url": "7.0.1",
+ "node-zlib": "0.4.0",
+ "nonempty": "7.0.0",
+ "now": "6.0.0",
+ "npm-package-json": "2.0.0",
+ "nullable": "6.0.0",
+ "numberfield": "0.2.2",
+ "numbers": "9.0.1",
+ "oak": "3.1.1",
+ "oak-debug": "1.2.2",
+ "object-maps": "0.3.0",
+ "ocarina": "1.5.4",
+ "oooooooooorrrrrrrmm-lib": "0.0.1",
+ "open-colors-scales-and-schemes": "1.0.0",
+ "open-folds": "6.4.0",
+ "open-foreign-generic": "11.0.3",
+ "open-memoize": "6.2.0",
+ "open-mkdirp-aff": "1.2.0",
+ "open-pairing": "6.2.0",
+ "open-smolder": "12.0.2",
+ "options": "7.0.0",
+ "optparse": "5.0.1",
+ "ordered-collections": "3.2.0",
+ "ordered-set": "0.4.0",
+ "orders": "6.0.0",
+ "owoify": "1.2.0",
+ "pairs": "9.0.1",
+ "parallel": "7.0.0",
+ "parsing": "10.2.0",
+ "parsing-dataview": "3.2.4",
+ "partial": "4.0.0",
+ "pathy": "9.0.0",
+ "pha": "0.13.0",
+ "phaser": "0.7.0",
+ "phylio": "1.1.2",
+ "pipes": "8.0.0",
+ "pirates-charm": "0.0.1",
+ "pmock": "0.9.0",
+ "point-free": "1.0.0",
+ "pointed-list": "0.5.1",
+ "polymorphic-vectors": "4.0.0",
+ "posix-types": "6.0.0",
+ "postgresql": "2.0.20",
+ "precise": "6.0.0",
+ "precise-datetime": "7.0.0",
+ "prelude": "6.0.1",
+ "prettier-printer": "3.0.0",
+ "printf": "0.1.0",
+ "priority-queue": "0.1.2",
+ "profunctor": "6.0.1",
+ "profunctor-lenses": "8.0.0",
+ "protobuf": "4.4.0",
+ "psa-utils": "8.0.0",
+ "psci-support": "6.0.0",
+ "punycode": "1.0.0",
+ "qualified-do": "2.2.0",
+ "quantities": "12.2.0",
+ "quickcheck": "8.0.1",
+ "quickcheck-combinators": "0.1.3",
+ "quickcheck-laws": "7.0.0",
+ "quickcheck-utf8": "0.0.0",
+ "random": "6.0.0",
+ "rationals": "6.0.0",
+ "rdf": "0.1.0",
+ "react": "11.0.0",
+ "react-aria": "0.2.0",
+ "react-basic": "17.0.0",
+ "react-basic-classic": "3.0.0",
+ "react-basic-dnd": "10.1.0",
+ "react-basic-dom": "7.0.0",
+ "react-basic-dom-beta": "0.1.1",
+ "react-basic-emotion": "7.1.0",
+ "react-basic-hooks": "8.2.0",
+ "react-basic-storybook": "2.0.0",
+ "react-dom": "8.0.0",
+ "react-halo": "3.0.0",
+ "react-icons": "1.1.5",
+ "react-markdown": "0.1.0",
+ "react-testing-library": "4.0.1",
+ "react-virtuoso": "1.0.0",
+ "reactix": "0.6.1",
+ "read": "1.0.1",
+ "recharts": "1.1.0",
+ "record": "4.0.0",
+ "record-extra": "5.0.1",
+ "record-ptional-fields": "0.1.2",
+ "record-studio": "1.0.4",
+ "refs": "6.0.0",
+ "remotedata": "5.0.1",
+ "repr": "0.5.0",
+ "resize-observer": "1.0.0",
+ "resource": "2.0.1",
+ "resourcet": "1.0.0",
+ "result": "1.0.3",
+ "return": "0.2.0",
+ "ring-modules": "5.0.1",
+ "rito": "0.3.4",
+ "roman": "0.4.0",
+ "rough-notation": "1.0.2",
+ "routing": "11.0.0",
+ "routing-duplex": "0.7.0",
+ "run": "5.0.0",
+ "safe-coerce": "2.0.0",
+ "safely": "4.0.1",
+ "school-of-music": "1.3.0",
+ "selection-foldable": "0.2.0",
+ "selective-functors": "1.0.1",
+ "semirings": "7.0.0",
+ "signal": "13.0.0",
+ "simple-emitter": "3.0.1",
+ "simple-i18n": "2.0.1",
+ "simple-json": "9.0.0",
+ "simple-json-generics": "0.2.1",
+ "simple-ulid": "3.0.0",
+ "sized-matrices": "1.0.0",
+ "sized-vectors": "5.0.2",
+ "slug": "3.1.0",
+ "small-ffi": "4.0.1",
+ "soundfonts": "4.1.0",
+ "sparse-matrices": "2.0.1",
+ "sparse-polynomials": "3.0.1",
+ "spec": "8.1.1",
+ "spec-discovery": "8.4.0",
+ "spec-mocha": "5.1.1",
+ "spec-node": "0.0.3",
+ "spec-quickcheck": "5.0.2",
+ "spec-reporter-xunit": "0.7.1",
+ "splitmix": "2.1.0",
+ "ssrs": "1.0.0",
+ "st": "6.2.0",
+ "statistics": "0.3.2",
+ "strictlypositiveint": "1.0.1",
+ "string-parsers": "8.0.0",
+ "strings": "6.0.1",
+ "strings-extra": "4.0.0",
+ "stringutils": "0.0.12",
+ "substitute": "0.2.3",
+ "supply": "0.2.0",
+ "svg-parser": "3.0.0",
+ "systemd-journald": "0.3.0",
+ "tagged": "4.0.2",
+ "tailrec": "6.1.0",
+ "tecton": "0.2.1",
+ "tecton-halogen": "0.2.0",
+ "test-unit": "17.0.0",
+ "thermite": "6.3.1",
+ "thermite-dom": "0.3.1",
+ "these": "6.0.0",
+ "threading": "0.0.3",
+ "tldr": "0.0.0",
+ "toestand": "0.9.0",
+ "transformation-matrix": "1.0.1",
+ "transformers": "6.1.0",
+ "tree-rose": "4.0.2",
+ "ts-bridge": "4.0.0",
+ "tuples": "7.0.0",
+ "two-or-more": "1.0.0",
+ "type-equality": "4.0.1",
+ "typedenv": "2.0.1",
+ "typelevel": "6.0.0",
+ "typelevel-lists": "2.1.0",
+ "typelevel-peano": "1.0.1",
+ "typelevel-prelude": "7.0.0",
+ "typelevel-regex": "0.0.3",
+ "typelevel-rows": "0.1.0",
+ "typisch": "0.4.0",
+ "uint": "7.0.0",
+ "ulid": "3.0.1",
+ "uncurried-transformers": "1.1.0",
+ "undefined": "2.0.0",
+ "undefined-is-not-a-problem": "1.1.0",
+ "unfoldable": "6.0.0",
+ "unicode": "6.0.0",
+ "unique": "0.6.1",
+ "unlift": "1.0.1",
+ "unordered-collections": "3.1.0",
+ "unsafe-coerce": "6.0.0",
+ "unsafe-reference": "5.0.0",
+ "untagged-to-tagged": "0.1.4",
+ "untagged-union": "1.0.0",
+ "uri": "9.0.0",
+ "url-immutable": "1.0.0",
+ "uuid": "9.0.0",
+ "uuidv4": "1.0.0",
+ "validation": "6.0.0",
+ "variant": "8.0.0",
+ "variant-encodings": "2.0.0",
+ "vectorfield": "1.0.1",
+ "vectors": "2.1.0",
+ "versions": "7.0.0",
+ "visx": "0.0.2",
+ "web-clipboard": "6.0.0",
+ "web-cssom": "2.0.0",
+ "web-cssom-view": "0.1.0",
+ "web-dom": "6.0.0",
+ "web-dom-parser": "8.0.0",
+ "web-dom-xpath": "3.0.0",
+ "web-encoding": "3.0.0",
+ "web-events": "4.0.0",
+ "web-fetch": "4.0.1",
+ "web-file": "4.0.0",
+ "web-geometry": "0.1.0",
+ "web-html": "4.1.0",
+ "web-pointerevents": "2.0.0",
+ "web-proletarian": "1.0.0",
+ "web-promise": "3.2.0",
+ "web-resize-observer": "2.1.0",
+ "web-router": "1.0.0",
+ "web-socket": "4.0.0",
+ "web-storage": "5.0.0",
+ "web-streams": "4.0.0",
+ "web-touchevents": "4.0.0",
+ "web-uievents": "5.0.0",
+ "web-url": "2.0.0",
+ "web-workers": "1.1.0",
+ "web-xhr": "5.0.1",
+ "webextension-polyfill": "0.1.0",
+ "webgpu": "0.0.1",
+ "which": "2.0.0",
+ "xterm": "1.0.0",
+ "yoga-fetch": "1.0.1",
+ "yoga-json": "5.1.0",
+ "yoga-om": "0.1.0",
+ "yoga-postgres": "6.0.0",
+ "yoga-subtlecrypto": "0.1.0",
+ "yoga-tree": "1.0.0",
+ "z3": "0.0.2",
+ "zipperarray": "2.0.0"
+ }
+ },
+ "extra_packages": {}
+ },
+ "packages": {
+ "aff": {
+ "type": "registry",
+ "version": "8.0.0",
+ "integrity": "sha256-5MmdI4+0RHBtSBy+YlU3/Cq4R5W2ih3OaRedJIrVHdk=",
+ "dependencies": [
+ "bifunctors",
+ "control",
+ "datetime",
+ "effect",
+ "either",
+ "exceptions",
+ "foldable-traversable",
+ "functions",
+ "maybe",
+ "newtype",
+ "parallel",
+ "prelude",
+ "refs",
+ "tailrec",
+ "transformers",
+ "unsafe-coerce"
+ ]
+ },
+ "aff-promise": {
+ "type": "registry",
+ "version": "4.0.0",
+ "integrity": "sha256-Kq5EupbUpXeUXx4JqGQE7/RTTz/H6idzWhsocwlEFhM=",
+ "dependencies": [
+ "aff",
+ "foreign"
+ ]
+ },
+ "argonaut-codecs": {
+ "type": "registry",
+ "version": "9.1.0",
+ "integrity": "sha256-N6efXByUeg848ompEqJfVvZuZPfdRYDGlTDFn0G0Oh8=",
+ "dependencies": [
+ "argonaut-core",
+ "arrays",
+ "effect",
+ "foreign-object",
+ "identity",
+ "integers",
+ "maybe",
+ "nonempty",
+ "ordered-collections",
+ "prelude",
+ "record"
+ ]
+ },
+ "argonaut-core": {
+ "type": "registry",
+ "version": "7.0.0",
+ "integrity": "sha256-RC82GfAjItydxrO24cdX373KHVZiLqybu19b5X8u7B4=",
+ "dependencies": [
+ "arrays",
+ "control",
+ "either",
+ "foreign-object",
+ "functions",
+ "gen",
+ "maybe",
+ "nonempty",
+ "prelude",
+ "strings",
+ "tailrec"
+ ]
+ },
+ "arraybuffer-types": {
+ "type": "registry",
+ "version": "3.0.2",
+ "integrity": "sha256-mQKokysYVkooS4uXbO+yovmV/s8b138Ws3zQvOwIHRA=",
+ "dependencies": []
+ },
+ "arrays": {
+ "type": "registry",
+ "version": "7.3.0",
+ "integrity": "sha256-tmcklBlc/muUtUfr9RapdCPwnlQeB3aSrC4dK85gQlc=",
+ "dependencies": [
+ "bifunctors",
+ "control",
+ "foldable-traversable",
+ "functions",
+ "maybe",
+ "nonempty",
+ "partial",
+ "prelude",
+ "safe-coerce",
+ "st",
+ "tailrec",
+ "tuples",
+ "unfoldable",
+ "unsafe-coerce"
+ ]
+ },
+ "b64": {
+ "type": "registry",
+ "version": "0.0.8",
+ "integrity": "sha256-3QkOhnewZIqhfn0wIU6Zde6Q/LfrS59kgSOcQQaG0gM=",
+ "dependencies": [
+ "arraybuffer-types",
+ "either",
+ "encoding",
+ "enums",
+ "exceptions",
+ "functions",
+ "partial",
+ "prelude",
+ "strings"
+ ]
+ },
+ "bifunctors": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-/gZwC9YhNxZNQpnHa5BIYerCGM2jeX9ukZiEvYxm5Nw=",
+ "dependencies": [
+ "const",
+ "either",
+ "newtype",
+ "prelude",
+ "tuples"
+ ]
+ },
+ "console": {
+ "type": "registry",
+ "version": "6.1.0",
+ "integrity": "sha256-CxmAzjgyuGDmt9FZW51VhV6rBPwR6o0YeKUzA9rSzcM=",
+ "dependencies": [
+ "effect",
+ "prelude"
+ ]
+ },
+ "const": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-tNrxDW8D8H4jdHE2HiPzpLy08zkzJMmGHdRqt5BQuTc=",
+ "dependencies": [
+ "invariant",
+ "newtype",
+ "prelude"
+ ]
+ },
+ "contravariant": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-TP+ooAp3vvmdjfQsQJSichF5B4BPDHp3wAJoWchip6c=",
+ "dependencies": [
+ "const",
+ "either",
+ "newtype",
+ "prelude",
+ "tuples"
+ ]
+ },
+ "control": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-sH7Pg9E96JCPF9PIA6oQ8+BjTyO/BH1ZuE/bOcyj4Jk=",
+ "dependencies": [
+ "newtype",
+ "prelude"
+ ]
+ },
+ "datetime": {
+ "type": "registry",
+ "version": "6.1.0",
+ "integrity": "sha256-g/5X5BBegQWLpI9IWD+sY6mcaYpzzlW5lz5NBzaMtyI=",
+ "dependencies": [
+ "bifunctors",
+ "control",
+ "either",
+ "enums",
+ "foldable-traversable",
+ "functions",
+ "gen",
+ "integers",
+ "lists",
+ "maybe",
+ "newtype",
+ "numbers",
+ "ordered-collections",
+ "partial",
+ "prelude",
+ "tuples"
+ ]
+ },
+ "distributive": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-HTDdmEnzigMl+02SJB88j+gAXDx9VKsbvR4MJGDPbOQ=",
+ "dependencies": [
+ "identity",
+ "newtype",
+ "prelude",
+ "tuples",
+ "type-equality"
+ ]
+ },
+ "effect": {
+ "type": "registry",
+ "version": "4.0.0",
+ "integrity": "sha256-eBtZu+HZcMa5HilvI6kaDyVX3ji8p0W9MGKy2K4T6+M=",
+ "dependencies": [
+ "prelude"
+ ]
+ },
+ "either": {
+ "type": "registry",
+ "version": "6.1.0",
+ "integrity": "sha256-6hgTPisnMWVwQivOu2PKYcH8uqjEOOqDyaDQVUchTpY=",
+ "dependencies": [
+ "control",
+ "invariant",
+ "maybe",
+ "prelude"
+ ]
+ },
+ "encoding": {
+ "type": "registry",
+ "version": "0.0.9",
+ "integrity": "sha256-vtyUO06Jww8pFl4wRekPd1YpJl2XuQXcaNXQgHtG8Tk=",
+ "dependencies": [
+ "arraybuffer-types",
+ "effect",
+ "either",
+ "exceptions",
+ "functions",
+ "prelude"
+ ]
+ },
+ "enums": {
+ "type": "registry",
+ "version": "6.0.1",
+ "integrity": "sha256-HWaD73JFLorc4A6trKIRUeDMdzE+GpkJaEOM1nTNkC8=",
+ "dependencies": [
+ "control",
+ "either",
+ "gen",
+ "maybe",
+ "newtype",
+ "nonempty",
+ "partial",
+ "prelude",
+ "tuples",
+ "unfoldable"
+ ]
+ },
+ "exceptions": {
+ "type": "registry",
+ "version": "6.1.0",
+ "integrity": "sha256-K0T89IHtF3vBY7eSAO7eDOqSb2J9kZGAcDN5+IKsF8E=",
+ "dependencies": [
+ "effect",
+ "either",
+ "maybe",
+ "prelude"
+ ]
+ },
+ "exists": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-A0JQHpTfo1dNOj9U5/Fd3xndlRSE0g2IQWOGor2yXn8=",
+ "dependencies": [
+ "unsafe-coerce"
+ ]
+ },
+ "ezfetch": {
+ "type": "registry",
+ "version": "1.1.0",
+ "integrity": "sha256-EA8KHFS6PMuOdZiFt7h0E5D5yCf7/sWAfsRJoCE/xP8=",
+ "dependencies": [
+ "aff",
+ "aff-promise",
+ "arraybuffer-types",
+ "b64",
+ "bifunctors",
+ "effect",
+ "either",
+ "exceptions",
+ "foldable-traversable",
+ "foreign",
+ "foreign-object",
+ "integers",
+ "maybe",
+ "newtype",
+ "node-buffer",
+ "node-streams",
+ "nullable",
+ "numbers",
+ "ordered-collections",
+ "prelude",
+ "record",
+ "simple-json",
+ "transformers",
+ "tuples",
+ "typelevel-prelude",
+ "unsafe-coerce",
+ "url-immutable",
+ "web-file",
+ "web-streams"
+ ]
+ },
+ "filterable": {
+ "type": "registry",
+ "version": "5.0.0",
+ "integrity": "sha256-cCojJHRnTmpY1j1kegI4CFwghdQ2Fm/8dzM8IlC+lng=",
+ "dependencies": [
+ "arrays",
+ "either",
+ "foldable-traversable",
+ "identity",
+ "lists",
+ "ordered-collections"
+ ]
+ },
+ "foldable-traversable": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-fLeqRYM4jUrZD5H4WqcwUgzU7XfYkzO4zhgtNc3jcWM=",
+ "dependencies": [
+ "bifunctors",
+ "const",
+ "control",
+ "either",
+ "functors",
+ "identity",
+ "maybe",
+ "newtype",
+ "orders",
+ "prelude",
+ "tuples"
+ ]
+ },
+ "foreign": {
+ "type": "registry",
+ "version": "7.0.0",
+ "integrity": "sha256-1ORiqoS3HW+qfwSZAppHPWy4/6AQysxZ2t29jcdUMNA=",
+ "dependencies": [
+ "either",
+ "functions",
+ "identity",
+ "integers",
+ "lists",
+ "maybe",
+ "prelude",
+ "strings",
+ "transformers"
+ ]
+ },
+ "foreign-object": {
+ "type": "registry",
+ "version": "4.1.0",
+ "integrity": "sha256-q24okj6mT+yGHYQ+ei/pYPj5ih6sTbu7eDv/WU56JVo=",
+ "dependencies": [
+ "arrays",
+ "foldable-traversable",
+ "functions",
+ "gen",
+ "lists",
+ "maybe",
+ "prelude",
+ "st",
+ "tailrec",
+ "tuples",
+ "typelevel-prelude",
+ "unfoldable"
+ ]
+ },
+ "functions": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-adMyJNEnhGde2unHHAP79gPtlNjNqzgLB8arEOn9hLI=",
+ "dependencies": [
+ "prelude"
+ ]
+ },
+ "functors": {
+ "type": "registry",
+ "version": "5.0.0",
+ "integrity": "sha256-zfPWWYisbD84MqwpJSZFlvM6v86McM68ob8p9s27ywU=",
+ "dependencies": [
+ "bifunctors",
+ "const",
+ "contravariant",
+ "control",
+ "distributive",
+ "either",
+ "invariant",
+ "maybe",
+ "newtype",
+ "prelude",
+ "profunctor",
+ "tuples",
+ "unsafe-coerce"
+ ]
+ },
+ "gen": {
+ "type": "registry",
+ "version": "4.0.0",
+ "integrity": "sha256-f7yzAXWwr+xnaqEOcvyO3ezKdoes8+WXWdXIHDBCAPI=",
+ "dependencies": [
+ "either",
+ "foldable-traversable",
+ "identity",
+ "maybe",
+ "newtype",
+ "nonempty",
+ "prelude",
+ "tailrec",
+ "tuples",
+ "unfoldable"
+ ]
+ },
+ "identity": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-4wY0XZbAksjY6UAg99WkuKyJlQlWAfTi2ssadH0wVMY=",
+ "dependencies": [
+ "control",
+ "invariant",
+ "newtype",
+ "prelude"
+ ]
+ },
+ "integers": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-sf+sK26R1hzwl3NhXR7WAu9zCDjQnfoXwcyGoseX158=",
+ "dependencies": [
+ "maybe",
+ "numbers",
+ "prelude"
+ ]
+ },
+ "invariant": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-RGWWyYrz0Hs1KjPDA+87Kia67ZFBhfJ5lMGOMCEFoLo=",
+ "dependencies": [
+ "control",
+ "prelude"
+ ]
+ },
+ "js-date": {
+ "type": "registry",
+ "version": "8.0.0",
+ "integrity": "sha256-6TVF4DWg5JL+jRAsoMssYw8rgOVALMUHT1CuNZt8NRo=",
+ "dependencies": [
+ "datetime",
+ "effect",
+ "exceptions",
+ "foreign",
+ "integers",
+ "now"
+ ]
+ },
+ "js-promise": {
+ "type": "registry",
+ "version": "1.0.0",
+ "integrity": "sha256-kXNo5g9RJgPdrTuKRe5oG2kBIwPp+j5VDPDplqZBJzQ=",
+ "dependencies": [
+ "effect",
+ "exceptions",
+ "foldable-traversable",
+ "functions",
+ "maybe",
+ "prelude"
+ ]
+ },
+ "lazy": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-lMsfFOnlqfe4KzRRiW8ot5ge6HtcU3Eyh2XkXcP5IgU=",
+ "dependencies": [
+ "control",
+ "foldable-traversable",
+ "invariant",
+ "prelude"
+ ]
+ },
+ "lists": {
+ "type": "registry",
+ "version": "7.0.0",
+ "integrity": "sha256-EKF15qYqucuXP2lT/xPxhqy58f0FFT6KHdIB/yBOayI=",
+ "dependencies": [
+ "bifunctors",
+ "control",
+ "foldable-traversable",
+ "lazy",
+ "maybe",
+ "newtype",
+ "nonempty",
+ "partial",
+ "prelude",
+ "tailrec",
+ "tuples",
+ "unfoldable"
+ ]
+ },
+ "maybe": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-5cCIb0wPwbat2PRkQhUeZO0jcAmf8jCt2qE0wbC3v2Q=",
+ "dependencies": [
+ "control",
+ "invariant",
+ "newtype",
+ "prelude"
+ ]
+ },
+ "media-types": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-n/4FoGBasbVSYscGVRSyBunQ6CZbL3jsYL+Lp01mc9k=",
+ "dependencies": [
+ "newtype",
+ "prelude"
+ ]
+ },
+ "newtype": {
+ "type": "registry",
+ "version": "5.0.0",
+ "integrity": "sha256-gdrQu8oGe9eZE6L3wOI8ql/igOg+zEGB5ITh2g+uttw=",
+ "dependencies": [
+ "prelude",
+ "safe-coerce"
+ ]
+ },
+ "node-buffer": {
+ "type": "registry",
+ "version": "9.0.0",
+ "integrity": "sha256-PWE2DJ5ruBLCmeA/fUiuySEFmUJ/VuRfyrnCuVZBlu4=",
+ "dependencies": [
+ "arraybuffer-types",
+ "effect",
+ "maybe",
+ "nullable",
+ "st",
+ "unsafe-coerce"
+ ]
+ },
+ "node-event-emitter": {
+ "type": "registry",
+ "version": "3.0.0",
+ "integrity": "sha256-Qw0MjsT4xRH2j2i4K8JmRjcMKnH5z1Cw39t00q4LE4w=",
+ "dependencies": [
+ "effect",
+ "either",
+ "functions",
+ "maybe",
+ "nullable",
+ "prelude",
+ "unsafe-coerce"
+ ]
+ },
+ "node-fs": {
+ "type": "registry",
+ "version": "9.2.0",
+ "integrity": "sha256-Sg0vkXycEzkEerX6hLccz21Ygd9w1+QSk1thotRZPGI=",
+ "dependencies": [
+ "datetime",
+ "effect",
+ "either",
+ "enums",
+ "exceptions",
+ "functions",
+ "integers",
+ "js-date",
+ "maybe",
+ "node-buffer",
+ "node-path",
+ "node-streams",
+ "nullable",
+ "partial",
+ "prelude",
+ "strings",
+ "unsafe-coerce"
+ ]
+ },
+ "node-net": {
+ "type": "registry",
+ "version": "5.1.0",
+ "integrity": "sha256-WnYf9RoPBJrTUAEkzc0NbIhOqN0lKbhyDf0JNgZjAKY=",
+ "dependencies": [
+ "effect",
+ "either",
+ "exceptions",
+ "foreign",
+ "maybe",
+ "node-buffer",
+ "node-event-emitter",
+ "node-fs",
+ "nullable",
+ "options",
+ "prelude",
+ "transformers"
+ ]
+ },
+ "node-path": {
+ "type": "registry",
+ "version": "5.0.0",
+ "integrity": "sha256-pd82nQ+2l5UThzaxPdKttgDt7xlsgIDLpPG0yxDEdyE=",
+ "dependencies": [
+ "effect"
+ ]
+ },
+ "node-streams": {
+ "type": "registry",
+ "version": "9.0.0",
+ "integrity": "sha256-2n6dq7YWleTDmD1Kur/ul7Cn08IvWrScgPf+0PgX2TQ=",
+ "dependencies": [
+ "aff",
+ "effect",
+ "either",
+ "exceptions",
+ "node-buffer",
+ "node-event-emitter",
+ "nullable",
+ "prelude"
+ ]
+ },
+ "nonempty": {
+ "type": "registry",
+ "version": "7.0.0",
+ "integrity": "sha256-54ablJZUHGvvlTJzi3oXyPCuvY6zsrWJuH/dMJ/MFLs=",
+ "dependencies": [
+ "control",
+ "foldable-traversable",
+ "maybe",
+ "prelude",
+ "tuples",
+ "unfoldable"
+ ]
+ },
+ "now": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-xZ7x37ZMREfs6GCDw/h+FaKHV/3sPWmtqBZRGTxybQY=",
+ "dependencies": [
+ "datetime",
+ "effect"
+ ]
+ },
+ "nullable": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-yiGBVl3AD+Guy4kNWWeN+zl1gCiJK+oeIFtZtPCw4+o=",
+ "dependencies": [
+ "effect",
+ "functions",
+ "maybe"
+ ]
+ },
+ "numbers": {
+ "type": "registry",
+ "version": "9.0.1",
+ "integrity": "sha256-/9M6aeMDBdB4cwYDeJvLFprAHZ49EbtKQLIJsneXLIk=",
+ "dependencies": [
+ "functions",
+ "maybe"
+ ]
+ },
+ "options": {
+ "type": "registry",
+ "version": "7.0.0",
+ "integrity": "sha256-treC6h+jvzcWhplPaF/aMENCOx+JGk+ysa5pL1BGHtg=",
+ "dependencies": [
+ "contravariant",
+ "foreign",
+ "foreign-object",
+ "maybe",
+ "tuples"
+ ]
+ },
+ "ordered-collections": {
+ "type": "registry",
+ "version": "3.2.0",
+ "integrity": "sha256-o9jqsj5rpJmMdoe/zyufWHFjYYFTTsJpgcuCnqCO6PM=",
+ "dependencies": [
+ "arrays",
+ "foldable-traversable",
+ "gen",
+ "lists",
+ "maybe",
+ "partial",
+ "prelude",
+ "st",
+ "tailrec",
+ "tuples",
+ "unfoldable"
+ ]
+ },
+ "orders": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-nBA0g3/ai0euH8q9pSbGqk53W2q6agm/dECZTHcoink=",
+ "dependencies": [
+ "newtype",
+ "prelude"
+ ]
+ },
+ "parallel": {
+ "type": "registry",
+ "version": "7.0.0",
+ "integrity": "sha256-gUC9i4Txnx9K9RcMLsjujbwZz6BB1bnE2MLvw4GIw5o=",
+ "dependencies": [
+ "control",
+ "effect",
+ "either",
+ "foldable-traversable",
+ "functors",
+ "maybe",
+ "newtype",
+ "prelude",
+ "profunctor",
+ "refs",
+ "transformers"
+ ]
+ },
+ "partial": {
+ "type": "registry",
+ "version": "4.0.0",
+ "integrity": "sha256-fwXerld6Xw1VkReh8yeQsdtLVrjfGiVuC5bA1Wyo/J4=",
+ "dependencies": []
+ },
+ "prelude": {
+ "type": "registry",
+ "version": "6.0.1",
+ "integrity": "sha256-o8p6SLYmVPqzXZhQFd2hGAWEwBoXl1swxLG/scpJ0V0=",
+ "dependencies": []
+ },
+ "profunctor": {
+ "type": "registry",
+ "version": "6.0.1",
+ "integrity": "sha256-E58hSYdJvF2Qjf9dnWLPlJKh2Z2fLfFLkQoYi16vsFk=",
+ "dependencies": [
+ "control",
+ "distributive",
+ "either",
+ "exists",
+ "invariant",
+ "newtype",
+ "prelude",
+ "tuples"
+ ]
+ },
+ "record": {
+ "type": "registry",
+ "version": "4.0.0",
+ "integrity": "sha256-Za5U85bTRJEfGK5Sk4hM41oXy84YQI0I8TL3WUn1Qzg=",
+ "dependencies": [
+ "functions",
+ "prelude",
+ "unsafe-coerce"
+ ]
+ },
+ "refs": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-Vgwne7jIbD3ZMoLNNETLT8Litw6lIYo3MfYNdtYWj9s=",
+ "dependencies": [
+ "effect",
+ "prelude"
+ ]
+ },
+ "safe-coerce": {
+ "type": "registry",
+ "version": "2.0.0",
+ "integrity": "sha256-a1ibQkiUcbODbLE/WAq7Ttbbh9ex+x33VCQ7GngKudU=",
+ "dependencies": [
+ "unsafe-coerce"
+ ]
+ },
+ "simple-json": {
+ "type": "registry",
+ "version": "9.0.0",
+ "integrity": "sha256-K3RJaThqsszTd+TEklzZmAdDqvIHWgXIfKqlsoykU1c=",
+ "dependencies": [
+ "arrays",
+ "exceptions",
+ "foreign",
+ "foreign-object",
+ "nullable",
+ "prelude",
+ "record",
+ "typelevel-prelude",
+ "variant"
+ ]
+ },
+ "st": {
+ "type": "registry",
+ "version": "6.2.0",
+ "integrity": "sha256-z9X0WsOUlPwNx9GlCC+YccCyz8MejC8Wb0C4+9fiBRY=",
+ "dependencies": [
+ "partial",
+ "prelude",
+ "tailrec",
+ "unsafe-coerce"
+ ]
+ },
+ "strings": {
+ "type": "registry",
+ "version": "6.0.1",
+ "integrity": "sha256-WssD3DbX4OPzxSdjvRMX0yvc9+pS7n5gyPv5I2Trb7k=",
+ "dependencies": [
+ "arrays",
+ "control",
+ "either",
+ "enums",
+ "foldable-traversable",
+ "gen",
+ "integers",
+ "maybe",
+ "newtype",
+ "nonempty",
+ "partial",
+ "prelude",
+ "tailrec",
+ "tuples",
+ "unfoldable",
+ "unsafe-coerce"
+ ]
+ },
+ "stringutils": {
+ "type": "registry",
+ "version": "0.0.12",
+ "integrity": "sha256-t63QWBlp49U0nRqUcFryKflSJsNKGTQAHKjn24/+ooI=",
+ "dependencies": [
+ "arrays",
+ "integers",
+ "maybe",
+ "partial",
+ "prelude",
+ "strings"
+ ]
+ },
+ "tailrec": {
+ "type": "registry",
+ "version": "6.1.0",
+ "integrity": "sha256-Xx19ECVDRrDWpz9D2GxQHHV89vd61dnXxQm0IcYQHGk=",
+ "dependencies": [
+ "bifunctors",
+ "effect",
+ "either",
+ "identity",
+ "maybe",
+ "partial",
+ "prelude",
+ "refs"
+ ]
+ },
+ "transformers": {
+ "type": "registry",
+ "version": "6.1.0",
+ "integrity": "sha256-3Bm+Z6tsC/paG888XkywDngJ2JMos+JfOhRlkVfb7gI=",
+ "dependencies": [
+ "control",
+ "distributive",
+ "effect",
+ "either",
+ "exceptions",
+ "foldable-traversable",
+ "identity",
+ "lazy",
+ "maybe",
+ "newtype",
+ "prelude",
+ "st",
+ "tailrec",
+ "tuples",
+ "unfoldable"
+ ]
+ },
+ "tuples": {
+ "type": "registry",
+ "version": "7.0.0",
+ "integrity": "sha256-1rXgTomes9105BjgXqIw0FL6Fz1lqqUTLWOumhWec1M=",
+ "dependencies": [
+ "control",
+ "invariant",
+ "prelude"
+ ]
+ },
+ "type-equality": {
+ "type": "registry",
+ "version": "4.0.1",
+ "integrity": "sha256-Hs9D6Y71zFi/b+qu5NSbuadUQXe5iv5iWx0226vOHUw=",
+ "dependencies": []
+ },
+ "typelevel-prelude": {
+ "type": "registry",
+ "version": "7.0.0",
+ "integrity": "sha256-uFF2ph+vHcQpfPuPf2a3ukJDFmLhApmkpTMviHIWgJM=",
+ "dependencies": [
+ "prelude",
+ "type-equality"
+ ]
+ },
+ "unfoldable": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-JtikvJdktRap7vr/K4ITlxUX1QexpnqBq0G/InLr6eg=",
+ "dependencies": [
+ "foldable-traversable",
+ "maybe",
+ "partial",
+ "prelude",
+ "tuples"
+ ]
+ },
+ "unsafe-coerce": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-IqIYW4Vkevn8sI+6aUwRGvd87tVL36BBeOr0cGAE7t0=",
+ "dependencies": []
+ },
+ "url-immutable": {
+ "type": "registry",
+ "version": "1.0.0",
+ "integrity": "sha256-6uCg5k4fjrqyTYUYKTmcykXgXCJKsvVpzx+gZJczAx0=",
+ "dependencies": [
+ "arrays",
+ "bifunctors",
+ "either",
+ "filterable",
+ "foldable-traversable",
+ "foreign",
+ "integers",
+ "maybe",
+ "newtype",
+ "nullable",
+ "ordered-collections",
+ "partial",
+ "prelude",
+ "simple-json",
+ "strings",
+ "stringutils",
+ "transformers",
+ "tuples"
+ ]
+ },
+ "variant": {
+ "type": "registry",
+ "version": "8.0.0",
+ "integrity": "sha256-SR//zQDg2dnbB8ZHslcxieUkCeNlbMToapvmh9onTtw=",
+ "dependencies": [
+ "enums",
+ "lists",
+ "maybe",
+ "partial",
+ "prelude",
+ "record",
+ "tuples",
+ "unsafe-coerce"
+ ]
+ },
+ "web-dom": {
+ "type": "registry",
+ "version": "6.0.0",
+ "integrity": "sha256-1kSKWFDI4LupdmpjK01b1MMxDFW7jvatEgPgVmCmSBQ=",
+ "dependencies": [
+ "web-events"
+ ]
+ },
+ "web-events": {
+ "type": "registry",
+ "version": "4.0.0",
+ "integrity": "sha256-YDt8b6u1tzGtnWyNRodne57iO8FNSGPaTCVzBUyUn4k=",
+ "dependencies": [
+ "datetime",
+ "enums",
+ "foreign",
+ "nullable"
+ ]
+ },
+ "web-file": {
+ "type": "registry",
+ "version": "4.0.0",
+ "integrity": "sha256-1h5jPBkvjY71jLEdwVadXCx86/2inNoMBO//Rd3eCSU=",
+ "dependencies": [
+ "foreign",
+ "media-types",
+ "web-dom"
+ ]
+ },
+ "web-streams": {
+ "type": "registry",
+ "version": "4.0.0",
+ "integrity": "sha256-02HgXIk6R+pU9fWOX42krukAI1QkCbLKcCv3b4Jq6WI=",
+ "dependencies": [
+ "arraybuffer-types",
+ "effect",
+ "exceptions",
+ "js-promise",
+ "nullable",
+ "prelude",
+ "tuples"
+ ]
+ }
+ }
+}
diff --git a/spago.yaml b/spago.yaml
index e50fb89..769506c 100644
--- a/spago.yaml
+++ b/spago.yaml
@@ -1,9 +1,22 @@
package:
name: tower
dependencies:
+ - aff
+ - argonaut-codecs
+ - argonaut-core
- console
- effect
+ - ezfetch
+ - maybe
+ - node-net
+ - node-streams
+ - nullable
- prelude
+ - strings
+ - transformers
+ - tuples
+ - url-immutable
+ - web-streams
test:
main: Test.Main
dependencies: []
diff --git a/src/Data.String.Lower.purs b/src/Data.String.Lower.purs
new file mode 100644
index 0000000..5ea595e
--- /dev/null
+++ b/src/Data.String.Lower.purs
@@ -0,0 +1,21 @@
+module Data.String.Lower where
+
+import Prelude
+
+import Data.Generic.Rep (class Generic)
+import Data.String as String
+
+newtype StringLower = StringLower String
+
+derive instance Generic StringLower _
+derive newtype instance Show StringLower
+derive newtype instance Eq StringLower
+derive newtype instance Ord StringLower
+derive newtype instance Monoid StringLower
+derive newtype instance Semigroup StringLower
+
+fromString :: String -> StringLower
+fromString = StringLower <<< String.toLower
+
+toString :: StringLower -> String
+toString (StringLower a) = a
diff --git a/src/Tower.Request.Method.purs b/src/Tower.Request.Method.purs
new file mode 100644
index 0000000..f9f1516
--- /dev/null
+++ b/src/Tower.Request.Method.purs
@@ -0,0 +1,38 @@
+module Tower.Request.Method where
+
+import Prelude
+
+import Data.Generic.Rep (class Generic)
+import Data.Maybe (Maybe(..))
+import Data.Show.Generic (genericShow)
+import Data.String as String
+
+data Method = GET | POST | PUT | PATCH | DELETE | OPTIONS | TRACE | CONNECT
+derive instance Generic Method _
+derive instance Eq Method
+instance Show Method where show = genericShow
+
+methodToString :: Method -> String
+methodToString GET = "GET"
+methodToString POST = "POST"
+methodToString PUT = "PUT"
+methodToString PATCH = "PATCH"
+methodToString DELETE = "DELETE"
+methodToString OPTIONS = "OPTIONS"
+methodToString TRACE = "TRACE"
+methodToString CONNECT = "CONNECT"
+
+methodFromString :: String -> Maybe Method
+methodFromString =
+ let
+ go "GET" = Just GET
+ go "POST" = Just POST
+ go "PUT" = Just PUT
+ go "PATCH" = Just PATCH
+ go "DELETE" = Just DELETE
+ go "OPTIONS" = Just OPTIONS
+ go "TRACE" = Just TRACE
+ go "CONNECT" = Just CONNECT
+ go _ = Nothing
+ in
+ go <<< String.toUpper
diff --git a/src/Tower.Request.Parts.Body.purs b/src/Tower.Request.Parts.Body.purs
new file mode 100644
index 0000000..8617e85
--- /dev/null
+++ b/src/Tower.Request.Parts.Body.purs
@@ -0,0 +1,18 @@
+module Tower.Request.Parts.Body where
+
+import Prelude
+
+import Data.Generic.Rep (class Generic)
+import Data.Newtype (class Newtype)
+import Node.Stream as Stream
+
+newtype Json a = Json a
+derive instance Generic (Json a) _
+derive instance Newtype (Json a) _
+derive newtype instance (Eq a) => Eq (Json a)
+derive newtype instance (Ord a) => Ord (Json a)
+derive newtype instance (Show a) => Show (Json a)
+
+newtype Stream = Stream (Stream.Readable ())
+derive instance Generic Stream _
+derive instance Newtype Stream _
diff --git a/src/Tower.Request.Parts.Class.purs b/src/Tower.Request.Parts.Class.purs
new file mode 100644
index 0000000..0dee8c3
--- /dev/null
+++ b/src/Tower.Request.Parts.Class.purs
@@ -0,0 +1,149 @@
+module Tower.Request.Parts.Class (class RequestParts, extractRequestParts, module Parts.Method, module Parts.Body, module Path.Parts) where
+
+import Prelude
+
+import Control.Alternative (guard)
+import Control.Monad.Except (ExceptT(..), runExceptT)
+import Control.Monad.Maybe.Trans (MaybeT(..), runMaybeT)
+import Control.Monad.Trans.Class (lift)
+import Data.Argonaut.Decode (class DecodeJson, decodeJson)
+import Data.Array as Array
+import Data.Bifunctor (lmap)
+import Data.Either (Either(..))
+import Data.Maybe (Maybe(..))
+import Data.Newtype (class Newtype, wrap)
+import Data.Tuple.Nested (type (/\), (/\))
+import Data.URL as URL
+import Effect.Aff (Aff)
+import Effect.Class (liftEffect)
+import Node.Buffer (Buffer)
+import Tower.Request (Request)
+import Tower.Request as Request
+import Tower.Request.Method (Method)
+import Tower.Request.Method as Method
+import Tower.Request.Parts.Body (Json(..), Stream(..))
+import Tower.Request.Parts.Body (Json(..), Stream(..)) as Parts.Body
+import Tower.Request.Parts.Method (Connect, Delete, Get, Options, Patch, Post, Put, Trace)
+import Tower.Request.Parts.Method (Get(..), Post(..), Put(..), Patch(..), Delete(..), Trace(..), Options(..), Connect(..)) as Parts.Method
+import Tower.Request.Parts.Path (Path(..)) as Path.Parts
+import Tower.Request.Parts.Path (class PathParts, Path(..), extractPathParts)
+import Tower.Response (Response)
+import Tower.Response as Response
+
+extractMethod :: forall @t a. RequestParts a => Newtype t a => Method -> Request -> Aff (Either Response (Maybe t))
+extractMethod method r =
+ if Request.method r == method then
+ extractRequestParts @a r
+ # ExceptT
+ # MaybeT
+ <#> wrap
+ # runMaybeT
+ # runExceptT
+ else
+ pure $ Right Nothing
+
+class RequestParts a where
+ extractRequestParts :: Request -> Aff (Either Response (Maybe a))
+
+instance RequestParts Unit where
+ extractRequestParts _ = pure unit # runMaybeT # runExceptT
+
+instance RequestParts Request where
+ extractRequestParts r = pure r # runMaybeT # runExceptT
+
+instance RequestParts String where
+ extractRequestParts r =
+ Request.bodyString r
+ <#> lmap (const $ Response.fromStatus 500)
+ # ExceptT
+ # lift
+ # runMaybeT
+ # runExceptT
+
+instance PathParts a b => RequestParts (Path a b) where
+ extractRequestParts r =
+ let
+ segments = Request.url r # URL.path # case _ of
+ URL.PathAbsolute a -> a
+ URL.PathRelative a -> a
+ _ -> []
+ extract = extractPathParts @a @b (Request.url r)
+ ensureConsumed (leftover /\ x) = guard (Array.null leftover) $> x
+ in
+ segments
+ # extract
+ # Right
+ # MaybeT
+ >>= ensureConsumed
+ <#> Path
+ # runMaybeT
+ # pure
+
+instance (DecodeJson a) => RequestParts (Json a) where
+ extractRequestParts r =
+ let
+ jsonBody =
+ Request.bodyJSON r
+ <#> lmap (const $ Response.fromStatus 500)
+ # ExceptT
+ # lift
+ decode j =
+ decodeJson j
+ # lmap (const $ Response.fromStatus 400)
+ # pure
+ # ExceptT
+ # lift
+ in
+ jsonBody >>= decode <#> Json # runMaybeT # runExceptT
+
+instance RequestParts Buffer where
+ extractRequestParts r =
+ let
+ bufBody =
+ Request.bodyBuffer r
+ <#> lmap (const $ Response.fromStatus 500)
+ # ExceptT
+ # lift
+ in
+ bufBody # runMaybeT # runExceptT
+
+instance RequestParts Stream where
+ extractRequestParts r =
+ let
+ streamBody =
+ Request.bodyReadable r
+ <#> lmap (const $ Response.fromStatus 500)
+ # ExceptT
+ # lift
+ in
+ streamBody <#> Stream # runMaybeT # runExceptT # liftEffect
+
+instance (RequestParts a) => RequestParts (Get a) where
+ extractRequestParts = extractMethod @(Get a) Method.GET
+
+instance (RequestParts a) => RequestParts (Post a) where
+ extractRequestParts = extractMethod @(Post a) Method.POST
+
+instance (RequestParts a) => RequestParts (Put a) where
+ extractRequestParts = extractMethod @(Put a) Method.PUT
+
+instance (RequestParts a) => RequestParts (Patch a) where
+ extractRequestParts = extractMethod @(Patch a) Method.PATCH
+
+instance (RequestParts a) => RequestParts (Delete a) where
+ extractRequestParts = extractMethod @(Delete a) Method.DELETE
+
+instance (RequestParts a) => RequestParts (Options a) where
+ extractRequestParts = extractMethod @(Options a) Method.OPTIONS
+
+instance (RequestParts a) => RequestParts (Connect a) where
+ extractRequestParts = extractMethod @(Connect a) Method.CONNECT
+
+instance (RequestParts a) => RequestParts (Trace a) where
+ extractRequestParts = extractMethod @(Trace a) Method.TRACE
+
+instance (RequestParts a, RequestParts b) => RequestParts (a /\ b) where
+ extractRequestParts r = runExceptT $ runMaybeT do
+ a <- extractRequestParts @a r # ExceptT # MaybeT
+ b <- extractRequestParts @b r # ExceptT # MaybeT
+ pure $ a /\ b
diff --git a/src/Tower.Request.Parts.Method.purs b/src/Tower.Request.Parts.Method.purs
new file mode 100644
index 0000000..fedd557
--- /dev/null
+++ b/src/Tower.Request.Parts.Method.purs
@@ -0,0 +1,62 @@
+module Tower.Request.Parts.Method where
+
+import Prelude
+
+import Data.Generic.Rep (class Generic)
+import Data.Newtype (class Newtype)
+
+newtype Get a = Get a
+derive instance Generic (Get a) _
+derive instance Newtype (Get a) _
+derive newtype instance (Eq a) => Eq (Get a)
+derive newtype instance (Ord a) => Ord (Get a)
+derive newtype instance (Show a) => Show (Get a)
+
+newtype Post a = Post a
+derive instance Generic (Post a) _
+derive instance Newtype (Post a) _
+derive newtype instance (Eq a) => Eq (Post a)
+derive newtype instance (Ord a) => Ord (Post a)
+derive newtype instance (Show a) => Show (Post a)
+
+newtype Put a = Put a
+derive instance Generic (Put a) _
+derive instance Newtype (Put a) _
+derive newtype instance (Eq a) => Eq (Put a)
+derive newtype instance (Ord a) => Ord (Put a)
+derive newtype instance (Show a) => Show (Put a)
+
+newtype Patch a = Patch a
+derive instance Generic (Patch a) _
+derive instance Newtype (Patch a) _
+derive newtype instance (Eq a) => Eq (Patch a)
+derive newtype instance (Ord a) => Ord (Patch a)
+derive newtype instance (Show a) => Show (Patch a)
+
+newtype Delete a = Delete a
+derive instance Generic (Delete a) _
+derive instance Newtype (Delete a) _
+derive newtype instance (Eq a) => Eq (Delete a)
+derive newtype instance (Ord a) => Ord (Delete a)
+derive newtype instance (Show a) => Show (Delete a)
+
+newtype Options a = Options a
+derive instance Generic (Options a) _
+derive instance Newtype (Options a) _
+derive newtype instance (Eq a) => Eq (Options a)
+derive newtype instance (Ord a) => Ord (Options a)
+derive newtype instance (Show a) => Show (Options a)
+
+newtype Trace a = Trace a
+derive instance Generic (Trace a) _
+derive instance Newtype (Trace a) _
+derive newtype instance (Eq a) => Eq (Trace a)
+derive newtype instance (Ord a) => Ord (Trace a)
+derive newtype instance (Show a) => Show (Trace a)
+
+newtype Connect a = Connect a
+derive instance Generic (Connect a) _
+derive instance Newtype (Connect a) _
+derive newtype instance (Eq a) => Eq (Connect a)
+derive newtype instance (Ord a) => Ord (Connect a)
+derive newtype instance (Show a) => Show (Connect a)
diff --git a/src/Tower.Request.Parts.Path.purs b/src/Tower.Request.Parts.Path.purs
new file mode 100644
index 0000000..12f67f6
--- /dev/null
+++ b/src/Tower.Request.Parts.Path.purs
@@ -0,0 +1,50 @@
+module Tower.Request.Parts.Path where
+
+import Prelude
+
+import Control.Alternative (guard)
+import Data.Array as Array
+import Data.Int as Int
+import Data.Maybe (Maybe(..), fromMaybe)
+import Data.Symbol (class IsSymbol, reflectSymbol)
+import Data.Tuple.Nested (type (/\), (/\))
+import Data.URL (URL)
+import Type.Prelude (Proxy(..))
+
+newtype Path :: Type -> Type -> Type
+newtype Path a b = Path b
+
+data Sep :: Type -> Type -> Type
+data Sep a b
+
+data IgnoreRest :: Type
+data IgnoreRest
+
+infixl 9 type Sep as /
+infixl 9 type IgnoreRest as ...
+
+class PathParts :: forall a. a -> Type -> Constraint
+class PathParts a b | a -> b where
+ extractPathParts :: URL -> Array String -> Maybe (Array String /\ b)
+
+instance (PathParts aa ab, PathParts ba bb) => PathParts (aa / ba) (ab /\ bb) where
+ extractPathParts u segments = do
+ segments' /\ ab <- extractPathParts @aa u segments
+ segments'' /\ bb <- extractPathParts @ba u segments'
+ pure $ segments'' /\ ab /\ bb
+else instance PathParts (...) Unit where
+ extractPathParts _ _ = Just $ [] /\ unit
+else instance PathParts String String where
+ extractPathParts _ segments = do
+ head <- Array.head segments
+ pure $ (fromMaybe [] (Array.tail segments) /\ head)
+else instance PathParts Int Int where
+ extractPathParts _ segments = do
+ head <- Array.head segments
+ a <- Int.fromString head
+ pure $ (fromMaybe [] (Array.tail segments) /\ a)
+else instance (IsSymbol k) => PathParts k Unit where
+ extractPathParts _ segments = do
+ head <- Array.head segments
+ guard $ head == reflectSymbol (Proxy @k)
+ pure $ (fromMaybe [] (Array.tail segments) /\ unit)
diff --git a/src/Tower.Request.purs b/src/Tower.Request.purs
new file mode 100644
index 0000000..d71730b
--- /dev/null
+++ b/src/Tower.Request.purs
@@ -0,0 +1,200 @@
+module Tower.Request (Request, BodyReadableError(..), BodyStringError(..), BodyJSONError(..), BodyBufferError(..), bodyReadable, bodyString, bodyJSON, bodyBuffer, headers, method, address, url, contentType, accept, contentLength, lookupHeader) where
+
+import Prelude
+
+import Control.Monad.Error.Class (throwError, try)
+import Control.Monad.Except (ExceptT(..), runExceptT)
+import Control.Monad.Trans.Class (lift)
+import Data.Argonaut.Core (Json)
+import Data.Argonaut.Core (stringify) as JSON
+import Data.Argonaut.Parser (jsonParser) as JSON
+import Data.Bifunctor (lmap)
+import Data.Either (Either, note)
+import Data.Generic.Rep (class Generic)
+import Data.Int as Int
+import Data.MIME (MIME)
+import Data.MIME as MIME
+import Data.Map (Map)
+import Data.Map as Map
+import Data.Maybe (Maybe(..))
+import Data.Show.Generic (genericShow)
+import Data.String.Lower (StringLower)
+import Data.String.Lower as String.Lower
+import Data.URL (URL)
+import Effect (Effect)
+import Effect.Aff (Aff)
+import Effect.Aff.Class (liftAff)
+import Effect.Class (liftEffect)
+import Effect.Exception (Error)
+import Effect.Exception as Error
+import Effect.Ref (Ref) as Effect
+import Effect.Ref as Ref
+import Node.Buffer (Buffer)
+import Node.Buffer as Buffer
+import Node.Encoding (Encoding(..))
+import Node.Net.Types (IPv4, IPv6, SocketAddress)
+import Node.Stream as Stream
+import Node.Stream.Aff as Stream.Aff
+import Tower.Request.Method (Method)
+
+data BodyReadableError
+ = BodyReadableErrorHasBeenConsumed
+ | BodyReadableErrorEmpty
+
+derive instance Generic BodyReadableError _
+derive instance Eq BodyReadableError
+instance Show BodyReadableError where
+ show = genericShow
+
+data BodyBufferError
+ = BodyBufferErrorReadable BodyReadableError
+ | BodyBufferErrorReading Error
+
+derive instance Generic BodyBufferError _
+instance Eq BodyBufferError where
+ eq (BodyBufferErrorReadable a) (BodyBufferErrorReadable b) = a == b
+ eq (BodyBufferErrorReading a) (BodyBufferErrorReading b) = Error.message a == Error.message b
+ eq _ _ = false
+instance Show BodyBufferError where
+ show = genericShow
+
+data BodyStringError
+ = BodyStringErrorBuffer BodyBufferError
+ | BodyStringErrorNotUTF8
+
+derive instance Generic BodyStringError _
+derive instance Eq BodyStringError
+instance Show BodyStringError where
+ show = genericShow
+
+data BodyJSONError
+ = BodyJSONErrorString BodyStringError
+ | BodyJSONErrorParsing String
+
+derive instance Generic BodyJSONError _
+derive instance Eq BodyJSONError
+instance Show BodyJSONError where
+ show = genericShow
+
+data Body
+ = BodyEmpty
+ | BodyReadable (Stream.Readable ())
+ | BodyReadableConsumed
+ | BodyCached Buffer
+ | BodyCachedString String
+ | BodyCachedJSON Json
+
+data Request =
+ Request
+ { headers :: Map StringLower String
+ , address :: Either (SocketAddress IPv4) (SocketAddress IPv6)
+ , url :: URL
+ , method :: Method
+ , bodyRef :: Effect.Ref Body
+ }
+
+headers :: Request -> Map StringLower String
+headers (Request a) = a.headers
+
+lookupHeader :: String -> Request -> Maybe String
+lookupHeader k (Request a) = Map.lookup (String.Lower.fromString k) a.headers
+
+contentType :: Request -> Maybe MIME
+contentType = lookupHeader "content-type" >>> map MIME.fromString
+
+accept :: Request -> Maybe MIME
+accept = lookupHeader "accept" >>> map MIME.fromString
+
+contentLength :: Request -> Maybe Int
+contentLength = lookupHeader "content-length" >=> Int.fromString
+
+method :: Request -> Method
+method (Request a) = a.method
+
+address :: Request -> Either (SocketAddress IPv4) (SocketAddress IPv6)
+address (Request a) = a.address
+
+url :: Request -> URL
+url (Request a) = a.url
+
+bodyReadable :: Request -> Effect (Either BodyReadableError (Stream.Readable ()))
+bodyReadable (Request {bodyRef}) = runExceptT do
+ body <- liftEffect $ Ref.read bodyRef
+ case body of
+ BodyEmpty -> throwError BodyReadableErrorEmpty
+ BodyReadableConsumed -> throwError BodyReadableErrorHasBeenConsumed
+ BodyReadable r ->
+ Ref.write BodyReadableConsumed bodyRef $> r # lift
+ BodyCached buf -> Stream.readableFromBuffer buf # lift
+ BodyCachedString str -> Stream.readableFromString str UTF8 # lift
+ BodyCachedJSON json -> json # JSON.stringify # flip Buffer.fromString UTF8 >>= Stream.readableFromBuffer # lift
+
+bodyBuffer :: Request -> Aff (Either BodyBufferError Buffer)
+bodyBuffer r@(Request {bodyRef}) =
+ let
+ stream =
+ bodyReadable r
+ # liftEffect
+ <#> lmap BodyBufferErrorReadable
+ # ExceptT
+ readAll s =
+ Stream.Aff.readAll s
+ # liftAff
+ # try
+ <#> lmap BodyBufferErrorReading
+ # ExceptT
+ >>= (liftEffect <<< Buffer.concat)
+ in
+ runExceptT do
+ body <- Ref.read bodyRef # liftEffect
+ case body of
+ BodyCached buf -> pure buf
+ BodyCachedString str -> Buffer.fromString str UTF8 # liftEffect
+ BodyCachedJSON json -> Buffer.fromString (JSON.stringify json) UTF8 # liftEffect
+ _ -> do
+ buf <- stream >>= readAll
+ Ref.write (BodyCached buf) bodyRef $> buf # liftEffect
+
+bodyString :: Request -> Aff (Either BodyStringError String)
+bodyString r@(Request {bodyRef}) =
+ let
+ buf =
+ bodyBuffer r
+ <#> lmap BodyStringErrorBuffer
+ # ExceptT
+ bufString b =
+ Buffer.toString UTF8 b
+ # liftEffect
+ # try
+ <#> lmap (const BodyStringErrorNotUTF8)
+ # ExceptT
+ in
+ runExceptT do
+ body <- Ref.read bodyRef # liftEffect
+ case body of
+ BodyCachedString str -> pure str
+ BodyCachedJSON json -> JSON.stringify json # pure
+ _ -> do
+ str <- buf >>= bufString
+ Ref.write (BodyCachedString str) bodyRef $> str # liftEffect
+
+bodyJSON :: Request -> Aff (Either BodyJSONError Json)
+bodyJSON r@(Request {bodyRef}) =
+ let
+ str =
+ bodyString r
+ <#> lmap BodyJSONErrorString
+ # ExceptT
+ parse s =
+ JSON.jsonParser s
+ # lmap BodyJSONErrorParsing
+ # pure
+ # ExceptT
+ in
+ runExceptT do
+ body <- Ref.read bodyRef # liftEffect
+ case body of
+ BodyCachedJSON j -> pure j
+ _ -> do
+ j <- str >>= parse
+ Ref.write (BodyCachedJSON j) bodyRef $> j # liftEffect
diff --git a/src/Tower.Response.Body.purs b/src/Tower.Response.Body.purs
new file mode 100644
index 0000000..70bbadc
--- /dev/null
+++ b/src/Tower.Response.Body.purs
@@ -0,0 +1,35 @@
+module Tower.Response.Body where
+
+import Prelude
+
+import Data.Argonaut.Core (Json, stringify)
+import Effect (Effect)
+import Effect.Aff.HTTP.Form (Form, RawFormData) as HTTP
+import Effect.Aff.HTTP.Form as HTTP.Form
+import Node.Buffer (Buffer)
+import Node.Stream as Stream
+
+data Body
+ = BodyEmpty
+ | BodyString String
+ | BodyBuffer Buffer
+ | BodyFormData HTTP.RawFormData
+ | BodyReadable (Stream.Readable ())
+
+formBody :: HTTP.Form -> Effect Body
+formBody f = HTTP.Form.toRawFormData f <#> BodyFormData
+
+stringBody :: String -> Body
+stringBody = BodyString
+
+bufferBody :: Buffer -> Body
+bufferBody = BodyBuffer
+
+streamBody :: Stream.Readable () -> Body
+streamBody = BodyReadable
+
+emptyBody :: Body
+emptyBody = BodyEmpty
+
+jsonBody :: Json -> Body
+jsonBody = stringify >>> BodyString
diff --git a/src/Tower.Response.purs b/src/Tower.Response.purs
new file mode 100644
index 0000000..61728b4
--- /dev/null
+++ b/src/Tower.Response.purs
@@ -0,0 +1,40 @@
+module Tower.Response (Response, response, body, status, headers, withHeader, withBody, withStatus, fromStatus, ok, module Body) where
+
+import Prelude
+
+import Data.FoldableWithIndex (foldlWithIndex)
+import Data.Map (Map)
+import Data.Map as Map
+import Data.String.Lower (StringLower)
+import Data.String.Lower as String.Lower
+import Tower.Response.Body (Body(..))
+import Tower.Response.Body (Body(..), formBody) as Body
+
+data Response = Response {body :: Body, headers :: Map StringLower String, status :: Int}
+
+response :: Int -> Body -> Map String String -> Response
+response s b h = Response {status: s, body: b, headers: h # foldlWithIndex (\k m v -> Map.insert (String.Lower.fromString k) v m) Map.empty}
+
+status :: Response -> Int
+status (Response a) = a.status
+
+body :: Response -> Body
+body (Response a) = a.body
+
+headers :: Response -> Map StringLower String
+headers (Response a) = a.headers
+
+withHeader :: String -> String -> Response -> Response
+withHeader k v (Response a) = Response $ a {headers = Map.insert (String.Lower.fromString k) v a.headers}
+
+withStatus :: Int -> Response -> Response
+withStatus s (Response a) = Response $ a {status = s}
+
+withBody :: Body -> Response -> Response
+withBody b (Response a) = Response $ a {body = b}
+
+fromStatus :: Int -> Response
+fromStatus s = Response {body: BodyEmpty, headers: Map.empty, status: s}
+
+ok :: Response
+ok = fromStatus 200
diff --git a/src/Tower.Service.Class.purs b/src/Tower.Service.Class.purs
new file mode 100644
index 0000000..90e16d0
--- /dev/null
+++ b/src/Tower.Service.Class.purs
@@ -0,0 +1,2 @@
+module Tower.Service.Class where
+
diff --git a/src/Tower.Web.Headers.js b/src/Tower.Web.Headers.js
new file mode 100644
index 0000000..7814dc8
--- /dev/null
+++ b/src/Tower.Web.Headers.js
@@ -0,0 +1,5 @@
+///
+///
+
+/** @type {(_: {tuple: (a: A) => (b: B) => unknown}) => (h: Headers) => () => Array} */
+export const headerEntries = ({tuple}) => hs => () => Array.from(hs.entries()).map(([a, b]) => tuple(a)(b))
diff --git a/src/Tower.Web.Headers.purs b/src/Tower.Web.Headers.purs
new file mode 100644
index 0000000..acba21b
--- /dev/null
+++ b/src/Tower.Web.Headers.purs
@@ -0,0 +1,7 @@
+module Tower.Web.Headers where
+
+import Data.Tuple.Nested (type (/\))
+import Effect (Effect)
+
+foreign import data WebHeaders :: Type
+foreign import headerEntries :: {tuple :: forall a b. a -> b -> a /\ b} -> WebHeaders -> Effect (Array (String /\ String))
diff --git a/src/Tower.Web.Request.js b/src/Tower.Web.Request.js
new file mode 100644
index 0000000..d9abfeb
--- /dev/null
+++ b/src/Tower.Web.Request.js
@@ -0,0 +1,45 @@
+///
+///
+
+import Stream from 'stream'
+
+/** @type {(r: Request) => () => ReadableStream | null} */
+export const body = r => () => r.body
+
+/** @type {(r: Request) => () => boolean} */
+export const bodyUsed = r => () => r.bodyUsed
+
+/** @type {(r: Request) => () => string} */
+export const method = r => () => r.method
+
+/** @type {(r: Request) => () => string} */
+export const url = r => () => r.url
+
+/** @type {(r: Request) => () => Headers} */
+export const headers = r => () => r.headers
+
+/** @type {(r: ReadableStream) => () => Stream.Readable} */
+export const readableFromWeb = r => () => {
+ const reader = r.getReader();
+ return new Stream.Readable({
+ read: function() {
+ (async () => {
+ /** @type {ReadableStreamReadResult | undefined} */
+ let res = undefined;
+ try {
+ res = await reader.read()
+ } catch(e) {
+ if (typeof e === 'undefined' || e instanceof Error) {
+ this.destroy(e)
+ return
+ } else {
+ throw e
+ }
+ }
+
+ if (res.value) this.push(res.value);
+ if (res.done) this.push(null);
+ })()
+ },
+ })
+}
diff --git a/src/Tower.Web.Request.purs b/src/Tower.Web.Request.purs
new file mode 100644
index 0000000..691e65b
--- /dev/null
+++ b/src/Tower.Web.Request.purs
@@ -0,0 +1,19 @@
+module Tower.Web.Request where
+
+import Data.ArrayBuffer.Types (Uint8Array)
+import Data.Nullable (Nullable)
+import Effect (Effect)
+import Node.Stream as Stream
+import Tower.Request.Web (WebHeaders)
+import Web.Streams.ReadableStream (ReadableStream)
+
+foreign import data WebRequest :: Type
+
+foreign import body :: WebRequest -> Effect (Nullable (ReadableStream Uint8Array))
+foreign import bodyUsed :: WebRequest -> Effect Boolean
+foreign import method :: WebRequest -> Effect String
+foreign import url :: WebRequest -> Effect String
+
+foreign import headers :: WebRequest -> Effect WebHeaders
+
+foreign import readableFromWeb :: ReadableStream Uint8Array -> Effect (Stream.Readable ())
diff --git a/src/Tower.Web.Response.purs b/src/Tower.Web.Response.purs
new file mode 100644
index 0000000..7086add
--- /dev/null
+++ b/src/Tower.Web.Response.purs
@@ -0,0 +1 @@
+module Tower.Web.Response where