From 9a375699d21eea8d5e10a9bf9983217e0db5c9ee Mon Sep 17 00:00:00 2001 From: Michael St Clair Date: Tue, 21 Jul 2026 21:55:12 -0600 Subject: [PATCH 1/4] update ci and deps --- .github/workflows/test.yaml | 52 +++++++++++------------------------ .tool-versions | 4 +-- mix.exs | 18 ++++++++----- mix.lock | 54 ++++++++++++++++++++----------------- 4 files changed, 59 insertions(+), 69 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 6694a37..ec6e9ed 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -35,14 +35,10 @@ jobs: - uses: actions/checkout@v4 - - uses: erlef/setup-beam@v1 - id: setup-elixir - with: - version-type: strict - version-file: .tool-versions + - uses: jdx/mise-action@v2 - name: Cache Deps - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: deps key: ${{ runner.os }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} @@ -50,7 +46,7 @@ jobs: ${{ runner.os }}-mix- - name: Cache Build - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: _build key: ${{ runner.os }}-build-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} @@ -76,14 +72,10 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: erlef/setup-beam@v1 - id: setup-elixir - with: - version-type: strict - version-file: .tool-versions + - uses: jdx/mise-action@v2 - name: Cache Deps - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: deps key: ${{ runner.os }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} @@ -91,7 +83,7 @@ jobs: ${{ runner.os }}-mix- - name: Cache Build - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: _build key: ${{ runner.os }}-build-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} @@ -111,14 +103,10 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: erlef/setup-beam@v1 - id: setup-elixir - with: - version-type: strict - version-file: .tool-versions + - uses: jdx/mise-action@v2 - name: Cache Deps - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: deps key: ${{ runner.os }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} @@ -126,7 +114,7 @@ jobs: ${{ runner.os }}-mix- - name: Cache Build - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: _build key: ${{ runner.os }}-build-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} @@ -143,14 +131,10 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: erlef/setup-beam@v1 - id: setup-elixir - with: - version-type: strict - version-file: .tool-versions + - uses: jdx/mise-action@v2 - name: Cache Deps - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: deps key: ${{ runner.os }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} @@ -158,7 +142,7 @@ jobs: ${{ runner.os }}-mix- - name: Cache Build - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: _build key: ${{ runner.os }}-build-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} @@ -177,14 +161,10 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: erlef/setup-beam@v1 - id: setup-elixir - with: - version-type: strict - version-file: .tool-versions + - uses: jdx/mise-action@v2 - name: Cache Deps - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: deps key: ${{ runner.os }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} @@ -192,7 +172,7 @@ jobs: ${{ runner.os }}-mix- - name: Cache Build - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: _build key: ${{ runner.os }}-build-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} @@ -200,4 +180,4 @@ jobs: ${{ runner.os }}-build- - run: mix do deps.get, compile - - run: mix sobelow --config \ No newline at end of file + - run: mix sobelow --config diff --git a/.tool-versions b/.tool-versions index 322df0e..f7487cb 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -elixir 1.18.2-otp-27 -erlang 27.1.3 +elixir 1.19.5-otp-28 +erlang 28.4.1 diff --git a/mix.exs b/mix.exs index 26aba35..5fdd9a4 100644 --- a/mix.exs +++ b/mix.exs @@ -11,12 +11,7 @@ defmodule LiveSync.MixProject do deps: deps(), package: package(), test_paths: ["lib"], - test_coverage: [tool: ExCoveralls], - preferred_cli_env: [ - coveralls: :test, - "coveralls.html": :test, - "coveralls.json": :test - ] + test_coverage: [tool: ExCoveralls] ] end @@ -29,6 +24,16 @@ defmodule LiveSync.MixProject do ] end + def cli do + [ + preferred_envs: [ + coveralls: :test, + "coveralls.html": :test, + "coveralls.json": :test + ] + ] + end + defp package do [ maintainers: ["Michael St Clair"], @@ -50,6 +55,7 @@ defmodule LiveSync.MixProject do {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, {:excoveralls, "~> 0.10", only: :test}, {:floki, ">= 0.30.0", only: :test}, + {:lazy_html, ">= 0.1.0", only: :test}, {:mix_audit, "~> 2.0", only: [:dev, :test], runtime: false}, {:sobelow, "~> 0.13", only: [:dev, :test], runtime: false}, {:styler, "~> 1.0", only: [:dev, :test], runtime: false} diff --git a/mix.lock b/mix.lock index 596cbdb..8b5badd 100644 --- a/mix.lock +++ b/mix.lock @@ -1,36 +1,40 @@ %{ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "castore": {:hex, :castore, "1.0.11", "4bbd584741601eb658007339ea730b082cc61f3554cf2e8f39bf693a11b49073", [:mix], [], "hexpm", "e03990b4db988df56262852f20de0f659871c35154691427a5047f4967a16a62"}, - "credo": {:hex, :credo, "1.7.13", "126a0697df6b7b71cd18c81bc92335297839a806b6f62b61d417500d1070ff4e", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "47641e6d2bbff1e241e87695b29f617f1a8f912adea34296fb10ecc3d7e9e84f"}, - "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, - "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.43", "34b2f401fe473080e39ff2b90feb8ddfeef7639f8ee0bbf71bb41911831d77c5", [:mix], [], "hexpm", "970a3cd19503f5e8e527a190662be2cee5d98eed1ff72ed9b3d1a3d466692de8"}, - "ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"}, - "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, - "ex_doc": {:hex, :ex_doc, "0.36.1", "4197d034f93e0b89ec79fac56e226107824adcce8d2dd0a26f5ed3a95efc36b1", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d7d26a7cf965dacadcd48f9fa7b5953d7d0cfa3b44fa7a65514427da44eafd89"}, + "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, + "credo": {:hex, :credo, "1.7.19", "cc52129665fc7c15143d47838fda0f9cd6dac9ceced7bf4da6f85fcbfe64b12a", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "2d8bc95d5a7bb99dd2613621d4f08c6a3575c3fd4b62e6a2b48a100352a557b8"}, + "db_connection": {:hex, :db_connection, "2.10.2", "ae391e803a5adff104da913c2fc1c0c14a37f8b10001dcef568796e1fb7bf95c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "510b14482330f1af6490a2fa0efd8d4f1435d1529b165647df22ac0f2df0fa93"}, + "decimal": {:hex, :decimal, "3.1.1", "430d87b04011ce6cbd4fd205be758311a81f87d552d40904abd00f015935b1d0", [:mix], [], "hexpm", "c5f25f2ced74a0587d03e6023f595db8e924c9d3922c8c8ffd9edfc4498cf1f6"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.46", "67607a0532e810c6f630a515c548d0b24949643f168cc556303bee4cf96105c7", [:mix], [], "hexpm", "9c44636e8a1c68c62f526b2dcd85d941dbbcee7ab82cf64ba06ce28bef8e89f5"}, + "ecto": {:hex, :ecto, "3.14.1", "7b740d87bdf45996aa0c2c2e081640906f10caa7ce5ba328fd294c7d49d0cc6f", [:mix], [{:decimal, "~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "24b991956796700f467d0a3ef3d303138a3ef9ddddf8b98f43758ee067b20a30"}, + "ecto_sql": {:hex, :ecto_sql, "3.14.0", "06446ab8410d2f85bfbb80857ee224ab3b693700cbb38f6535d507449a627b2e", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.14.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.8", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f4d8d36faf294c9417b5a37ec7ac8217ee2abdef5fcf197ba690f361548d3949"}, + "elixir_make": {:hex, :elixir_make, "0.10.0", "16577e2583a79bb79237bbff349619ef5d80afffc07eac6e4faf0d00e2ddaf7d", [:mix], [], "hexpm", "dc1f09fb7fa68866b886abd5f0f3c83553b1a19a52359a899e92af1bb3b31982"}, + "ex_doc": {:hex, :ex_doc, "0.40.3", "4a972ffe64bc07dc605af487e98fc19b72a4185f55ca031b94c0552d6071c1d9", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "2756e357742fecd9749b489b85d67c9ce99c465f2e75728d9e6dc8d704b973de"}, "excoveralls": {:hex, :excoveralls, "0.18.5", "e229d0a65982613332ec30f07940038fe451a2e5b29bce2a5022165f0c9b157e", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "523fe8a15603f86d64852aab2abe8ddbd78e68579c8525ae765facc5eae01562"}, "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, - "floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"}, - "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, - "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, + "fine": {:hex, :fine, "0.1.6", "4bf7151493443c454aac9f2fa2f34f5fefd0346a83fb5586a016c4a135c63247", [:mix], [], "hexpm", "5638eb4495488e885ebec167fa57973e5c35e1a50c344eb7666c90ec1c4e3b12"}, + "floki": {:hex, :floki, "0.38.4", "10f98971e892aed2c2f1b3a0f928e488e3797e1c6dd3dfd98db40b14e9a78bcf", [:mix], [], "hexpm", "bdb34645eee8e79845c7edaca2d4099a52804ee4d4a3ecc683a69451f0244973"}, + "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, + "lazy_html": {:hex, :lazy_html, "0.1.12", "31a55ee622918fce988c94b06232227b42daa64e4eab14ac32081d0f3fd8db6f", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "8a0da594776caee58782c6f93b2abaa5bdb809daf8d43351a561f7de9dc2e2a8"}, + "makeup": {:hex, :makeup, "1.2.2", "882d46dc0905e9ff7abf2aab61a7e6b3dcc555533977d8a23b06019e6c89ac94", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "9a1a24e5b343b8ae16abea0822c10a6f75da27af7fa802ada5251f7579bfccfa"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, - "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, - "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, - "mix_audit": {:hex, :mix_audit, "2.1.4", "0a23d5b07350cdd69001c13882a4f5fb9f90fbd4cbf2ebc190a2ee0d187ea3e9", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "fd807653cc8c1cada2911129c7eb9e985e3cc76ebf26f4dd628bb25bbcaa7099"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.1.0", "835f7e60792e08824cda445639555d7bf1bbbddb1b60b306e33cb6f6db24dc74", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "1cd6780fb1dd1a03979abaed0fe82712b0625118fd5257d3ebbf73f960c73c3c"}, + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, + "mix_audit": {:hex, :mix_audit, "2.1.5", "c0f77cee6b4ef9d97e37772359a187a166c7a1e0e08b50edf5bf6959dfe5a016", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "87f9298e21da32f697af535475860dc1d3617a010e0b418d2ec6142bc8b42d69"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, - "phoenix": {:hex, :phoenix, "1.7.19", "36617efe5afbd821099a8b994ff4618a340a5bfb25531a1802c4d4c634017a57", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "ba4dc14458278773f905f8ae6c2ec743d52c3a35b6b353733f64f02dfe096cd6"}, - "phoenix_html": {:hex, :phoenix_html, "4.2.0", "83a4d351b66f472ebcce242e4ae48af1b781866f00ef0eb34c15030d4e2069ac", [:mix], [], "hexpm", "9713b3f238d07043583a94296cc4bbdceacd3b3a6c74667f4df13971e7866ec8"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.3", "33914a5d47345c7bde056054bca05c0bbbda5ce94a3ee734cb20a4d5d361e20d", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "157d93a5a7c79b836d9b7b5d55c7dd021435139d6160112f0e630f0686b8ea88"}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, + "phoenix": {:hex, :phoenix, "1.8.9", "a63ed0962ed5b903b146dab0ae8eb8387fe478f8171a5e26d56a165f35996fe1", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "3477e2dd5a4f61820341169031bdfe21275f659923bea9c5c0ea2aa1c3fcc046"}, + "phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.2.7", "d0f20871681216598e78baccd4f66d8686fdb8007821989bab508d293a3ed9ad", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "61e97938a4fcca6d6f2c836925623abf2f52a572cc8c6085e4074f3f6337e0eb"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, - "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, - "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, - "postgrex": {:hex, :postgrex, "0.19.3", "a0bda6e3bc75ec07fca5b0a89bffd242ca209a4822a9533e7d3e84ee80707e19", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d31c28053655b78f47f948c85bb1cf86a9c1f8ead346ba1aa0d0df017fa05b61"}, - "sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"}, - "styler": {:hex, :styler, "1.3.3", "5196fc9e9bf1254af4337b051103d38532c2f500b0f01bc6f53f56ee678f65e6", [:mix], [], "hexpm", "c275f73f2ff1b7dfafb03a0f8cf3e928709a235a2b049162b40168f53ba3a07c"}, - "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "plug": {:hex, :plug, "1.20.3", "56c480c633ec2ce10140e236e15233bf576e1d323887d7c96711bd02ab5160db", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "be266aee1b8536ef6409d58cf39a3121319f0ec47cfa1b24024485aa0e76ad76"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, + "postgrex": {:hex, :postgrex, "0.22.3", "bf65941737ee7a9adbe4a64c91080310d11703da343e8ac9188aacb9eb9f6f02", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "f018c13752b2b46e8d35d7e2d84c3276557cbfd880769109021a1d0ee36c1cfe"}, + "sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"}, + "styler": {:hex, :styler, "1.11.0", "35010d970689a23c2bcc8e97bd8bf7d20e3561d60c49be84654df5c37d051a9c", [:mix], [], "hexpm", "70f36165d0cf238a32b7a456fdef6a9c72e77e657d7ac4a0ace33aeba3f2b8c0"}, + "telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, - "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, + "websock_adapter": {:hex, :websock_adapter, "0.6.0", "73db5ab8aaefd1a876a97ce3e6afc96562625de69ef17a4e04426e034849d0b8", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "50021a85bce8f203b086705d9e0c5415e2c7eb05d319111b0428fe71f9934617"}, "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, - "yaml_elixir": {:hex, :yaml_elixir, "2.11.0", "9e9ccd134e861c66b84825a3542a1c22ba33f338d82c07282f4f1f52d847bd50", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "53cc28357ee7eb952344995787f4bb8cc3cecbf189652236e9b163e8ce1bc242"}, + "yaml_elixir": {:hex, :yaml_elixir, "2.12.2", "9dd1330fb4cd9a36a7b0f502e5b12486eff632792ee4a5f0eba52a4d4ec32c9c", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "e7c1b10122f973e6558462d51c39026ba0e14afbc6745318e990ea82cfe9e159"}, } From 106d42ee81d6f5b41ba3fb827257f037f232136f Mon Sep 17 00:00:00 2001 From: Michael St Clair Date: Tue, 21 Jul 2026 21:56:06 -0600 Subject: [PATCH 2/4] fix array decoding --- lib/live_sync/replication.ex | 34 ++++++++++++++++++++++--- lib/live_sync/replication_test.exs | 41 +++++++++++++++++++++++++++--- lib/live_sync/socket_test.exs | 2 +- lib/test_helper.exs | 1 + test/support/data_case.ex | 2 +- test/support/example.ex | 3 ++- 6 files changed, 73 insertions(+), 10 deletions(-) diff --git a/lib/live_sync/replication.ex b/lib/live_sync/replication.ex index 43599eb..d163eb6 100644 --- a/lib/live_sync/replication.ex +++ b/lib/live_sync/replication.ex @@ -281,9 +281,7 @@ defmodule LiveSync.Replication do defp load_value({:array, type}, value) do value - |> String.replace_leading("{", "") - |> String.replace_trailing("}", "") - |> String.split(",") + |> parse_array() |> Enum.map(&load_value(type, &1)) end @@ -308,6 +306,36 @@ defmodule LiveSync.Replication do Ecto.Type.cast!(type, value) end + # Postgres delivers arrays in its text output format, e.g. + # {"a, b","c",NULL,plain} + # Any element containing a comma, quote, brace, backslash or whitespace (or an + # empty string) is double-quoted, with `"` and `\` backslash-escaped inside, + # and an unquoted NULL means nil. Splitting on `,` corrupts quoted elements, so + # we walk the literal, returning raw strings (or nil) for load_value/2 to cast. + defp parse_array("{" <> rest), do: parse_array(rest, []) + + defp parse_array("}" <> _rest, acc), do: Enum.reverse(acc) + defp parse_array("," <> rest, acc), do: parse_array(rest, acc) + + defp parse_array(<>, acc) do + {value, rest} = parse_quoted(rest, "") + parse_array(rest, [value | acc]) + end + + defp parse_array(binary, acc) do + {value, rest} = parse_unquoted(binary, "") + parse_array(rest, [value | acc]) + end + + defp parse_quoted(<>, acc), do: parse_quoted(rest, <>) + defp parse_quoted(<>, acc), do: {acc, rest} + defp parse_quoted(<>, acc), do: parse_quoted(rest, <>) + + # a nil element is only ever an unquoted NULL; a real "NULL" string is quoted + defp parse_unquoted(<> = binary, "NULL") when char in [?,, ?}], do: {nil, binary} + defp parse_unquoted(<> = binary, acc) when char in [?,, ?}], do: {acc, binary} + defp parse_unquoted(<>, acc), do: parse_unquoted(rest, <>) + @epoch DateTime.to_unix(~U[2000-01-01 00:00:00Z], :microsecond) defp current_time, do: System.os_time(:microsecond) - @epoch diff --git a/lib/live_sync/replication_test.exs b/lib/live_sync/replication_test.exs index 93a5a28..ed3d665 100644 --- a/lib/live_sync/replication_test.exs +++ b/lib/live_sync/replication_test.exs @@ -10,7 +10,7 @@ defmodule LiveSync.ReplicationTest do @moduletag cleanup: ["ignored", "examples"] setup do - LiveSync.start_link(repo: LiveSync.Repo, otp_app: :live_sync) + LiveSync.start_link(repo: Repo, otp_app: :live_sync) :ok end @@ -38,12 +38,12 @@ defmodule LiveSync.ReplicationTest do assert_receive {:live_sync, [ - delete: %LiveSync.Example{id: delete_id, name: nil} + delete: %Example{id: delete_id, name: nil} ]} assert_receive {:live_sync, [ - update: %LiveSync.Example{ + update: %Example{ id: update_id, name: "more replication", enabled: true, @@ -51,7 +51,7 @@ defmodule LiveSync.ReplicationTest do embed_one: %EmbedOne{name: "embed"}, embed_many: [%EmbedMany{name: "many"}] }, - insert: %LiveSync.Example{ + insert: %Example{ id: insert_id, name: "replication", enabled: false, @@ -65,4 +65,37 @@ defmodule LiveSync.ReplicationTest do assert id == update_id assert id == delete_id end + + test "decodes array columns whose elements need quoting" do + Replication.subscribe("live_sync:1") + + # Postgres quotes array elements that contain commas, quotes, braces, + # backslashes or whitespace, and represents an unquoted NULL for nil. + tags = [ + "DELETE FROM t RETURNING id, name, tags", + ~s(has "double" quotes), + "back\\slash", + "{braced}", + "", + # a quoted "NULL" string must stay a string, distinct from a real NULL (nil) + "NULL", + nil, + "plain" + ] + + {:ok, id} = + Repo.transaction(fn -> + # insert the struct directly so changeset empty-value filtering doesn't + # drop the empty-string element we want to exercise the decoder against + example = Repo.insert!(%Example{organization_id: 1, name: "arrays", tags: tags}) + Repo.update!(change(example, name: "updated arrays")) + example.id + end) + + assert_receive {:live_sync, + [ + update: %Example{id: ^id, name: "updated arrays", tags: ^tags}, + insert: %Example{id: ^id, name: "arrays", tags: ^tags} + ]} + end end diff --git a/lib/live_sync/socket_test.exs b/lib/live_sync/socket_test.exs index 4ea1f14..fe63d5c 100644 --- a/lib/live_sync/socket_test.exs +++ b/lib/live_sync/socket_test.exs @@ -6,7 +6,7 @@ defmodule LiveSync.SocketTest do alias LiveSync.Repo setup do - LiveSync.start_link(repo: LiveSync.Repo, otp_app: :live_sync) + LiveSync.start_link(repo: Repo, otp_app: :live_sync) :ok end diff --git a/lib/test_helper.exs b/lib/test_helper.exs index de4baf7..0554f2a 100644 --- a/lib/test_helper.exs +++ b/lib/test_helper.exs @@ -13,6 +13,7 @@ CREATE TABLE examples ( parent_id bytea REFERENCES examples(id), organization_id integer, input jsonb, + tags text[], embed_one jsonb, embed_many jsonb ); diff --git a/test/support/data_case.ex b/test/support/data_case.ex index 428242f..31a4aa1 100644 --- a/test/support/data_case.ex +++ b/test/support/data_case.ex @@ -29,7 +29,7 @@ defmodule LiveSync.DataCase do if cleanup = tags[:cleanup] do on_exit(fn -> - Ecto.Adapters.SQL.Sandbox.checkout(LiveSync.Repo, sandbox: false) + Sandbox.checkout(LiveSync.Repo, sandbox: false) LiveSync.Repo.query!("TRUNCATE ONLY #{Enum.join(cleanup, ",")}") end) end diff --git a/test/support/example.ex b/test/support/example.ex index 7b32082..cd3c379 100644 --- a/test/support/example.ex +++ b/test/support/example.ex @@ -13,6 +13,7 @@ defmodule LiveSync.Example do field :enabled, :boolean, default: true field :organization_id, :integer field :input, :map + field :tags, {:array, :string} embeds_one :embed_one, EmbedOne, on_replace: :delete do field :name, :string @@ -29,7 +30,7 @@ defmodule LiveSync.Example do def changeset(struct \\ %__MODULE__{}, params) do struct - |> cast(params, [:name, :enabled, :input, :organization_id, :parent_id]) + |> cast(params, [:name, :enabled, :input, :tags, :organization_id, :parent_id]) |> cast_embed(:embed_one, with: &embed_changeset/2) |> cast_embed(:embed_many, with: &embed_changeset/2) end From eee22aecf5a752df9ed8543572aa1b7e52cfe46c Mon Sep 17 00:00:00 2001 From: Michael St Clair Date: Tue, 21 Jul 2026 22:01:06 -0600 Subject: [PATCH 3/4] bump version --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 5fdd9a4..be990dd 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule LiveSync.MixProject do def project do [ app: :live_sync, - version: "0.1.10", + version: "0.1.11", elixir: "~> 1.17", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, From cad06aa2a1f030e88cacdcf6ab514e5bde352a52 Mon Sep 17 00:00:00 2001 From: Michael St Clair Date: Tue, 21 Jul 2026 22:04:53 -0600 Subject: [PATCH 4/4] make array parsing more performant --- lib/live_sync/replication.ex | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/live_sync/replication.ex b/lib/live_sync/replication.ex index d163eb6..7e6311f 100644 --- a/lib/live_sync/replication.ex +++ b/lib/live_sync/replication.ex @@ -318,23 +318,28 @@ defmodule LiveSync.Replication do defp parse_array("," <> rest, acc), do: parse_array(rest, acc) defp parse_array(<>, acc) do - {value, rest} = parse_quoted(rest, "") + {value, rest} = parse_quoted(rest, []) parse_array(rest, [value | acc]) end defp parse_array(binary, acc) do - {value, rest} = parse_unquoted(binary, "") + {value, rest} = parse_unquoted(binary, []) parse_array(rest, [value | acc]) end - defp parse_quoted(<>, acc), do: parse_quoted(rest, <>) - defp parse_quoted(<>, acc), do: {acc, rest} - defp parse_quoted(<>, acc), do: parse_quoted(rest, <>) + defp parse_quoted(<>, acc), do: parse_quoted(rest, [<> | acc]) + defp parse_quoted(<>, acc), do: {IO.iodata_to_binary(Enum.reverse(acc)), rest} + defp parse_quoted(<>, acc), do: parse_quoted(rest, [<> | acc]) # a nil element is only ever an unquoted NULL; a real "NULL" string is quoted - defp parse_unquoted(<> = binary, "NULL") when char in [?,, ?}], do: {nil, binary} - defp parse_unquoted(<> = binary, acc) when char in [?,, ?}], do: {acc, binary} - defp parse_unquoted(<>, acc), do: parse_unquoted(rest, <>) + defp parse_unquoted(<> = binary, acc) when char in [?,, ?}] do + case IO.iodata_to_binary(Enum.reverse(acc)) do + "NULL" -> {nil, binary} + value -> {value, binary} + end + end + + defp parse_unquoted(<>, acc), do: parse_unquoted(rest, [<> | acc]) @epoch DateTime.to_unix(~U[2000-01-01 00:00:00Z], :microsecond) defp current_time, do: System.os_time(:microsecond) - @epoch