commit e8f791b0dcd819278ef050504d69f1411583769c parent 0feb75306944f3c61cf5cc6a6d1e80a1d97b36ca Author: Silas Brack <silasbrack@gmail.com> Date: Tue, 8 Oct 2024 08:03:07 +0200 Random changes Diffstat:
22 files changed, 570 insertions(+), 82 deletions(-)
diff --git a/apps/my-app/client/gleam.toml b/apps/my-app/client/gleam.toml @@ -20,3 +20,4 @@ shared = { path = "../shared/" } [dev-dependencies] gleeunit = ">= 1.0.0 and < 2.0.0" +lustre_dev_tools = ">= 1.5.8 and < 2.0.0" diff --git a/apps/my-app/client/manifest.toml b/apps/my-app/client/manifest.toml @@ -2,24 +2,54 @@ # You typically do not need to edit this file packages = [ + { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, + { name = "directories", version = "1.1.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "BDA521A4EB9EE3A7894F0DC863797878E91FF5C7826F7084B2E731E208BDB076" }, + { name = "envoy", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "CFAACCCFC47654F7E8B75E614746ED924C65BD08B1DE21101548AC314A8B6A41" }, + { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, + { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, + { name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" }, + { name = "gleam_community_ansi", version = "1.4.1", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "4CD513FC62523053E62ED7BAC2F36136EC17D6A8942728250A9A00A15E340E4B" }, + { name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" }, + { name = "gleam_crypto", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "8AE56026B3E05EBB1F076778478A762E9EB62B31AEEB4285755452F397029D22" }, { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, { name = "gleam_fetch", version = "0.4.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_javascript", "gleam_stdlib"], otp_app = "gleam_fetch", source = "hex", outer_checksum = "7446410A44A1D1328F5BC1FF4FC9CBD1570479EA69349237B3F82E34521CCC10" }, { name = "gleam_http", version = "3.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "8C07DF9DF8CC7F054C650839A51C30A7D3C26482AC241C899C1CEA86B22DBE51" }, + { name = "gleam_httpc", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "CF6CDD88830CC9853F7638ECC0BE7D7CD9522640DA5FAB4C08CFAC8DEBD08028" }, { name = "gleam_javascript", version = "0.12.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "6EB652538B31E852FE0A8307A8B6314DEB34930944B6DDC41CCC31CA344DA35D" }, { name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" }, { name = "gleam_otp", version = "0.11.2", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "517FFB679E44AD71D059F3EF6A17BA6EFC8CB94FA174D52E22FB6768CF684D78" }, + { name = "gleam_package_interface", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "CF3BFC5D0997750D9550D8D73A90F4B8D71C6C081B20ED4E70FFBE1E99AFC3C2" }, { name = "gleam_stdlib", version = "0.40.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86606B75A600BBD05E539EB59FABC6E307EEEA7B1E5865AFB6D980A93BCB2181" }, + { name = "glearray", version = "0.2.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "9C207E05F38D724F464FA921378DB3ABC2B0A2F5821116D8BC8B2CACC68930D5" }, { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, + { name = "glint", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "369C8A289017F73581D6B9FE2F5748169EB6FA021FFAA34FA7A49EE2094C73B3" }, + { name = "glisten", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "48EF7F6D1DCA877C2F49AF35CC33946C7129EEB05A114758A2CC569C708BFAF8" }, + { name = "gramps", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "3CCAA6E081225180D95C79679D383BBF51C8D1FDC1B84DA1DA444F628C373793" }, + { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, + { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, { name = "lustre", version = "4.4.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "B64EFA26B6DDA8391C85E33B06C2B5F4CD24B9772342824085DBC445B7E05C5A" }, + { name = "lustre_dev_tools", version = "1.5.8", build_tools = ["gleam"], requirements = ["argv", "filepath", "fs", "gleam_community_ansi", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_stdlib", "glint", "glisten", "mist", "simplifile", "spinner", "term_size", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "C65F0A3AB666988B201101E5804CDB9EDFFF7773A3AF9A2CE33392C4FFB86612" }, { name = "lustre_http", version = "0.5.2", build_tools = ["gleam"], requirements = ["gleam_fetch", "gleam_http", "gleam_javascript", "gleam_json", "gleam_stdlib", "lustre"], otp_app = "lustre_http", source = "hex", outer_checksum = "FB0478CBFA6B16DBE8ECA326DAE2EC15645E04900595EF2C4F039ABFA0512ABA" }, + { name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" }, + { name = "mist", version = "2.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "981F12FC8BA0656B40099EC876D6F2BEE7B95593610F342E9AB0DC4E663A932F" }, + { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" }, { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, + { name = "repeatedly", version = "2.1.1", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "38808C3EC382B0CD981336D5879C24ECB37FCB9C1D1BD128F7A80B0F74404D79" }, { name = "shared", version = "1.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_stdlib"], source = "local", path = "../shared" }, + { name = "simplifile", version = "2.2.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0DFABEF7DC7A9E2FF4BB27B108034E60C81BEBFCB7AB816B9E7E18ED4503ACD8" }, + { name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" }, + { name = "spinner", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "200BA3D4A04D468898E63C0D316E23F526E02514BC46454091975CB5BAE41E8F" }, + { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" }, + { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" }, + { name = "tom", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "228E667239504B57AD05EC3C332C930391592F6C974D0EFECF32FFD0F3629A27" }, + { name = "wisp", version = "1.1.0", build_tools = ["gleam"], requirements = ["directories", "exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "5B25E37E08A94039668D2ACF7B2D2A665BEAF4B48EFA8613010B4E3164137664" }, ] [requirements] gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" } +lustre_dev_tools = { version = ">= 1.5.8 and < 2.0.0"} lustre_http = { version = ">= 0.5.2 and < 1.0.0" } shared = { path = "../shared/" } diff --git a/apps/my-app/client/shell.nix b/apps/my-app/client/shell.nix @@ -1,10 +1,10 @@ let - # pkgs = import <nixpkgs> {}; - pkgs = import (builtins.fetchTarball { - name = "nixos-24.05"; - url = "https://github.com/nixos/nixpkgs/archive/63dacb46bf939521bdc93981b4cbb7ecb58427a0.tar.gz"; - sha256 = "1lr1h35prqkd1mkmzriwlpvxcb34kmhc9dnr48gkm8hh089hifmx"; - }) {}; + pkgs = import <nixpkgs> {}; + # pkgs = import (builtins.fetchTarball { + # name = "nixos-24.05"; + # url = "https://github.com/nixos/nixpkgs/archive/63dacb46bf939521bdc93981b4cbb7ecb58427a0.tar.gz"; + # sha256 = "1lr1h35prqkd1mkmzriwlpvxcb34kmhc9dnr48gkm8hh089hifmx"; + # }) {}; in pkgs.mkShell { buildInputs = [ pkgs.gleam diff --git a/apps/my-app/server/gleam.toml b/apps/my-app/server/gleam.toml @@ -22,6 +22,7 @@ gleam_otp = ">= 0.12.0 and < 1.0.0" gleam_pgo = ">= 0.14.0 and < 1.0.0" mist = ">= 2.0.0 and < 3.0.0" lustre = ">= 4.3.6 and < 5.0.0" +wisp = ">= 1.1.0 and < 2.0.0" glenv = ">= 0.4.0 and < 1.0.0" birl = ">= 1.7.1 and < 2.0.0" decode = ">= 0.2.0 and < 1.0.0" diff --git a/apps/my-app/server/manifest.toml b/apps/my-app/server/manifest.toml @@ -6,7 +6,9 @@ packages = [ { name = "backoff", version = "1.1.6", build_tools = ["rebar3"], requirements = [], otp_app = "backoff", source = "hex", outer_checksum = "CF0CFFF8995FB20562F822E5CC47D8CCF664C5ECDC26A684CBE85C225F9D7C39" }, { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, { name = "decode", version = "0.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "decode", source = "hex", outer_checksum = "965F517F67B8C172CA27A5C8E34C73733139E8C9E64736181B8C3179281F9793" }, + { name = "directories", version = "1.1.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "BDA521A4EB9EE3A7894F0DC863797878E91FF5C7826F7084B2E731E208BDB076" }, { name = "envoy", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "CFAACCCFC47654F7E8B75E614746ED924C65BD08B1DE21101548AC314A8B6A41" }, + { name = "exception", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "F5580D584F16A20B7FCDCABF9E9BE9A2C1F6AC4F9176FA6DD0B63E3B20D450AA" }, { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, { name = "gleam_crypto", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "ADD058DEDE8F0341F1ADE3AAC492A224F15700829D9A3A3F9ADF370F875C51B7" }, { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, @@ -23,11 +25,13 @@ packages = [ { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, { name = "lustre", version = "4.3.6", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib"], otp_app = "lustre", source = "hex", outer_checksum = "A122A11C761E2366EEA4649CBBE5C162BCA8EF5D78C0B1C7AACBA6E824AE9F5D" }, + { name = "marceau", version = "1.2.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "5188D643C181EE350D8A20A3BDBD63AF7B6C505DE333CFBE05EF642ADD88A59B" }, { name = "mist", version = "2.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "981F12FC8BA0656B40099EC876D6F2BEE7B95593610F342E9AB0DC4E663A932F" }, { name = "opentelemetry_api", version = "1.3.0", build_tools = ["rebar3", "mix"], requirements = ["opentelemetry_semantic_conventions"], otp_app = "opentelemetry_api", source = "hex", outer_checksum = "B9E5FF775FD064FA098DBA3C398490B77649A352B40B0B730A6B7DC0BDD68858" }, { name = "opentelemetry_semantic_conventions", version = "0.2.0", build_tools = ["rebar3", "mix"], requirements = [], otp_app = "opentelemetry_semantic_conventions", source = "hex", outer_checksum = "D61FA1F5639EE8668D74B527E6806E0503EFC55A42DB7B5F39939D84C07D6895" }, { name = "pg_types", version = "0.4.0", build_tools = ["rebar3"], requirements = [], otp_app = "pg_types", source = "hex", outer_checksum = "B02EFA785CAECECF9702C681C80A9CA12A39F9161A846CE17B01FB20AEEED7EB" }, { name = "pgo", version = "0.14.0", build_tools = ["rebar3"], requirements = ["backoff", "opentelemetry_api", "pg_types"], otp_app = "pgo", source = "hex", outer_checksum = "71016C22599936E042DC0012EE4589D24C71427D266292F775EBF201D97DF9C9" }, + { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" }, { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, { name = "shared", version = "1.0.0", build_tools = ["gleam"], requirements = ["birl", "gleam_stdlib"], source = "local", path = "../shared" }, { name = "simplifile", version = "2.1.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "BDD04F5D31D6D34E2EDFAEF0B68A6297AEC939888C3BFCE61133DE13857F6DA2" }, @@ -35,6 +39,7 @@ packages = [ { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" }, { name = "thoas", version = "1.2.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "E38697EDFFD6E91BD12CEA41B155115282630075C2A727E7A6B2947F5408B86A" }, { name = "tom", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "9EECB60150E834A07238BD5C7DF1FF07F7D4C5862BB8A773923D1981C7875FB0" }, + { name = "wisp", version = "1.1.0", build_tools = ["gleam"], requirements = ["directories", "exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "5B25E37E08A94039668D2ACF7B2D2A665BEAF4B48EFA8613010B4E3164137664" }, ] [requirements] @@ -52,3 +57,4 @@ glenv = { version = ">= 0.4.0 and < 1.0.0" } lustre = { version = ">= 4.3.6 and < 5.0.0" } mist = { version = ">= 2.0.0 and < 3.0.0" } shared = { path = "../shared" } +wisp = { version = ">= 1.1.0 and < 2.0.0" } diff --git a/apps/my-app/server/shell.nix b/apps/my-app/server/shell.nix @@ -1,15 +1,15 @@ let - # pkgs = import <nixpkgs> {}; + pkgs = import <nixpkgs> {}; # pkgs = import (builtins.fetchTarball { # name = "nixos-24.05"; # url = "https://github.com/nixos/nixpkgs/archive/63dacb46bf939521bdc93981b4cbb7ecb58427a0.tar.gz"; # sha256 = "1lr1h35prqkd1mkmzriwlpvxcb34kmhc9dnr48gkm8hh089hifmx"; # }) {}; - pkgs = import (builtins.fetchTarball { - name = "nixos-unstable"; - url = "https://github.com/nixos/nixpkgs/archive/896d74a4f75c23bca045a48ea7d0c1f16f604943.tar.gz"; - sha256 = "1j9rhjql4qjm9xky90m0chdcjhkm3dsijqr0kc6n3xaw7f21wgj4"; - }) {}; + # pkgs = import (builtins.fetchTarball { + # name = "nixos-unstable"; + # url = "https://github.com/nixos/nixpkgs/archive/896d74a4f75c23bca045a48ea7d0c1f16f604943.tar.gz"; + # sha256 = "1j9rhjql4qjm9xky90m0chdcjhkm3dsijqr0kc6n3xaw7f21wgj4"; + # }) {}; in pkgs.mkShell { buildInputs = [ pkgs.gleam @@ -18,5 +18,12 @@ in pkgs.mkShell { pkgs.inotify-tools pkgs.nodejs_22 ]; - # imports = ["./test.nix"]; + shellHook = '' + export PORT=52393 + export PGHOST="127.0.0.1" + export PGPASSWORD="postgres" + export PGPORT=44537 + export PGUSER="silas" + export PGDB="mydb" + ''; } diff --git a/apps/my-app/server/src/erlib.erl b/apps/my-app/server/src/erlib.erl @@ -0,0 +1,7 @@ +-module(erlib). +-export([process_message_queue_len/0]). + +process_message_queue_len() -> + % Use process_info/2 to get the message_queue_len of the current process + {message_queue_len, Len} = process_info(self(), message_queue_len), + Len. diff --git a/apps/my-app/server/src/server.gleam b/apps/my-app/server/src/server.gleam @@ -1,5 +1,6 @@ import gleam/erlang/process import mist +import server/actors/room_actor import server/db import server/env import server/router @@ -15,7 +16,13 @@ pub fn main() { // // load this from somewhere so that it is not regenerated on every restart. // let secret_key_base = wisp.random_string(64) - let ctx = web.Context(connection: db.get_connection(), env: env.get_env()) + let env = env.get_env() + let ctx = + web.Context( + connection: db.get_connection(env), + env: env, + room_actor: room_actor.start(), + ) // Start the Mist web server. let assert Ok(_) = diff --git a/apps/my-app/server/src/server/actors.gleam b/apps/my-app/server/src/server/actors.gleam @@ -0,0 +1,12 @@ +import gleam/erlang/process.{type Subject} +import shared.{type Message} + +pub type CustomWebsocketMessage { + SendToClient(message: Message) +} + +pub type RoomActorMessage { + ConnectUser(user_subject: Subject(CustomWebsocketMessage)) + DisconnectUser(user_subject: Subject(CustomWebsocketMessage)) + SendToAll(message: Message) +} diff --git a/apps/my-app/server/src/server/actors/calculator_actor.gleam b/apps/my-app/server/src/server/actors/calculator_actor.gleam @@ -0,0 +1,72 @@ +// an example of a cpu intensive actor +// supports: factorial 5! = 5 * 4 * 3 * 2 * 1 = 120 + +import gleam/erlang/process.{type Subject} +import gleam/int +import gleam/list +import gleam/otp/actor.{type Next} +import gleam/regex +import gleam/result +import gleam/string +import wisp + +pub type CalculatorActorMessage { + // Subjects are generic over their message type, and `Result(String, Nil)` is the type + // we want our public `solve` function to return. + Solve(reply_to: Subject(Result(String, Nil)), calculation: String) + Shutdown +} + +pub fn is_calculation(content: String) -> Bool { + // regex {number}! or {number}!! + let assert Ok(input_pattern) = regex.from_string("^\\d+!+$") + regex.check(input_pattern, content) +} + +pub fn start() -> Subject(CalculatorActorMessage) { + wisp.log_info("Starting calculator actor") + + let state = Nil + let assert Ok(actor) = actor.start(state, handle_message) + + actor +} + +fn handle_message( + message: CalculatorActorMessage, + _state: Nil, +) -> Next(CalculatorActorMessage, Nil) { + case message { + Shutdown -> actor.Stop(process.Normal) + Solve(client, calculation) -> { + let number = + calculation + |> string.split("!") + |> list.first + |> result.unwrap("") + |> int.parse + + let result = case number { + Ok(number) -> { + let result = number |> factorial(1) |> int.to_string + wisp.log_info("Calculation inner finished!") + Ok(result) + } + _ -> { + wisp.log_error("Invalid calculation") + Error(Nil) + } + } + + process.send(client, result) + actor.continue(Nil) + } + } +} + +fn factorial(n: Int, acc: Int) -> Int { + case n { + 0 -> acc + _ -> factorial(n - 1, n * acc) + } +} diff --git a/apps/my-app/server/src/server/actors/room_actor.gleam b/apps/my-app/server/src/server/actors/room_actor.gleam @@ -0,0 +1,80 @@ +import gleam/erlang/process.{type Subject} +import gleam/int +import gleam/list +import gleam/otp/actor.{type Next} +import gleam/string +import server/actors.{ + type CustomWebsocketMessage, type RoomActorMessage, ConnectUser, + DisconnectUser, SendToAll, SendToClient, +} +import shared.{type Message, RoomUpdate} +import wisp + +pub type RoomActorState { + RoomActorState(participants: List(#(String, Subject(CustomWebsocketMessage)))) +} + +pub fn start() -> Subject(RoomActorMessage) { + wisp.log_info("Starting room actor") + + let state = RoomActorState(participants: []) + let assert Ok(actor) = actor.start(state, handle_message) + + actor +} + +fn handle_message( + message: RoomActorMessage, + state: RoomActorState, +) -> Next(RoomActorMessage, RoomActorState) { + case message { + ConnectUser(user_subject) -> { + let new_participant = #("User", user_subject) + let new_participants = list.append(state.participants, [new_participant]) + let num_participants = list.length(new_participants) + + let new_state = RoomActorState(participants: new_participants) + + send_to_all(new_participants, RoomUpdate(num_participants)) + + wisp.log_info( + "User joined! num participants: " <> num_participants |> int.to_string, + ) + + new_state |> actor.continue + } + DisconnectUser(user_subject) -> { + let new_participants = + list.filter(state.participants, fn(p) { p.1 != user_subject }) + let new_state = RoomActorState(participants: new_participants) + let num_participants = list.length(new_participants) + + send_to_all(new_participants, RoomUpdate(num_participants)) + + wisp.log_info( + "User left! num participants: " <> num_participants |> int.to_string, + ) + + new_state |> actor.continue + } + SendToAll(message) -> { + send_to_all(state.participants, message) + + state |> actor.continue + } + } +} + +fn send_to_all( + participants: List(#(String, Subject(CustomWebsocketMessage))), + message: Message, +) { + let num_participants = list.length(participants) + wisp.log_info( + "Sending to " + <> num_participants |> int.to_string + <> " participants: " + <> string.slice(shared.message_to_string(message), 0, 200), + ) + list.each(participants, fn(p) { process.send(p.1, SendToClient(message)) }) +} diff --git a/apps/my-app/server/src/server/actors/websocket_actor.gleam b/apps/my-app/server/src/server/actors/websocket_actor.gleam @@ -0,0 +1,191 @@ +import birl +import gleam/erlang/process.{type Subject, Normal} +import gleam/function +import gleam/http/request.{type Request} +import gleam/http/response.{type Response} +import gleam/int +import gleam/option.{type Option, None, Some} +import gleam/otp/actor.{type Next, Stop} +import gleam/string +import mist.{ + type Connection, type ResponseData, type WebsocketConnection, + type WebsocketMessage, Custom, Text, +} +import server/actors.{ + type CustomWebsocketMessage, type RoomActorMessage, ConnectUser, + DisconnectUser, SendToAll, SendToClient, +} +import server/actors/calculator_actor.{Solve, is_calculation} +import server/process_utils.{process_message_queue_len} +import shared.{ChatMessage} +import wisp + +pub type WebsocketActorState { + WebsocketActorState( + name: Option(String), + ws_subject: Subject(CustomWebsocketMessage), + room_subject: Option(Subject(RoomActorMessage)), + ) +} + +pub fn start( + req: Request(Connection), + room_subject: Subject(RoomActorMessage), +) -> Response(ResponseData) { + mist.websocket( + request: req, + on_init: fn(_) { + let ws_subject = process.new_subject() + let new_selector = + process.new_selector() + |> process.selecting(ws_subject, function.identity) + + let state = + WebsocketActorState( + name: None, + ws_subject: ws_subject, + room_subject: Some(room_subject), + ) + + // register at room + process.send(room_subject, ConnectUser(ws_subject)) + + #(state, Some(new_selector)) + }, + on_close: fn(state) { + wisp.log_info("A connection was closed") + state |> cleanup + Nil + }, + handler: handle_message, + ) +} + +pub fn handle_message( + state: WebsocketActorState, + connection: WebsocketConnection, + message: WebsocketMessage(CustomWebsocketMessage), +) -> Next(CustomWebsocketMessage, WebsocketActorState) { + case message { + // from internal + Custom(message) -> + case message { + SendToClient(message) -> { + // stringify to json + let message_json = shared.message_to_string(message) + let assert Ok(_) = mist.send_text_frame(connection, message_json) + state |> actor.continue + } + } + // from browser + Text(message) -> { + // check queue length of process + // if too high this means client is sending faster than we can process + // we stop calculations if > 3 + // in testing it was discovered that this number increases by 1 between + // runs of handle_message. So our view of how much work is queued is + // delayed. + let process_queue_len = process_message_queue_len() + case process_queue_len { + i if i > 3 -> { + wisp.log_warning("queue len:" <> process_queue_len |> int.to_string) + } + _ -> Nil + } + + // parse from json + let parsed_message = case message |> shared.message_from_string { + Ok(parsed_message) -> { + parsed_message + } + Error(_) -> { + wisp.log_error("Failed to parse message: " <> message) + let now = birl.now() + ChatMessage( + "Failed to parse message: " <> message, + "System", + birl.to_iso8601(now), + ) + } + } + + // transform message (calculator) + // forward ChatMessage to room + case parsed_message { + ChatMessage(content, author, created_at) -> { + // calculator (sync) + let parsed_message = case + is_calculation(content) && process_queue_len <= 3 + { + False -> parsed_message + True -> { + // start new actor + let calc_actor = calculator_actor.start() + + // call actor with timeout ms + let calc_result = + process.try_call( + calc_actor, + fn(reply_to) { Solve(reply_to, content) }, + 2000, + ) + + // stop actor + // without the unlink we kill (our)self + process.unlink(process.subject_owner(calc_actor)) + process.kill(process.subject_owner(calc_actor)) + + case calc_result { + Ok(Ok(calc_result)) -> { + ChatMessage( + content <> " = " <> calc_result, + author, + created_at, + ) + } + Error(e) -> { + let error = string.inspect(e) + ChatMessage( + content <> " = ? (" <> error <> ")", + author, + created_at, + ) + } + _ -> parsed_message + } + } + } + + // send to room + { + use room_subject <- option.then(state.room_subject) + Some(process.send(room_subject, SendToAll(parsed_message))) + } + } + _ -> { + wisp.log_error("Received message is not a ChatMessage: " <> message) + None + } + } + + state |> actor.continue + } + _ -> { + wisp.log_info("Stopping websocket actor in handle_message") + cleanup(state) + Stop(Normal) + } + } +} + +fn cleanup(state: WebsocketActorState) -> WebsocketActorState { + case state.room_subject { + Some(room_subject) -> { + process.send(room_subject, DisconnectUser(state.ws_subject)) + WebsocketActorState(..state, room_subject: None) + } + None -> state + } + + state +} diff --git a/apps/my-app/server/src/server/db.gleam b/apps/my-app/server/src/server/db.gleam @@ -1,21 +1,24 @@ import gleam/option.{Some} import gleam/pgo -import server/env.{get_env} +import server/env.{type Env} -pub fn get_connection() { - let env = get_env() +pub fn get_connection(env: Env) { + let use_ssl = case env.db_host { + "127.0.0.1" -> False + _ -> True + } let connection = - pgo.connect( - pgo.Config( - ..pgo.default_config(), - host: env.db_host, - port: env.db_port, - user: env.db_user, - password: Some(env.db_password), - database: env.db_name, - ssl: True, - ), + pgo.Config( + ..pgo.default_config(), + host: env.db_host, + port: env.db_port, + user: env.db_user, + password: Some(env.db_password), + database: env.db_name, + ssl: use_ssl, ) + |> pgo.connect + connection } diff --git a/apps/my-app/server/src/server/env.gleam b/apps/my-app/server/src/server/env.gleam @@ -24,14 +24,16 @@ pub fn get_env() { let decoder = decode.into({ + use port <- decode.parameter use db_host <- decode.parameter use db_password <- decode.parameter use db_port <- decode.parameter use db_user <- decode.parameter use db_name <- decode.parameter - Env(db_host, db_password, db_port, db_user, db_name) + Env(port:, db_host:, db_password:, db_port:, db_user:, db_name:) }) + |> decode.field("PORT", decode.int) |> decode.field("PGHOST", decode.string) |> decode.field("PGPASSWORD", decode.string) |> decode.field("PGPORT", decode.int) diff --git a/apps/my-app/server/src/server/process_utils.gleam b/apps/my-app/server/src/server/process_utils.gleam @@ -0,0 +1,4 @@ +// see erlib.erl for the implementation + +@external(erlang, "erlib", "process_message_queue_len") +pub fn process_message_queue_len() -> Int diff --git a/apps/my-app/server/src/server/router.gleam b/apps/my-app/server/src/server/router.gleam @@ -1,12 +1,9 @@ -import gleam/list -import gleam/pgo -import server/routes/posts -import server/web - import gleam/bytes_builder import gleam/http/request.{type Request} import gleam/http/response.{type Response} import gleam/int +import gleam/list +import gleam/pgo import gleam/string_builder import lustre/attribute.{attribute, class, href, id, name, rel, src} import lustre/element.{type Element, text} @@ -15,7 +12,10 @@ import lustre/element/html.{ section, span, title, } import mist.{type Connection, type ResponseData} +import server/actors/websocket_actor import server/db/post +import server/routes/posts +import server/web import shared pub type Model { @@ -35,7 +35,7 @@ pub fn handle_request( // NOTE: assets under /static are caught by web.middleware before this let segments = request.path_segments(req) case segments { - [] | ["posts"] -> { + ["posts"] -> { let posts = case post.posts(ctx.connection) { Ok(returned) -> case returned { @@ -77,9 +77,8 @@ pub fn handle_request( |> mist.Bytes, ) } + ["ws"] -> websocket_actor.start(req, ctx.room_actor) ["api", "posts"] -> posts.posts(req, ctx) - // ["api", ..] -> wisp.not_found() - // _ -> page_routes(req, wisp.path_segments(req), ctx) _ -> { let empty_body = mist.Bytes(bytes_builder.new()) let not_found = response.new(404) |> response.set_body(empty_body) diff --git a/apps/my-app/server/src/server/routes/posts.gleam b/apps/my-app/server/src/server/routes/posts.gleam @@ -1,20 +1,17 @@ import birl import gleam/bytes_builder import gleam/http.{Get} +import gleam/http/request.{type Request} +import gleam/http/response.{type Response} import gleam/json.{type Json} import gleam/pgo import gleam/result import gleam/string_builder +import mist.{type Connection, type ResponseData} import server/db/post import server/web import shared -// import wisp.{type Request, type Response} -import gleam/http/request.{type Request} - -import gleam/http/response.{type Response} -import mist.{type Connection, type ResponseData} - pub fn posts( req: Request(Connection), ctx: web.Context, diff --git a/apps/my-app/server/src/server/web.gleam b/apps/my-app/server/src/server/web.gleam @@ -1,44 +1,52 @@ +import gleam/erlang/process.{type Subject} import gleam/pgo -import server/env.{Env} +import server/actors.{type RoomActorMessage} +import server/env.{type Env} +import wisp pub type Context { - Context(connection: pgo.Connection, env: Env) + Context( + connection: pgo.Connection, + env: Env, + room_actor: Subject(RoomActorMessage), + ) +} + +/// The middleware stack that the request handler uses. The stack is itself a +/// middleware function! +/// +/// Middleware wrap each other, so the request travels through the stack from +/// top to bottom until it reaches the request handler, at which point the +/// response travels back up through the stack. +/// +/// The middleware used here are the ones that are suitable for use in your +/// typical web application. +/// +pub fn middleware( + req: wisp.Request, + handle_request: fn(wisp.Request) -> wisp.Response, +) -> wisp.Response { + // Permit browsers to simulate methods other than GET and POST using the + // `_method` query parameter. + let req = wisp.method_override(req) + + // Log information about the request and response. + use <- wisp.log_request(req) + + // Return a default 500 response if the request handler crashes. + use <- wisp.rescue_crashes + + // Rewrite HEAD requests to GET requests and return an empty body. + use req <- wisp.handle_head(req) + + // Serve build output site files + let assert Ok(priv_directory) = wisp.priv_directory("server") + use <- wisp.serve_static( + req, + under: "/static", + from: priv_directory <> "/static", + ) + + // Handle the request! + handle_request(req) } -// /// The middleware stack that the request handler uses. The stack is itself a -// /// middleware function! -// /// -// /// Middleware wrap each other, so the request travels through the stack from -// /// top to bottom until it reaches the request handler, at which point the -// /// response travels back up through the stack. -// /// -// /// The middleware used here are the ones that are suitable for use in your -// /// typical web application. -// /// -// pub fn middleware( -// req: wisp.Request, -// handle_request: fn(wisp.Request) -> wisp.Response, -// ) -> wisp.Response { -// // Permit browsers to simulate methods other than GET and POST using the -// // `_method` query parameter. -// let req = wisp.method_override(req) -// -// // Log information about the request and response. -// use <- wisp.log_request(req) -// -// // Return a default 500 response if the request handler crashes. -// use <- wisp.rescue_crashes -// -// // Rewrite HEAD requests to GET requests and return an empty body. -// use req <- wisp.handle_head(req) -// -// // Serve build output site files -// let assert Ok(priv_directory) = wisp.priv_directory("server") -// use <- wisp.serve_static( -// req, -// under: "/static", -// from: priv_directory <> "/static", -// ) -// -// // Handle the request! -// handle_request(req) -// } diff --git a/apps/my-app/shared/gleam.toml b/apps/my-app/shared/gleam.toml @@ -15,6 +15,7 @@ target = "javascript" [dependencies] gleam_stdlib = ">= 0.34.0 and < 2.0.0" +gleam_json = ">= 2.0.0 and < 3.0.0" birl = ">= 1.7.1 and < 2.0.0" [dev-dependencies] diff --git a/apps/my-app/shared/manifest.toml b/apps/my-app/shared/manifest.toml @@ -3,6 +3,7 @@ packages = [ { name = "birl", version = "1.7.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "ranger"], otp_app = "birl", source = "hex", outer_checksum = "5C66647D62BCB11FE327E7A6024907C4A17954EF22865FE0940B54A852446D01" }, + { name = "gleam_json", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "CB10B0E7BF44282FB25162F1A24C1A025F6B93E777CCF238C4017E4EEF2CDE97" }, { name = "gleam_stdlib", version = "0.40.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86606B75A600BBD05E539EB59FABC6E307EEEA7B1E5865AFB6D980A93BCB2181" }, { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, { name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" }, @@ -10,5 +11,6 @@ packages = [ [requirements] birl = { version = ">= 1.7.1 and < 2.0.0" } +gleam_json = { version = ">= 2.0.0 and < 3.0.0"} gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } gleeunit = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/apps/my-app/shared/src/shared.gleam b/apps/my-app/shared/src/shared.gleam @@ -1,5 +1,61 @@ import birl +import gleam/dynamic.{type DecodeError, type Dynamic, DecodeError} +import gleam/json.{type Json} +import gleam/result pub type Post { Post(id: Int, user_id: Int, content: String, created_at: birl.Time) } + +pub type Message { + ChatMessage(text: String, author: String, created_at: String) + RoomUpdate(num_participants: Int) +} + +pub fn message_to_json(msg: Message) -> Json { + case msg { + ChatMessage(text, author, created_at) -> + json.object([ + #("$", json.string("ChatMessage")), + #("text", json.string(text)), + #("author", json.string(author)), + #("created_at", json.string(created_at)), + ]) + RoomUpdate(num_participants) -> + json.object([ + #("$", json.string("RoomUpdate")), + #("num_participants", json.int(num_participants)), + ]) + } +} + +fn decoder(dynamic: Dynamic) -> Result(Message, List(DecodeError)) { + use tag <- result.then(dynamic.field("$", dynamic.string)(dynamic)) + let decoder = case tag { + "ChatMessage" -> + dynamic.decode3( + ChatMessage, + dynamic.field("text", dynamic.string), + dynamic.field("author", dynamic.string), + dynamic.field("created_at", dynamic.string), + ) + "RoomUpdate" -> + dynamic.decode1( + RoomUpdate, + dynamic.field("num_participants", dynamic.int), + ) + + _ -> fn(_) { Error([DecodeError("Message", tag, ["$"])]) } + } + + decoder(dynamic) +} + +pub fn message_to_string(msg: Message) -> String { + message_to_json(msg) + |> json.to_string +} + +pub fn message_from_string(json: String) -> Result(Message, _) { + json.decode(json, decoder) +} diff --git a/db/mydb/migrations/001.sql b/db/mydb/migrations/001.sql @@ -1,3 +1,5 @@ +create schema if not exists twitter; + create table if not exists twitter.users ( id bigint primary key generated always as identity, username text unique not null,