├── .clj-kondo ├── config.edn └── hyperfiddle │ └── electric │ └── config.edn ├── .cljfmt.edn ├── .cljstyle ├── .githooks ├── Readme.md └── pre-commit ├── .github └── workflows │ └── test_and_deploy.yml ├── .gitignore ├── .node-version ├── .projectile ├── .vscode └── settings.json ├── CHANGELOG.md ├── Readme.md ├── ci ├── run_tests_all.sh ├── run_tests_browser.sh ├── run_tests_jvm.sh ├── run_tests_node.sh └── running_dom_tests.md ├── deps.edn ├── docs ├── electric-protocol.md ├── electric3-explainer.png └── lightning_talk_cover.png ├── karma.conf.js ├── package-lock.json ├── package.json ├── resources └── public │ ├── hyperfiddle-electric-ui.css │ ├── hyperfiddle-forms.css │ └── hyperfiddle-popover.css ├── shadow-cljs.edn ├── src-build ├── build.clj └── build.md ├── src-dev └── toxiproxy.sh ├── src ├── clj-kondo.exports │ └── hyperfiddle │ │ └── electric │ │ └── config.edn ├── contrib │ ├── electric_codemirror.cljc │ ├── missionary_contrib.cljc │ ├── missionary_core_async.cljc │ ├── stacktrace.cljs │ ├── test_match.clj │ ├── trace3.cljc │ ├── triple_store.cljc │ └── walk.cljc └── hyperfiddle │ ├── detest.cljc │ ├── domlike.cljc │ ├── electric │ ├── debug3.cljc │ └── impl │ │ ├── array_fields.cljc │ │ ├── cljs_analyzer2.clj │ │ ├── destructure.cljc │ │ ├── event_store.cljc │ │ ├── jetty9_ring_websocket_adapter.clj │ │ ├── lang3.clj │ │ ├── lang3.cljs │ │ ├── lang_3_walkthrough.md │ │ ├── missionary_util.cljc │ │ ├── mount_point.cljc │ │ ├── pures_fns.clj │ │ ├── runtime3.cljc │ │ └── sunng87_ring_jetty9_ws_adapter.clj │ ├── electric3.cljc │ ├── electric3_contrib.cljc │ ├── electric_client3.cljs │ ├── electric_css3.cljc │ ├── electric_dom3.cljc │ ├── electric_dom3_events.cljc │ ├── electric_dom3_props.cljc │ ├── electric_forms3.cljc │ ├── electric_forms5.cljc │ ├── electric_fulcro_dom_adapter.cljc │ ├── electric_httpkit_adapter3.clj │ ├── electric_jetty9_ring_adapter3.clj │ ├── electric_local_def3.cljc │ ├── electric_ring_adapter3.clj │ ├── electric_scroll0.cljc │ ├── electric_svg3.cljc │ ├── electric_tokens.cljc │ ├── incseq.cljc │ ├── incseq │ ├── arrays_impl.cljc │ ├── diff_impl.cljc │ ├── fixed_impl.cljc │ ├── flow_protocol_enforcer.cljc │ ├── items_eager_impl.cljc │ ├── items_impl.cljc │ ├── latest_concat_impl.cljc │ ├── latest_product_impl.cljc │ ├── mount_impl.cljc │ └── perm_impl.cljc │ ├── input_zoo0.cljc │ ├── kvs.cljc │ └── token_zoo0.cljc └── test ├── cljs └── analyzer_testing_auto_alias.cljc ├── contrib ├── missionary_contrib_test.edn └── triple_store_test.cljc └── hyperfiddle ├── browser_test_setup.clj ├── detest └── incseq_test.cljc ├── electric └── impl │ ├── cljs_analyzer2_test.clj │ ├── cljs_file_to_analyze.cljs │ ├── cljs_file_to_analyze │ ├── include.cljc │ ├── macro_ns.clj │ ├── refer_macros.cljc │ ├── require.cljc │ ├── runtime.clj │ ├── runtime.cljs │ ├── use.cljc │ └── use_macros.cljc │ ├── compiler_test.cljc │ ├── compiler_test_clj.clj │ ├── compiler_test_cljs.cljs │ ├── expand3_test.cljc │ ├── expand_macro.clj │ ├── expand_require_referred.cljc │ ├── expand_unloaded.cljc │ ├── mount_point_test.cljc │ └── runtime3_test.cljc ├── electric3_exceptions_test.edn ├── electric3_network_test.cljc ├── electric3_test.cljc ├── goog_calls_test3.cljc ├── incseq ├── diff_impl_test.cljc ├── fixed_impl_test.cljc ├── items_eager_impl_test.cljc ├── items_impl_test.cljc ├── latest_concat_impl_test.cljc ├── latest_product_impl_test.cljc ├── mount_impl_test.cljc └── perm_impl_test.cljc ├── js_calls_test3.cljs ├── js_calls_test3.js ├── missionary_test.cljc └── transaction_test.clj /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {hyperfiddle.electric3/defn clojure.core/defn 2 | hyperfiddle.electric3/for clojure.core/for 3 | hyperfiddle.electric3/cursor clojure.core/let 4 | hyperfiddle.electric3/with-cycle clojure.core/let 5 | hyperfiddle.electric3/fn clojure.core/fn 6 | hyperfiddle.electric3/declare clojure.core/declare 7 | hyperfiddle.electric.impl.array-fields/deffields clojure.core/declare} 8 | :linters {:redundant-expression {:level :off}}} 9 | -------------------------------------------------------------------------------- /.clj-kondo/hyperfiddle/electric/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {hyperfiddle.electric/def clojure.core/def 2 | hyperfiddle.electric/defn clojure.core/defn 3 | hyperfiddle.electric3/defn clojure.core/defn 4 | hyperfiddle.electric3/declare clojure.core/declare 5 | hyperfiddle.electric3/cursor clojure.core/for 6 | hyperfiddle.electric3/for clojure.core/for 7 | hyperfiddle.electric/for clojure.core/for 8 | hyperfiddle.electric/with-cycle clojure.core/let 9 | hyperfiddle.electric/fn clojure.core/fn} 10 | :linters {:redundant-expression {:level :off}}} 11 | -------------------------------------------------------------------------------- /.cljfmt.edn: -------------------------------------------------------------------------------- 1 | {:indents ^:replace {#"^." [[:inner 0]]} 2 | :test-code [(sui/ui-grid {:columns 2} 3 | (sui/ui-grid-row {} 4 | (sui/ui-grid-column {:width 12} 5 | ...))) 6 | (let [foo bar] 7 | (str "foo" 8 | "bar"))]} -------------------------------------------------------------------------------- /.cljstyle: -------------------------------------------------------------------------------- 1 | {:files 2 | {:extensions #{"cljc" "cljs" "clj" "cljx"}, 3 | :ignore #{".hg" ".git", "target", "scratch"}}, 4 | :rules 5 | {:namespaces 6 | {:enabled? false, 7 | :indent-size 2, 8 | :break-libs? true, 9 | :import-break-width 60}, 10 | :whitespace 11 | {:enabled? true, 12 | :remove-surrounding? true, 13 | :remove-trailing? true, 14 | :insert-missing? true}, 15 | :comments {:enabled? true, :inline-prefix " ", :leading-prefix "; "}, 16 | :functions {:enabled? false}, 17 | :eof-newline {:enabled? true}, 18 | :types 19 | {:enabled? true, 20 | :types? true, 21 | :protocols? true, 22 | :reifies? true, 23 | :proxies? true}, 24 | :blank-lines 25 | {:enabled? true, 26 | :trim-consecutive? true, 27 | :max-consecutive 2, 28 | :insert-padding? true, 29 | :padding-lines 1}, 30 | :indentation 31 | {:enabled? true, 32 | :list-indent 2, 33 | :indents 34 | {are [[:block 2]], 35 | when-first [[:block 1]], 36 | cond->> [[:block 1]], 37 | while [[:block 1]], 38 | try [[:block 0]], 39 | bound-fn [[:inner 0]], 40 | thrown-with-msg? [[:block 2]], 41 | match [[:block 1]], 42 | testing [[:block 1]], 43 | if-not [[:block 1]], 44 | doseq [[:block 1]], 45 | finally [[:block 0]], 46 | deftype [[:block 1] [:inner 1]], 47 | when-let [[:block 1]], 48 | go [[:block 0]], 49 | if-some [[:block 1]], 50 | with-precision [[:block 1]], 51 | let [[:block 1]], 52 | defstruct [[:block 1]], 53 | doto [[:block 1]], 54 | future [[:block 0]], 55 | fn [[:inner 0]], 56 | alt! [[:block 0]], 57 | as-> [[:block 1]], 58 | do [[:block 0]], 59 | when-not [[:block 1]], 60 | when [[:block 1]], 61 | #"^def" [[:inner 0]], 62 | #"^with-" [[:inner 0]], 63 | extend [[:block 1]], 64 | go-loop [[:block 1]], 65 | defn [[:inner 0]], 66 | if [[:block 1]], 67 | ns [[:block 1]], 68 | deftest [[:inner 0]], 69 | extend-type [[:block 1] [:inner 1]], 70 | defmethod [[:inner 0]], 71 | struct-map [[:block 1]], 72 | extend-protocol [[:block 1] [:inner 1]], 73 | cond-> [[:block 1]], 74 | dotimes [[:block 1]], 75 | reify [[:inner 0] [:inner 1]], 76 | with-open [[:block 1]], 77 | defonce [[:inner 0]], 78 | defn- [[:inner 0]], 79 | alt!! [[:block 0]], 80 | defprotocol [[:block 1] [:inner 1]], 81 | letfn [[:block 1] [:inner 2 0]], 82 | use-fixtures [[:inner 0]], 83 | loop [[:block 1]], 84 | with-out-str [[:block 0]], 85 | condp [[:block 2]], 86 | cond [[:block 0]], 87 | for [[:block 1]], 88 | binding [[:block 1]], 89 | with-local-vars [[:block 1]], 90 | defmacro [[:inner 0]], 91 | proxy [[:block 2] [:inner 1]], 92 | with-redefs [[:block 1]], 93 | locking [[:block 1]], 94 | defmulti [[:inner 0]], 95 | if-let [[:block 1]], 96 | case [[:block 1]], 97 | catch [[:block 2]], 98 | thread [[:block 0]], 99 | comment [[:block 0]], 100 | defrecord [[:block 1] [:inner 1]], 101 | thrown? [[:block 1]], 102 | when-some [[:block 1]], 103 | def [[:inner 0]] 104 | 105 | tests [[:inner 0]], 106 | with [[:inner 0]], 107 | }}, 108 | :vars {:enabled? true}}} 109 | -------------------------------------------------------------------------------- /.githooks/Readme.md: -------------------------------------------------------------------------------- 1 | Register this folder with: 2 | 3 | ```shell 4 | git config core.hooksPath .githooks 5 | ``` 6 | -------------------------------------------------------------------------------- /.githooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed. 4 | # Called by "git commit" with no arguments. The hook should 5 | # exit with non-zero status after issuing an appropriate message if 6 | # it wants to stop the commit. 7 | 8 | if git rev-parse --verify HEAD >/dev/null 2>&1 9 | then 10 | against=HEAD 11 | else 12 | # Initial commit: diff against an empty tree object 13 | against=$(git hash-object -t tree /dev/null) 14 | fi 15 | 16 | if command -v cljstyle &> /dev/null 17 | then 18 | echo "Running cljstyle on pre-commit hook" 19 | echo "Will check files $(git diff --name-only --cached)" 20 | echo "$(cljstyle version)" 21 | cljstyle fix `git diff --name-only --cached` 22 | git add `git diff --name-only --cached` 23 | exit 0 24 | fi 25 | 26 | -------------------------------------------------------------------------------- /.github/workflows/test_and_deploy.yml: -------------------------------------------------------------------------------- 1 | name: hyperfiddle/electric 2 | on: 3 | push: 4 | branches: 5 | - "**" 6 | pull_request: 7 | jobs: 8 | jvm: 9 | name: Run JVM tests 10 | runs-on: [ubuntu-latest] 11 | timeout-minutes: 5 12 | steps: 13 | - uses: actions/checkout@v4.1.1 14 | 15 | - run: git status 16 | 17 | - uses: actions/setup-java@v4 18 | with: 19 | distribution: 'temurin' 20 | java-version: '11' 21 | 22 | - name: Cache local Maven repository 23 | uses: actions/cache@v3 24 | with: 25 | path: ~/.m2/repository 26 | key: ${{ runner.os }}-maven-${{ hashFiles('**/deps.edn') }} 27 | restore-keys: | 28 | ${{ runner.os }}-maven- 29 | 30 | - name: Install clojure tools 31 | uses: DeLaGuardo/setup-clojure@12.1 32 | with: 33 | cli: 1.11.1.1113 34 | 35 | - name: Run tests 36 | run: ./ci/run_tests_jvm.sh 37 | 38 | browser: 39 | name: Run browser tests 40 | runs-on: [ubuntu-latest] 41 | timeout-minutes: 5 42 | steps: 43 | 44 | - uses: actions/checkout@v4.1.1 45 | 46 | - run: git status 47 | 48 | - uses: actions/setup-java@v4 49 | with: 50 | distribution: 'temurin' 51 | java-version: '11' 52 | 53 | - name: Cache local Maven repository 54 | uses: actions/cache@v3 55 | with: 56 | path: ~/.m2/repository 57 | key: ${{ runner.os }}-maven-${{ hashFiles('**/deps.edn') }} 58 | restore-keys: | 59 | ${{ runner.os }}-maven- 60 | 61 | - name: Install clojure tools 62 | uses: DeLaGuardo/setup-clojure@12.1 63 | with: 64 | cli: 1.11.1.1113 65 | 66 | - name: Use Node.js 67 | uses: actions/setup-node@v4.0.1 68 | with: 69 | node-version: 18.x 70 | 71 | - name: NPM install 72 | run: npm install --include=dev 73 | 74 | # Install chrome and dependencies, puppeteer fails otherwise 75 | - run: sudo apt-get update && sudo apt-get install -y wget gnupg 76 | - run: wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - 77 | - run: sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' 78 | - run: sudo apt-get update && sudo apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 --no-install-recommends 79 | # - run: rm -rf /var/lib/apt/lists/* 80 | 81 | 82 | - name: Run Tests 83 | run: ./ci/run_tests_browser.sh 84 | 85 | nodejs: 86 | name: Run NodeJS tests 87 | runs-on: [ubuntu-latest] 88 | timeout-minutes: 5 89 | steps: 90 | - uses: actions/checkout@v4.1.1 91 | 92 | - run: git status 93 | 94 | - uses: actions/setup-java@v4 95 | with: 96 | distribution: 'temurin' 97 | java-version: '11' 98 | 99 | - name: Cache local Maven repository 100 | uses: actions/cache@v3 101 | with: 102 | path: ~/.m2/repository 103 | key: ${{ runner.os }}-maven-${{ hashFiles('**/deps.edn') }} 104 | restore-keys: | 105 | ${{ runner.os }}-maven- 106 | 107 | - name: Install clojure tools 108 | uses: DeLaGuardo/setup-clojure@12.1 109 | with: 110 | cli: 1.11.1.1113 111 | 112 | - name: Use Node.js 113 | uses: actions/setup-node@v4.0.1 114 | with: 115 | node-version: 18.x 116 | 117 | - name: NPM install 118 | run: npm install 119 | 120 | - name: Run Tests 121 | run: ./ci/run_tests_node.sh 122 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Please add user editor configs to system gitignore: 2 | # git config --global core.excludesfile 3 | # git config --global core.excludesfile ~/.gitignore 4 | # see also: https://gist.github.com/subfuzion/db7f57fff2fb6998a16c 5 | .clj-kondo/.cache 6 | .cpcache 7 | .env 8 | .idea 9 | .lsp 10 | .nrepl-port 11 | .shadow-cljs 12 | *.~undo-tree~ 13 | /pom.xml 14 | node_modules 15 | target 16 | tsconfig.tsbuildinfo 17 | .DS_Store 18 | /out/ 19 | /.dir-locals.el 20 | 21 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v16.1.0 -------------------------------------------------------------------------------- /.projectile: -------------------------------------------------------------------------------- 1 | -/.shadow-cljs 2 | -/node_modules 3 | -/package-lock.json -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "/scratch": true 4 | } 5 | } -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Electric Clojure – full-stack differential dataflow for UI 2 | 3 | `com.hyperfiddle/electric {:mvn/version "v3-alpha-SNAPSHOT"}` 4 | 5 | > [!NOTE] 6 | > Electric v3 is now in private beta! Request beta access here: https://www.hyperfiddle.net/early-access.html 7 | 8 | 9 | Electric is a new way to build rich, interactive web products that simply have too much interactivity, realtime streaming, and too rich network connections to be able to write all the frontend/backend network plumbing by hand. With Electric, you can compose your client and server expressions directly (i.e. in the same function), and the Electric compiler macros will **infer at compile time the implied frontend/backend boundary** and generate the corresponding full-stack app. 10 | 11 | ![](docs/electric3-explainer.png) 12 | 13 | *Figure: In Electric, client and server expressions compose directly, and the Electric compiler transparently solves the network boundary through a straightforward dataflow graph analysis.* 14 | 15 | **How it works:** Unlike request/response frameworks, frontend ORMs, and client-side databases that suffer from request waterfalls, over/under fetching, large payload deserialization and other performance issues that get worse as your codebase and database grows, Electric uses a **custom Clojure/Script compiler to perform deep graph analysis** of your unified frontend/backend program and automatically determine the implied network cut, and then compile your program into separate reactive client and server target programs that cooperate and anticipate each other's needs. See [UIs are streaming DAGs (Getz 2022)](https://hyperfiddle.notion.site/UIs-are-streaming-DAGs-e181461681a8452bb9c7a9f10f507991) for a quick 10 minute video explainer of how this works and why it **actually doesn't result in the request waterfalls you might expect,** even in the presence of deep nesting, loops and control flow. 16 | 17 | - **Fully reactive:** unlike javascript frameworks, in Electric, reactivity is built directly into the programming language itself. Reactive-if, reactive-for, reactive lambda. When everything is reactive, it feels like nothing is reactive. No observables! No async types! De-load your mind and relax. 18 | 19 | 20 | - **Multi-tier**: frontend and backend are defined in the same expression, same function, same file. It's not code sharing, it's code *splitting*. Let the compiler infer the boundary from your code, instead of contorting your code — nay, your entire architecture — to fit the boundary. 21 | 22 | 23 | - **Network-transparent**: Electric closures close over server and client scope bindings, all in the same expression. The Electric compiler uses compile-time static knowledge of your source code to slice your expressions into client and server portions. Right through closures, loops and deeply nested function calls. 24 | 25 | 26 | - **Strong composition:** Network-transparent Electric functions are true functions. You have lambda, recursion, HOFs, closures, dynamic scope, macros, etc: the full undamaged composition power of Lisp. Goodbye "functional core imperative shell"; with Electric the **entire system is a function**. 27 | 28 | Our mission is to raise the abstraction ceiling in web development in the same way that managed memory did in the 90s, paving the way for something new. 29 | 30 | 31 | # Lightning talk – 5 minutes 32 | 33 |
34 | 35 | lightning talk video 36 | 37 |
38 | 39 | # Tutorial and live examples 40 | 41 | - v3 live tutorial: https://electric.hyperfiddle.net/ 42 | - v3 starter app: not yet generally available, [request early access here](https://www.hyperfiddle.net/early-access.html) 43 | 44 | # Talks and essays 45 | - [Talk: Electric Clojure v3: Differential Dataflow for UI (Getz 2024)](https://hyperfiddle-docs.notion.site/Talk-Electric-Clojure-v3-Differential-Dataflow-for-UI-Getz-2024-2e611cebd73f45dc8cc97c499b3aa8b8) 46 | - [Talk: Electric Clojure: compiler managed datasync for rich web apps (Getz 2023)](https://hyperfiddle-docs.notion.site/Talk-Electric-Clojure-compiler-managed-datasync-for-rich-web-apps-Getz-2023-e089a8c0caeb456daaf2f9675e3ac4e7) 47 | - [Talk: UIs are streaming DAGs (Getz 2022)](https://hyperfiddle.notion.site/UIs-are-streaming-DAGs-e181461681a8452bb9c7a9f10f507991) 48 | - [Talk: Missionary: a functional approach to massively concurrent application design (Noel 2023)](https://hyperfiddle-docs.notion.site/Talk-Missionary-a-functional-approach-to-massively-concurrent-application-design-Noel-2023-a74748f610c044328d19d038a6daffa1) 49 | - [You don't need a web framework, you need a web language (Getz 2021)](https://hyperfiddle.notion.site/Reactive-Clojure-You-don-t-need-a-web-framework-you-need-a-web-language-44b5bfa526be4af282863f34fa1cfffc) 50 | - [Talk: Functional effects and streaming systems in Clojure (Noel 2021)](https://hyperfiddle-docs.notion.site/Talk-Functional-effects-and-streaming-systems-in-Clojure-Noel-2021-f3f907e5e9b04d08a3be33d53a3cd18e) 51 | - https://clojureverse.org/t/electric-clojure-a-signals-dsl-for-fullstack-web-ui/9788 52 | - https://clojureverse.org/t/signals-vs-streams/9840/1 53 | 54 | # Community 55 | 56 | * slack support chatroom: #hyperfiddle @ [clojurians.net](https://clojurians.net/) 57 | * follow https://twitter.com/dustingetz for progress updates 58 | * **Contributing:** we do not currently accept PRs against Electric itself as this has historically not been productive for us. However, **we'd love for you to contribute demos!** Many of our coolest demos were started by early users, this is an amazing and productive way for you to get involved and collaborate with us. This will require a signed contributors agreement (like Clojure), DM dustingetz on slack. 59 | 60 | # License 61 | 62 | Electric v3 is free for bootstrappers and non-commercial use, but is otherwise a commercial project, which helps us continue to invest and maintain payroll for a team of 4. See [license change announcement](https://tana.pub/lQwRvGRaQ7hM/electric-v3-license-change). 63 | -------------------------------------------------------------------------------- /ci/run_tests_all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ./ci/run_tests_jvm.sh; jvm_code=$? 4 | ./ci/run_tests_node.sh; node_code=$? 5 | ./ci/run_tests_browser.sh "$@"; browser_code=$? 6 | 7 | RED=$(tput setaf 1) 8 | GREEN=$(tput setaf 2) 9 | RESET=$(tput sgr0) 10 | 11 | reportResult() { 12 | if [ "$1" -eq 0 ]; then echo "${GREEN}ok${RESET}"; else echo "${RED}failed${RESET}"; fi 13 | } 14 | 15 | printf "\n --results--\n" 16 | printf "JVM %s\n" "$(reportResult $jvm_code)" 17 | printf "node %s\n" "$(reportResult $node_code)" 18 | printf "browser %s\n" "$(reportResult $browser_code)" 19 | 20 | [ $jvm_code -eq 0 ] && [ $node_code -eq 0 ] && [ $browser_code -eq 0 ] 21 | -------------------------------------------------------------------------------- /ci/run_tests_browser.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -x 2 | 3 | echo "Running Browser tests" 4 | clojure -M:test:shadow-cljs compile :browser-test --force-spawn && \ 5 | ./node_modules/.bin/karma start --single-run $@ # --browsers Chrome 6 | -------------------------------------------------------------------------------- /ci/run_tests_jvm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Running JVM tests" 4 | 5 | # All namespaces are tested by default to encourage tested code. 6 | # Use :ns-regexp to blacklist specific namespaces. 7 | # ^(?!foo.(bar|baz)).* : includes everything except foo.bar or foo.baz 8 | 9 | clojure -X:test \ 10 | :dirs "[\"src\" \"test\"]" \ 11 | :patterns "[\"^(?!hyperfiddle.(api|popover|txn|electric-fulcro|electric-httpkit|electric-jetty|spool|spec|impl.jetty|electric.impl.sunng|electric.impl.jetty9)|contrib.(datomic|test.datomic)).*\"]" 12 | -------------------------------------------------------------------------------- /ci/run_tests_node.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Running NodeJS tests" 4 | clojure -M:test:shadow-cljs compile :test --force-spawn "$@" \ 5 | && node out/node-tests.js 6 | -------------------------------------------------------------------------------- /ci/running_dom_tests.md: -------------------------------------------------------------------------------- 1 | # How to run tests in a browser 2 | 3 | ## Running once 4 | Run `./ci/run_tests_browser.sh`. 5 | 6 | ## Live reloading 7 | ``` 8 | clojure -M:test:shadow-cljs watch :browser-test 9 | # in another shell: 10 | ./node_modules/.bin/karma start 11 | ``` 12 | 13 | Karma will: 14 | - use Chrome if installed 15 | - fallback to Chromium if installed 16 | - download and install Chromium for you otherwise 17 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:hyperfiddle.build/name com.hyperfiddle/electric 2 | :hyperfiddle.build/description "A reactive Clojure dialect for web development that uses a compiler to infer the frontend/backend boundary" 3 | :hyperfiddle.build/version "v3-alpha-SNAPSHOT" 4 | :hyperfiddle.build/pom-data [[:licenses 5 | [:license 6 | [:name "Hyperfiddle Business Source License"] 7 | [:url "http://hyperfiddle.net"]]] 8 | [:developers 9 | [:developer 10 | [:organization "Hyperfiddle, Inc."] 11 | [:organizationUrl "http://www.hyperfiddle.net"]]]] 12 | :hyperfiddle.build/pom-scm {:url "https://github.com/hyperfiddle/electric" 13 | :connection "scm:git:git://github.com/hyperfiddle/electric.git" 14 | :developerConnection "scm:git:ssh://git@github.com/hyperfiddle/electric.git"} 15 | :paths ["src"] 16 | :deps { 17 | com.cognitect/transit-clj {:mvn/version "1.0.333"} 18 | com.cognitect/transit-cljs {:mvn/version "0.8.280"} 19 | com.hyperfiddle/rcf {:mvn/version "20220926-202227"} 20 | missionary/missionary {:mvn/version "b.44"} 21 | fipp/fipp {:mvn/version "0.6.26"} 22 | org.clojure/clojure {:mvn/version "1.12.0-alpha11"} 23 | org.clojure/clojurescript {:mvn/version "1.11.121"} 24 | org.clojure/tools.logging {:mvn/version "1.2.4"} 25 | borkdude/edamame {:mvn/version "1.4.25"} 26 | 27 | ;; prevents building from source, use maven release 28 | com.hyperfiddle/hyperfiddle-contrib {:local/root "../hyperfiddle-contrib"} 29 | com.hyperfiddle/electric-secret {:local/root "../electric-secret"} 30 | } 31 | 32 | :aliases {:shadow-cljs {:extra-deps {thheller/shadow-cljs {:mvn/version "2.26.2"}} 33 | :main-opts ["-m" "shadow.cljs.devtools.cli"]} 34 | 35 | :test {:extra-paths ["test"] 36 | :exec-fn cognitect.test-runner.api/test 37 | :jvm-opts ["-Dhyperfiddle.rcf.generate-tests=true" 38 | "-Dhyperfiddle.electric.impl.missionary-util.wrap=true" 39 | "-XX:-OmitStackTraceInFastThrow"] ;; https://archive.md/NNt9r 40 | :extra-deps {org.clojure/core.async {:mvn/version "1.6.681"} ; for interop helpers only 41 | ch.qos.logback/logback-classic {:mvn/version "1.4.14"} 42 | io.github.cognitect-labs/test-runner {:git/url "https://github.com/cognitect-labs/test-runner.git" :sha "cc75980b43011773162b485f46f939dc5fba91e4"} 43 | org.clojure/test.check {:mvn/version "1.1.1"} 44 | com.datomic/local {:mvn/version "1.0.285"} ; for hyperfiddle.transaction-test 45 | com.datomic/client-cloud {:mvn/version "1.0.130"} ; for hyperfiddle.transaction-test 46 | thheller/shadow-cljs {:mvn/version "2.26.2"} ; for hooks tests 47 | ring/ring-core {:mvn/version "1.11.0"} 48 | }} 49 | :build {:extra-paths ["src-build"] 50 | :ns-default build 51 | :extra-deps {com.hyperfiddle/build {:local/root "../build"}}} 52 | :release {:override-deps {com.hyperfiddle/hyperfiddle-contrib {:mvn/version "v0-alpha-SNAPSHOT"}}}} 53 | } 54 | -------------------------------------------------------------------------------- /docs/electric-protocol.md: -------------------------------------------------------------------------------- 1 | STATUS : draft 2 | 3 | # The electric protocol 4 | 5 | ## Purpose and goals 6 | 7 | The electric protocol allows two processes called *peers*, typically running on two different physical machines (e.g. 8 | a server and a client) on two different host plaforms (e.g. a JVM and a browser), to run an electric program and 9 | synchronize their states in reaction to local events. The program being run is the only knowledge initially shared by 10 | the two peers. 11 | 12 | TODO: what is an electric program ? what is the entrypoint ? is it a zero-argument e/defn ? 13 | 14 | The electric protocol allows for : 15 | * maintaining the set of currently active transfer sessions, according to the active branches of the program 16 | * multiplexing the successive changes for these transfer sessions, according to the state of each expression 17 | 18 | An explicit goal of the protocol design is to allow for optimistic transfers, i.e. allow a given peer to anticipate 19 | transfer of a local expression state when it's able to infer (from shared program knowledge) that a local event 20 | requires a remote program section having this local expression as a dependency. This optimization helps reduce latency 21 | by avoiding unnecessary round-trips. 22 | 23 | As a consequence of this optimization, both peers can concurrently decide to start or stop a transfer session for the 24 | same expression, and thus need to reach consensus about its lifecycle. This problem is exacerbated by the fact that 25 | differential state propagation relies on sequential delivery - if one peer resets a session while the other one doesn't, 26 | the latter will observe permanent state corruption. 27 | 28 | TODO: example 29 | 30 | ## Channel 31 | 32 | The protocol relies on a persistent connection, established beforehand, able to transport messages in both directions 33 | with reliable delivery and ordering. The channel must support flow control in the writing direction, i.e. expose a 34 | means to limit the rate of messages sent by a given peer when the underlying network layers can't keep up. Support for 35 | flow control in the reading direction is not a requirement, because a peer is always able to accept new messages. 36 | 37 | In the current implementation, websockets are used as the channel implementation. Alternative implementations could be 38 | considered in the future. 39 | 40 | TODO: is channel reconnection logic part of this document ? 41 | 42 | ## Serialization 43 | 44 | The protocol assumes each expression being evaluated is identifiable by a serializable value called a *slot*, and the 45 | differential states of any given expression are serializable. 46 | 47 | TODO: is the expression identification scheme part of this document ? 48 | TODO: is the serialization method is part of the protocol ? currently, transit is not an option 49 | 50 | ## Message structure 51 | All messages are the serialization of a 4-tuple `[acks request change freeze]`, where : 52 | * `acks` is a non-negative integer 53 | * `request` is a map associating slots to non-zero integers 54 | * `change` is a map associating slots to diffs 55 | * `freeze` is a set of slots 56 | 57 | The message structure is a natural monoid because all of its 4 components are monoids, as explained further. 58 | 59 | ### `acks` - Changeset acknowledgement 60 | The `acks` number represents the count of non-*pure-ack* messages received by the sender since the previous message 61 | was sent. A *pure-ack* is a message with an empty `request`, an empty `change`, and an empty `freeze`, i.e. a message 62 | describing an empty changeset. 63 | 64 | The *empty message* is a *pure-ack* with zero `acks`. The *empty message* has no effect and should not be sent. Any 65 | other message MUST be sent as soon as the channel is ready to write. 66 | 67 | The `acks` number is a monoid. The identity element is `0`, the binary operation is addition. 68 | 69 | TODO: example 70 | 71 | ### `request` - Local request propagation 72 | An electric program can reify the differential state of an expression as an effect. A peer with access to this effect 73 | can decide to perform it, typically as part of an effect composition managed by another part of the same electric 74 | program. For each remote expression that is a dependency of the local expression wrapped by this effect, the effect 75 | lifecycle must be propagated via the `request` map by the association of the dependency slot with the number `1` (if 76 | the process was spawned) or `-1` (if the process was terminated). The `request` map being sent on the wire is the 77 | aggregation of all `request` maps generated by each local event since the last message was sent. 78 | 79 | The `request` map is a monoid. The identity element is `{}`, the binary operation is map merging with value addition 80 | and elision of zero values. 81 | 82 | ```clojure 83 | (def merge-request 84 | (partial reduce-kv 85 | (fn [r k n] 86 | (let [n (+ n (r k 0))] 87 | (if (zero? n) 88 | (dissoc r k) 89 | (assoc r k n)))))) 90 | ``` 91 | 92 | TODO: example 93 | 94 | ### `change` and `freeze` - Differential state propagation 95 | The `request` map transfers, in combination with message `acks`, are used by both peers to reach consensus about the 96 | transfer session lifecycle. When a given expression is part of the active session set, a half-port is instanciated on 97 | each side of the channel - an output on the local peer, an input on the remote peer. The output is a subscription to 98 | the expression's differential signal, the input is an object exposing the current state as a differential signal 99 | according to incoming changes. The `change` map being sent on the wire is the aggregation of slot-diff pairs generated 100 | by all local changes, the `freeze` set is the union of slots generated by local freezes (i.e. spontaneous termination 101 | of the signal subscription), since the last message was sent. 102 | 103 | The `change` map and `freeze` set form a monoid. The identity element is `{} #{}`, the binary operation is map merging 104 | and set union. Diff sequences must be semigroups to resolve collisions on the change map (i.e. diff squashing). 105 | 106 | TODO: example 107 | 108 | ## Session lifecycle consensus 109 | The essence of the algorithm is to delay session teardown on local events, to give the remote peer a grace period. The 110 | grace period is defined by the remote peer via message acknowledgement, therefore we know it's always long enough to 111 | reach consensus but never longer than round-trip latency. 112 | 113 | Let's consider the state of the half-port of a single expression on a given peer. The state machine is the same for 114 | inputs and outputs, therefore it doesn't matter if the expression is local or remote. 115 | 116 | The state of the half-port is defined by : 117 | * the *remote request* flag : if some dependent local expression has a positive request count. 118 | * the *local request* flag : if some dependent remote expression is required by a local effect. 119 | * the *pending toggle* count : the messages sent but not yet acknowledged that will change the remote request flag on 120 | the remote half-port. 121 | 122 | Then we can derive the *inferred request* flag as the xor of the local request and the *pending toggle* odd parity. 123 | This flag reflects the state of the local request before the oldest unacknowledged message was sent, i.e. the state 124 | known to the remote peer when its messages are processed. 125 | 126 | The half-port is considered idle when the *remote request* flag is disabled, the *inferred request* flag is disabled, 127 | and the *pending toggle* count is zero. A new session starts on leaving the idle state and stops on entering it again. 128 | 129 | TODO: example 130 | -------------------------------------------------------------------------------- /docs/electric3-explainer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperfiddle/electric/695e0f2e3c2298d3afb7d0c6e308dfb949dd7c14/docs/electric3-explainer.png -------------------------------------------------------------------------------- /docs/lightning_talk_cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperfiddle/electric/695e0f2e3c2298d3afb7d0c6e308dfb949dd7c14/docs/lightning_talk_cover.png -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | process.env.CHROME_BIN = require('puppeteer').executablePath() 2 | module.exports = function (config) { 3 | config.set({ 4 | browsers: ['Chrome_CI'], 5 | // The directory where the output file lives 6 | basePath: 'out', 7 | // The file itself 8 | files: ['karma-tests.js'], 9 | frameworks: ['cljs-test'], 10 | plugins: ['karma-cljs-test', 'karma-chrome-launcher'], 11 | colors: true, 12 | logLevel: config.LOG_INFO, 13 | client: { 14 | args: ["shadow.test.karma.init"], 15 | singleRun: true 16 | }, 17 | customLaunchers: { 18 | Chrome_CI: { 19 | base: 'ChromeHeadless', 20 | flags: ['--no-sandbox'] 21 | } 22 | } 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "karma": "6.4.0", 4 | "karma-chrome-launcher": "3.1.1", 5 | "karma-cljs-test": "0.1.0", 6 | "puppeteer": "15.2.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /resources/public/hyperfiddle-electric-ui.css: -------------------------------------------------------------------------------- 1 | .hyperfiddle button[aria-disabled=true], input[type="button"][aria-disabled="true"]{ 2 | color: GrayText; 3 | } 4 | 5 | .hyperfiddle button[aria-busy=true] 6 | , input[type=checkbox][aria-busy=true] 7 | { 8 | cursor:wait; 9 | position: relative; 10 | } 11 | 12 | /* Button spinner */ 13 | .hyperfiddle button[aria-busy=true]::before 14 | , .hyperfiddle input[type=checkbox][aria-busy=true]::after 15 | , .hyperfiddle .input-load-mask[aria-busy=true]::after 16 | { 17 | content:""; 18 | position:absolute; 19 | z-index: 1; 20 | width: 0.9em; 21 | height: 0.9em; 22 | margin: auto; 23 | top:0; 24 | bottom:0; 25 | left:0; 26 | right:0; 27 | animation: hyperfiddle-spinner-spin 1s linear infinite; 28 | border-width: 2px; 29 | border-style: solid; 30 | border-left-color: transparent; 31 | border-radius: 50%; 32 | } 33 | 34 | .hyperfiddle .input-load-mask[aria-busy=true] { 35 | position: relative; 36 | } 37 | .hyperfiddle .input-load-mask[aria-busy=true]::after{ 38 | left: auto; 39 | right: 1rem; 40 | } 41 | 42 | /* Button spinner color */ 43 | .hyperfiddle button[aria-busy=true]::before{ 44 | border-color: initial; 45 | border-left-color: transparent; 46 | } 47 | 48 | @keyframes hyperfiddle-spinner-spin{ 49 | from { transform: rotate(0deg); } 50 | to { transform: rotate(360deg); } 51 | } 52 | -------------------------------------------------------------------------------- /resources/public/hyperfiddle-forms.css: -------------------------------------------------------------------------------- 1 | .hyperfiddle input:not([type="radio"]):not([type="checkbox"]), select { 2 | box-sizing: border-box; 3 | /* width:100%; */ 4 | border-width: 1px; 5 | /*border-left-width: 0.2rem;*/ 6 | border-style:solid; 7 | border-radius: 2px; 8 | /* padding: 0.5rem 0.5rem; */ 9 | width: 100%; 10 | } 11 | 12 | .hyperfiddle input[type="checkbox"]{ 13 | margin:0; 14 | width: 1rem; 15 | height: 1rem; 16 | } 17 | 18 | .hyperfiddle table { 19 | table-layout: fixed; 20 | border-collapse: collapse; 21 | } 22 | 23 | 24 | .hyperfiddle .hyperfiddle-select 25 | , .hyperfiddle .hyperfiddle-typeahead 26 | , .hyperfiddle .hyperfiddle-tag-picker 27 | { 28 | /* background-color: red; */ 29 | position: relative; 30 | } 31 | 32 | .hyperfiddle .hyperfiddle-select input, 33 | .hyperfiddle .hyperfiddle-typeahead input 34 | { 35 | border: none; 36 | border-style: none !important; 37 | } 38 | 39 | .hyperfiddle .hyperfiddle-select > ul 40 | , .hyperfiddle .hyperfiddle-typeahead > ul 41 | , .hyperfiddle .hyperfiddle-tag-picker-input-container > ul 42 | { 43 | margin: 0; 44 | padding: 0.25rem 0; 45 | position:absolute; 46 | width: 100%; 47 | z-index: 2; 48 | list-style-type: none; 49 | background-color: white; 50 | box-shadow: 0 0.5rem 1rem gray; 51 | 52 | display: grid; 53 | grid-gap: var(--hf-grid-gap); 54 | } 55 | 56 | .hyperfiddle .hyperfiddle-select > ul > li 57 | , .hyperfiddle .hyperfiddle-typeahead > ul > li 58 | , .hyperfiddle .hyperfiddle-tag-picker-input-container > ul > li 59 | { 60 | height: var(--hf-grid-row-height); 61 | padding: 0 0.5rem; 62 | 63 | } 64 | 65 | .hyperfiddle .hyperfiddle-select > ul > li:hover 66 | , .hyperfiddle .hyperfiddle-typeahead > ul > li:hover 67 | , .hyperfiddle .hyperfiddle-tag-picker-input-container > ul > li:hover 68 | { 69 | cursor: pointer; 70 | } 71 | 72 | .hyperfiddle .hyperfiddle-tag-picker 73 | , .hyperfiddle .hyperfiddle-tag-picker-input-container 74 | , .hyperfiddle .hyperfiddle-tag-picker-items 75 | , .hyperfiddle .hyperfiddle-tag-picker-items > li 76 | { 77 | display: inline-block; 78 | } 79 | 80 | .hyperfiddle .hyperfiddle-tag-picker-items 81 | { 82 | list-style-type: none; 83 | } 84 | 85 | .hyperfiddle .hyperfiddle-tag-picker{ 86 | display: inline-flex; 87 | flex-direction: row; 88 | flex-wrap: wrap; 89 | gap: 0.25rem 0.5rem; 90 | align-items:center; 91 | padding: 0.25rem; 92 | background: white; 93 | } 94 | 95 | .hyperfiddle .hyperfiddle-tag-picker:focus-within{ 96 | box-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.05); 97 | } 98 | 99 | .hyperfiddle .hyperfiddle-tag-picker-items{ 100 | display:contents; 101 | } 102 | 103 | .hyperfiddle .hyperfiddle-tag-picker-items > li 104 | { 105 | border: 1px solid; 106 | box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); 107 | white-space: nowrap; 108 | } 109 | 110 | .hyperfiddle .hyperfiddle-tag-picker-input-container{ 111 | position: relative; 112 | height: 100%; 113 | max-height: 100%; 114 | padding:0; 115 | 116 | } 117 | 118 | .hyperfiddle .hyperfiddle-tag-picker-input-container > input{ 119 | height: 100%; 120 | border-width:0!important; /* TODO drop the !important once we have a clean CSS system (tailwind) */ 121 | outline: none; 122 | } 123 | .hyperfiddle .hyperfiddle-tag-picker-input-container > ul{ 124 | position: absolute; 125 | width: max-content; 126 | margin-top: 0.25rem; 127 | border-radius: 0 0 0.25rem 0.25rem; 128 | box-shadow: none; 129 | box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); 130 | } 131 | 132 | .hyperfiddle .hyperfiddle-tag-picker-items > li { 133 | border-radius: 0.25rem; 134 | padding: 0 0 0 0.5rem; 135 | } 136 | 137 | .hyperfiddle .hyperfiddle-tag-picker-items > li > span { 138 | margin: 0.25rem; 139 | color: rgb(55 65 81); 140 | cursor:pointer; 141 | } 142 | 143 | .hyperfiddle .hyperfiddle-selected 144 | { 145 | background-color: orange; 146 | } 147 | 148 | .hyperfiddle .hyperfiddle-modal-backdrop 149 | { 150 | display: block; 151 | /* background-color: pink; */ 152 | /* opacity: 0.1; */ 153 | position: fixed; 154 | top:0; 155 | bottom:0; 156 | left:0; 157 | right:0; 158 | z-index:1; 159 | } 160 | 161 | .hyperfiddle table td{ 162 | vertical-align: top; 163 | padding: 0.3rem; 164 | border-width: 1px; 165 | border-style: solid; 166 | } 167 | 168 | .hyperfiddle table thead td:empty{ 169 | visibility:hidden; 170 | border: none; 171 | } 172 | -------------------------------------------------------------------------------- /resources/public/hyperfiddle-popover.css: -------------------------------------------------------------------------------- 1 | .hyperfiddle.popover-wrapper{ 2 | display: inline-block; 3 | position: relative; 4 | } 5 | 6 | .hyperfiddle.popover-body{ 7 | position: absolute; 8 | z-index: 2; 9 | width: max-content; 10 | 11 | border: 1px pink solid; 12 | padding: 0.5rem; 13 | background-color: rgb(248 250 252); 14 | box-shadow: 0 0 1rem lightgray; 15 | } 16 | 17 | .hyperfiddle.popover-body:focus-within{ 18 | /* NOTE popover is itself focusable so a click brings it to the top */ 19 | z-index:3; 20 | } 21 | -------------------------------------------------------------------------------- /shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | ;; All namespaces are tested by default to encourage tested code. 2 | ;; Use :ns-regexp to blacklist specific namespaces. 3 | ;; ^(?!foo.(bar|baz)).* : includes everything except foo.bar or foo.baz 4 | 5 | {:builds {:test {:target :node-test 6 | :output-to "out/node-tests.js" 7 | :ns-regexp "^(?!contrib.(electric-codemirror|datomic)|hyperfiddle.(api|popover|spool|spec|electric-fulcro|electric.impl.compiler-test|electric.impl.cljs-file-to-analyze)).*" 8 | :build-options {:cache-level :off} 9 | :compiler-options {:closure-defines {hyperfiddle.electric.impl.missionary-util/wrap true} 10 | :reader-features #{:node} ; allow #?(:node …, :cljs …), falls back to :cljs. 11 | :warnings {:redef-in-file false}}} 12 | :browser-test {:target :karma 13 | :output-to "out/karma-tests.js" 14 | :ns-regexp "^(?!contrib.(electric-codemirror|datomic)|hyperfiddle.(api|popover|spool|spec|electric-fulcro|electric.impl.compiler-test|electric.impl.cljs-file-to-analyze)).*" 15 | :build-options {:cache-level :off} 16 | :build-hooks [(hyperfiddle.browser-test-setup/blow-up-tests-on-warnings)] 17 | :compiler-options {:closure-defines {hyperfiddle.electric.impl.missionary-util/wrap true} 18 | :warnings-as-errors {:warning-types #{:infer-warning}}}}}} 19 | -------------------------------------------------------------------------------- /src-build/build.clj: -------------------------------------------------------------------------------- 1 | (ns build 2 | (:require [clojure.tools.build.api :as tools.build] 3 | [hyperfiddle.build :as build])) 4 | 5 | ;; Expose generic tasks 6 | (def clean #'build/clean) 7 | (def install #'build/install) 8 | (def deploy #'build/deploy) 9 | 10 | (defn build [opts] ; custom build task because of AOT 11 | (build/clean opts) 12 | (let [basis (partial build/create-basis :project "deps.edn", :extra "../electric-secret/deps.edn") 13 | {:keys [class-dir src-dirs] :as opts} (build/defaults (basis :aliases [:release]) opts)] 14 | (tools.build/write-pom opts) 15 | (tools.build/copy-dir {:src-dirs src-dirs, :target-dir class-dir}) 16 | (tools.build/compile-clj {:basis (basis :aliases [:release :build-deps]) 17 | :class-dir class-dir 18 | :ns-compile '[hyperfiddle.electric.impl.entrypoint hyperfiddle.electric.impl.auth hyperfiddle.electric.impl.jwt hyperfiddle.electric.impl.auth0 hyperfiddle.electric.shadow-cljs.hooks3] 19 | :filter-nses '[hyperfiddle.electric.impl.entrypoint hyperfiddle.electric.impl.auth hyperfiddle.electric.impl.jwt hyperfiddle.electric.impl.auth0 hyperfiddle.electric.shadow-cljs.hooks3]}) 20 | (tools.build/jar opts))) 21 | 22 | -------------------------------------------------------------------------------- /src-build/build.md: -------------------------------------------------------------------------------- 1 | # Build clojars maven artifact — Electric 2 | 3 | Build version is to be set under `deps.edn` > `:hyperfiddle.build/version`. 4 | 5 | ```shell 6 | clojure -T:build build 7 | clojure -T:build install 8 | # To test in electric-starter-app: 9 | clj -A:dev -X dev/-main -Sdeps '{:deps {com.hyperfiddle/electric {:mvn/version ""}}}' 10 | # No way to test remote clojars version without rm in .m2/repositories/com/hyperfiddle 11 | # Optional: test electric-starter-app with local maven install 12 | env $(cat .env | xargs) clojure -T:build deploy 13 | ``` 14 | 15 | - `CLOJARS_USERNAME` is your clojars username. 16 | - `CLOJARS_PASSWORD` is not your account password, but rather a genareted token granting 17 | deploy rights to the target coordinates. 18 | - idea: how to run tests cli? (No need, deployed artifacts already passed CI) 19 | -------------------------------------------------------------------------------- /src-dev/toxiproxy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ### INSTALLATION 4 | 5 | # https://github.com/Shopify/toxiproxy 6 | # brew tap shopify/shopify 7 | # brew install toxiproxy 8 | 9 | ### RUNNING 10 | 11 | # src-dev/toxiproxy.sh 7080 8080 200 12 | # toxiproxy-cli list 13 | # killall toxiproxy-server 14 | # toxiproxy-cli toxic help 15 | # toxiproxy-cli toxic update --toxicName hf_latency_toxic --attribute latency=500 hf_dev_proxy 16 | 17 | 18 | log_level=fatal # even 'error' is too spammy 19 | 20 | LOG_LEVEL=${log_level} toxiproxy-server & 21 | echo "waiting for server to come up:" 22 | while ! nc -z localhost 8474; do 23 | printf . 24 | sleep 1 25 | done 26 | echo " server up" 27 | toxiproxy-cli create --listen 0.0.0.0:$1 --upstream localhost:$2 hf_dev_proxy && \ 28 | toxiproxy-cli toxic add --toxicName hf_latency_toxic --type latency --attribute latency=$3 hf_dev_proxy -------------------------------------------------------------------------------- /src/clj-kondo.exports/hyperfiddle/electric/config.edn: -------------------------------------------------------------------------------- 1 | {:lint-as {hyperfiddle.electric/def clojure.core/def 2 | hyperfiddle.electric/defn clojure.core/defn 3 | hyperfiddle.electric3/defn clojure.core/defn 4 | hyperfiddle.electric3/fn clojure.core/fn 5 | hyperfiddle.electric3/declare clojure.core/declare 6 | hyperfiddle.electric3/cursor clojure.core/for 7 | hyperfiddle.electric3/for clojure.core/for 8 | hyperfiddle.electric/for clojure.core/for 9 | hyperfiddle.electric/with-cycle clojure.core/let 10 | hyperfiddle.electric/fn clojure.core/fn} 11 | :linters {:redundant-expression {:level :off}}} 12 | -------------------------------------------------------------------------------- /src/contrib/electric_codemirror.cljc: -------------------------------------------------------------------------------- 1 | (ns contrib.electric-codemirror 2 | ;; #?(:cljs (:require-macros contrib.electric-codemirror)) 3 | (:require 4 | [clojure.edn :as edn] 5 | [clojure.pprint :as pprint] 6 | #?(:clj clojure.tools.logging) 7 | [hyperfiddle.electric3 :as e :refer [$]] 8 | [hyperfiddle.electric-dom3 :as dom] 9 | [missionary.core :as m] 10 | [hyperfiddle.rcf :as rcf :refer [% tap tests with]] 11 | #?@(:cljs [["@codemirror/language" :as language] 12 | ["@codemirror/state" :refer [EditorState]] 13 | ["@codemirror/commands" :refer [history historyKeymap]] 14 | ["@codemirror/view" :as view :refer [EditorView lineNumbers]] 15 | [nextjournal.clojure-mode :as cm-clj]]))) 16 | 17 | #?(:cljs 18 | (def theme 19 | (.theme EditorView (clj->js {#_#_".cm-content" {:white-space "pre-wrap" 20 | :padding "10px 0"} 21 | "&.cm-focused" {:outline "none"} 22 | #_#_".cm-line" {:padding "0 0.5rem" 23 | :line-height "1.6" 24 | :font-size "16px" 25 | :font-family "var(--code-font)"} 26 | ".cm-matchingBracket" {:border-bottom "1px solid var(--teal-color)" 27 | :color "inherit"} 28 | ".cm-gutters" {:background "transparent" 29 | :border "none"} 30 | ".cm-gutterElement" {:margin-left "5px"} 31 | ;; only show cursor when focused 32 | ".cm-cursor" {:visibility "hidden"} 33 | "&.cm-focused .cm-cursor" {:visibility "visible"}})))) 34 | 35 | #?(:cljs 36 | (defonce inline-extensions 37 | (list 38 | theme 39 | (history) 40 | (language/syntaxHighlighting language/defaultHighlightStyle) 41 | (view/drawSelection #js{:cursorBlinkRate 0}) 42 | ;; cm-clj/default-extensions 43 | ;; (.of view/keymap cm-clj/complete-keymap) 44 | (cm-clj/syntax) 45 | (.of view/keymap historyKeymap) 46 | ))) 47 | 48 | #?(:cljs 49 | (defn make-state [props ^string doc, on-update] 50 | (.create EditorState 51 | #js{:doc doc 52 | :extensions (into-array 53 | (cond->> inline-extensions 54 | (:theme props) (cons (.theme EditorView (clj->js (:theme props)))) 55 | (:readonly props) (cons (.. EditorState -readOnly (of true))) 56 | (not (:inline props)) (into (list (lineNumbers) (language/foldGutter))) 57 | true (cons (.. EditorView -updateListener (of (fn [^js view-update] 58 | (on-update view-update) 59 | true))))))}))) 60 | 61 | #?(:cljs 62 | (defn make-cm! [props on-change] 63 | (new EditorView #js{:parent (:parent props) :state (make-state props "nil" on-change)}))) 64 | 65 | #?(:cljs 66 | (defn cm-set! [^js !cm v] 67 | (.dispatch !cm #js {:changes #js {:insert (str v) 68 | :from 0 :to (.. !cm -state -doc -length)}}))) 69 | 70 | #?(:cljs 71 | (defn codemirror [props] 72 | (let [!hook (atom nil) 73 | >cm-v (m/observe 74 | (fn [!] 75 | #_(println 'cm-mount) 76 | (let [^js !cm (make-cm! props (fn [^js cm-view-update] 77 | (when (and (.. cm-view-update -view -hasFocus) ;; user manual action 78 | (.-docChanged cm-view-update)) 79 | (let [v (.. cm-view-update -state -doc (toString))] 80 | (assert (some? v)) 81 | (! v)))))] 82 | (reset! !hook !cm) ; ref escapes 83 | #(do #_(println 'cm-unmount) (.destroy !cm)))))] 84 | (m/cp [(m/?< (m/watch !hook)) ; cm ref escapes 85 | >cm-v])))) ; this is discrete. Don't accidentally damage this by giving it a nil initial value 86 | 87 | (e/defn CodeMirror [props readf writef controlled-value] 88 | (e/client 89 | (when-some [[!cm >cm-v] (e/input (codemirror props))] ; stable through cv changes 90 | (some-> !cm (cm-set! (writef controlled-value))) ; guard "when true" bug causing NPE in certain tutorials 91 | (doto (e/input (m/relieve {} (m/reductions #(readf %2) controlled-value >cm-v))) ; reduction rebuilt if cv changes, which is fine 92 | #_(as-> $ (println 'cm-v (hash $))))))) 93 | 94 | (defn read-edn [edn-str] 95 | (try (edn/read-string edn-str) 96 | (catch #?(:clj Throwable :cljs :default) t 97 | #?(:clj (clojure.tools.logging/error t) 98 | :cljs (js/console.warn t)) nil))) 99 | 100 | (defn write-edn [edn] (with-out-str (pprint/pprint edn))) 101 | 102 | (e/defn Edn [v] ($ CodeMirror {:parent dom/node} read-edn write-edn v)) 103 | (e/defn String_ [v] ($ CodeMirror {:parent dom/node} identity identity v)) 104 | 105 | #_ 106 | (tests "cm/string" 107 | (def discard (e/run (binding [dom/node js/document.body] 108 | (tap (String_ "hi"))))) 109 | ;; (def line (.querySelector js/document ".cm-line")) 110 | ;; (def content (.querySelector js/document ".cm-line")) 111 | ;; (.dispatchEvent line (js/Event. "mousedown")) 112 | ;; (.dispatchEvent content (js/Event. "mousedown")) 113 | ;; (uit/focus line) 114 | ;; (uit/focus content) 115 | ;; (set! (.-innerText line) "there") 116 | 117 | % := "hi" 118 | ;; TODO I see this works when trying out in the REPL and interacting with the browser manually 119 | ;; but I can't seem to trigger the user-like typing behavior from the test. 120 | ;; Exposing the EditorView doesn't help because the `on-change` handler 121 | ;; checks if the action was a user action. 122 | ;; % := "there" 123 | ;; % := "buddy" 124 | (discard) 125 | ) 126 | -------------------------------------------------------------------------------- /src/contrib/missionary_contrib.cljc: -------------------------------------------------------------------------------- 1 | (ns contrib.missionary-contrib 2 | "staging area, to be considered for missionary inclusion?" 3 | (:require [missionary.core :as m] 4 | [hyperfiddle.rcf :refer [tests]])) 5 | 6 | (defn mix [& flows] (m/ap (m/?> (m/?> (count flows) (m/seed flows))))) 7 | 8 | #?(:clj (defn iterator-consumer "blocking iterable pattern" 9 | [^java.util.Iterator it] 10 | ; why not one thread tied to the iterator extent? 11 | ; (future (while (.hasNext it) (! (.next it)))) 12 | (m/ap 13 | (loop [] 14 | (if (m/? (m/via m/blk (.hasNext it))) 15 | (m/amb (m/? (m/via m/blk (.next it))) (recur)) 16 | (m/amb)))))) 17 | 18 | #?(:clj (defn seq-consumer [xs] ; xs is iterable 19 | (m/ap 20 | (loop [xs xs] 21 | (if (m/? (m/via m/blk (seq xs))) 22 | (m/amb (m/? (m/via m/blk (first xs))) (recur (rest xs))) 23 | (m/amb)))))) 24 | 25 | #?(:clj 26 | (tests 27 | (def !it (.iterator (.keySet {:a 1, :b 2, :c 3, :d 4}))) 28 | (->> (iterator-consumer !it) 29 | (m/eduction (take 3)) 30 | (m/reduce conj []) m/?) 31 | := [:a :b :c] 32 | 33 | ; careful, Java iterator is stateful 34 | 35 | (def xs (iterator-seq (.iterator (.keySet {:a 1, :b 2, :c 3, :d 4})))) 36 | (take 3 xs) := [:a :b :c] 37 | 38 | (->> (seq-consumer xs) 39 | (m/eduction (take 3)) 40 | (m/reduce conj []) m/?) 41 | := [:a :b :c])) 42 | 43 | (defn poll-task 44 | "derive discrete flow from succession of polled values from a task (or mbox)" 45 | [task] 46 | #_(m/ap (m/? (m/?> (m/seed (repeat mbox))))) 47 | (m/ap 48 | (loop [v (m/? task)] 49 | (m/amb v (recur (m/? task)))))) 50 | 51 | (defn document 52 | "compare (document log) to (d/entity db eid). if a datomic txn is [op eid a v], 53 | log here is [op a v], or in other words, there is only one entity (the `eid` is 54 | constant) so we are left with not an entity but a document." 55 | [>txs] 56 | (m/reductions (fn [m [op a v]] ; or is the a actually e? 57 | (case op 58 | ::add (assoc m a v) 59 | ::retract (dissoc m a))) {} >txs)) 60 | 61 | (defn throttle [dur >in] 62 | (m/ap 63 | (let [x (m/?> (m/relieve {} >in))] 64 | (m/amb x (do (m/? (m/sleep dur)) (m/amb)))))) 65 | 66 | (defn delay-flow [>x] 67 | (->> (m/reductions (fn [[_ b] nx] [b nx]) [] >x) 68 | (m/eduction (map second)))) 69 | 70 | ; When waiting for multiple tasks there is m/join and m/race which in terminology 71 | ; of JS Promises are like Promise.all and Promise.race respectively. Is there an 72 | ; equivalent to Promise.allSettled which would wait for all tasks to complete 73 | ; (either success or failure) and does not cancel other tasks when some task fails? 74 | (defn all " 75 | the task result will be a vector of zero-argument functions that you can call 76 | with try/catch to check status" 77 | [& tasks] (apply m/join vector (map m/attempt tasks))) -------------------------------------------------------------------------------- /src/contrib/missionary_core_async.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc ; quick band-aid fix for clj-build needing core-async provided for dynamic analysis 2 | contrib.missionary-core-async 3 | "Missionary adapters for core.async. Isolated so as to not create a hard 4 | library dependency on core.async." 5 | (:import (missionary Cancelled)) 6 | (:require [clojure.core.async :as a] 7 | [missionary.core :as m] 8 | [hyperfiddle.rcf :refer [tests]])) 9 | 10 | (defn chan-read! 11 | "Return a task taking one value from `chan`. Return nil if chan is closed. Does 12 | not close chan. Stops waiting for chan when cancelled." 13 | ([chan] (chan-read! chan (Cancelled.))) 14 | ([chan cancelled-value] 15 | (fn [success failure] ; a task is a 2-args function, success and failure are callbacks. 16 | (let [cancel-chan (a/chan)] ; we will put a value on this chan to cancel reading from `chan` 17 | (a/go (let [[v port] (a/alts! [chan cancel-chan])] ; race two chans 18 | (if (= port cancel-chan) ; if the winning chan is the cancelation one, then task has been cancelled 19 | (failure cancelled-value) ; task has been cancelled, must produce a failure state 20 | (success v)))) ; complete task with value from chan 21 | (fn cancel [] 22 | ;; if this task is cancelled by its parent process, close the cancel-chan 23 | ;; which will make cancel-chan produce `nil` and cause cancellation of read on `chan`. 24 | (a/close! cancel-chan)))))) 25 | 26 | (defn chan->ap 27 | "Adapt a core.async channel to a discrete flow" 28 | [ch] 29 | (m/ap 30 | (loop [] 31 | (if-some [x (m/? (chan-read! ch))] ; wait for one value, nil means channel closed. 32 | ;; We successfully read a non-nil value, we use `m/amb` with two branches. m/amb will fork 33 | ;; the current process (ap) and do two things sequentially, in two branches: 34 | ;; - return x, meaning `loop` ends and return x, ap will produce x 35 | ;; - recur to read the next value from chan 36 | (m/amb x (recur)) 37 | ;; nil means the channel has been closed, so terminate this flow without producing any value 38 | ;; (not even nil). We use (m/amb) which produces nothing and terminates immediately. The 39 | ;; parent m/ap block has nothing to produce anymore and will also terminate. 40 | (m/amb))))) 41 | 42 | (defmacro use-channel ; TODO rename 43 | ([chan] `(use-channel nil ~chan)) 44 | ([init chan] `(new (m/reductions {} ~init (chan->ap ~chan))))) 45 | 46 | (defn chan->task [ch] 47 | ; for streaming database results into a vector at the repl (which is not great) 48 | (->> (chan->ap ch) 49 | (m/reduce into []))) 50 | 51 | ;(defn chan->cp [ch] (->> (chan->ap ch) (m/reductions into []))) ; is this useful? Channels are discrete 52 | -------------------------------------------------------------------------------- /src/contrib/test_match.clj: -------------------------------------------------------------------------------- 1 | (ns contrib.test-match 2 | (:require [hyperfiddle.rcf :as rcf :refer [tests]] 3 | [fipp.ednize])) 4 | 5 | (set! *warn-on-reflection* true) 6 | 7 | ;; test matcher 8 | ;; goal: succinct, simple, useful test output 9 | ;; patterns: 10 | ;; _ -> any value 11 | ;; _& -> any values 12 | ;; view f subpat -> match subpat on (f v) 13 | ;; 14 | ;; missing: 15 | ;; - strict map check (all keys) 16 | ;; - compiler instead of interpreter 17 | ;; - locals inside pattern 18 | ;; - unification (logical vars) 19 | ;; - guard predicates 20 | ;; 21 | ;; differences to matcher-combinators: 22 | ;; - no pretty printing. Result can be used programatically 23 | ;; - more concise syntax 24 | ;; - fits on 1 page 25 | ;; - test framework agnostic 26 | ;; 27 | ;; how to check if pattern matched? 28 | ;; (= v (test-match v pat)) 29 | ;; this is also RCF-friendly 30 | 31 | (deftype Diff [a b] 32 | Object 33 | (toString [_] (str "<>")) 34 | (hashCode [_] (+ (.hashCode a) (.hashCode b))) 35 | (equals [_ that] 36 | (and (instance? Diff that) 37 | (= a (.-a ^Diff that)) (= b (.-b ^Diff that)))) 38 | clojure.lang.IPersistentCollection 39 | (equiv [this that] (.equals this that)) 40 | fipp.ednize/IEdn 41 | (-edn [_] (list '<Diff 1 [2]) := (->Diff 1 [2]) 46 | #{(->Diff 1 [2])} := #{(->Diff 1 [2])} 47 | ) 48 | 49 | (defmethod print-method Diff [^Diff d ^java.io.Writer w] 50 | (.write w "<>")) 51 | 52 | (deftype Missing [] 53 | Object (toString [_] "_") 54 | fipp.ednize/IEdn (-edn [_] '_) 55 | ) 56 | (defmethod print-method Missing [_ ^java.io.Writer w] (.write w "_")) 57 | (def missing (Missing.)) 58 | 59 | (defn pair [v pat] 60 | (loop [v v, pat pat, ret []] 61 | (let [v* (if (seq v) (first v) missing) 62 | pat* (if (seq pat) (first pat) missing)] 63 | (if (= missing v* pat*) 64 | ret 65 | (recur (rest v) (rest pat) (conj ret [v* pat*])))))) 66 | 67 | (tests 68 | (pair [1] [:a]) := [[1 :a]] 69 | (pair [1 2] [:a]) := [[1 :a] [2 missing]] 70 | (pair [1] [:a :b]) := [[1 :a] [missing :b]] 71 | ) 72 | 73 | (defn diffs-over-50%? [v] (> (/ (count ((group-by #(instance? Diff %) v) true)) (count v)) 0.5)) 74 | 75 | (tests 76 | (diffs-over-50%? [1 2 3]) := false 77 | (diffs-over-50%? [(->Diff 1 2) (->Diff 2 3) 3]) 78 | (diffs-over-50%? [(->Diff 1 2) 2]) := false 79 | (diffs-over-50%? [(->Diff 1 2) (->Diff 2 3)]) := true) 80 | 81 | (defn test-match [v pat] 82 | (cond 83 | (coll? pat) (if (and (or (list? pat) (seq? pat)) (= `view (first pat))) 84 | ;; TODO turn into pattern compiler so we don't need `eval` 85 | (let [[_ ap subpat] pat, subv (eval (list ap v)), ret (test-match subv subpat)] 86 | (if (= subv ret) v ret)) 87 | (if (coll? v) 88 | (cond 89 | (map? v) 90 | (if (map? pat) 91 | (let [[v pat] (reduce-kv (fn [[ac pat] k v] 92 | (if (contains? pat k) 93 | [(assoc ac k (test-match v (get pat k))) (dissoc pat k)] 94 | [(assoc ac k v) pat])) 95 | [{} pat] v)] 96 | (reduce-kv (fn [ac k pat] (assoc ac k (test-match missing pat))) v pat)) 97 | (->Diff v pat)) 98 | 99 | (set? v) 100 | (if (set? pat) 101 | (reduce (fn [v nx] (if (contains? v nx) v (conj v (->Diff missing nx)))) v pat) 102 | (->Diff v pat)) 103 | 104 | :else 105 | (let [ret (first (reduce (fn [[ac care?] [v pat]] 106 | (if care? 107 | (let [ret (test-match v pat)] 108 | (if (= ::dont-care ret) 109 | [(conj ac v) false] 110 | [(conj ac ret) care?])) 111 | [(conj ac v) false])) 112 | [(empty v) true] (pair v pat))) 113 | listy-v? (or (list? v) (seq? v)), listy-pat? (or (list? pat) (seq? pat))] 114 | (if (and (seq v) (diffs-over-50%? ret)) 115 | (->Diff (into (empty v) (map #(if (instance? Diff %) (.-a ^Diff %) %)) ret) 116 | (into (empty pat) (map #(if (instance? Diff %) (.-b ^Diff %) %)) (cond-> ret (not= listy-v? listy-pat?) reverse))) 117 | (cond-> ret (or (list? v) (seq? v)) reverse)))) 118 | (->Diff v pat))) 119 | (= `_& pat) ::dont-care 120 | (= `_ pat) v 121 | (= v pat) v 122 | :else (->Diff v pat)) 123 | ) 124 | 125 | (tests 126 | (test-match 1 1) := 1 127 | (test-match :x :x) := :x 128 | (test-match 1 0) := (->Diff 1 0) 129 | (test-match 1 2) := (->Diff 1 2) 130 | (test-match [1 2] [1 2]) := [1 2] 131 | (test-match [1 2] [1 0]) := [1 (->Diff 2 0)] 132 | (test-match '(1 2) [1 0]) := (list 1 (->Diff 2 0)) 133 | (test-match '(1 2) '(1 2)) := '(1 2) 134 | (class (test-match '(1 2) '(1 2))) := (class '(1 2)) 135 | (test-match [1 2 3] [1 2]) := [1 2 (->Diff 3 missing)] 136 | (test-match [1 2] [1 2 3]) := [1 2 (->Diff missing 3)] 137 | (test-match [1 2 3] [1 `_&]) := [1 2 3] 138 | (test-match [1 2 3] [1 `_ 3]) := [1 2 3] 139 | (test-match [1 2] [1 2 `_]) := [1 2 missing] 140 | (test-match [1 [2 3]] [1 [2 `_]]) := [1 [2 3]] 141 | (test-match `(inc (dec x)) `(inc (dec _))) := `(inc (dec x)) 142 | (test-match {:x 1} {:x `_}) := {:x 1} 143 | (test-match {:x 1} 1) := (->Diff {:x 1} 1) 144 | (test-match 1 {:x 1}) := (->Diff 1 {:x 1}) 145 | (test-match {:x 1} {:x 1 :y 2}) := {:x 1 :y (->Diff missing 2)} 146 | (test-match {:x 1} {:x 1 :y `_}) := {:x 1 :y missing} 147 | (test-match {:x 1, :y 2} {:y 2}) := {:x 1, :y 2} 148 | (test-match {:x [1 2], :y 3} {:x [1 `_]}) := {:x [1 2], :y 3} 149 | (test-match [1 2] `(view first 1)) := [1 2] 150 | (test-match [1 2] `(view first 2)) := (->Diff 1 2) 151 | (test-match [1 2] `[(view identity 0) 2]) := [(->Diff 1 0) 2] 152 | (test-match [1 2] [3 4]) := (->Diff [1 2] [3 4]) 153 | (test-match '(1 2) [3 4]) := (->Diff '(1 2) [3 4]) 154 | (test-match [1 2] '(3 4)) := (->Diff [1 2] '(3 4)) 155 | (test-match '(1 2) '(3 4)) := (->Diff '(1 2) '(3 4)) 156 | (test-match [] []) := [] 157 | (test-match #{1 2 3} #{1 2}) := #{1 2 3} 158 | (test-match #{1 2} #{2 3}) := #{1 2 (->Diff missing 3)} 159 | (test-match #{1 2 3} [1 2]) := (->Diff #{1 2 3} [1 2]) 160 | (test-match {:a 1} [:a 1]) := (->Diff {:a 1} [:a 1]) 161 | 162 | (require '[hyperfiddle.electric.impl.lang3 :as-alias lang]) 163 | (require '[hyperfiddle.electric.impl.runtime3 :as-alias r]) 164 | (let [v `(r/peer 165 | (lang/r-defs 166 | (lang/r-static 1) 167 | (lang/r-ap (lang/r-static 168 | (clojure.core/fn [x32133] 169 | (clojure.core/fn [& rest-args32134] 170 | (clojure.core/let [x x32133] 171 | (clojure.core/apply (fn* ([] x)) rest-args32134))))) 172 | (lang/r-local 0))) 173 | [] 1)] 174 | (test-match v 175 | `(r/peer 176 | (lang/r-defs 177 | (lang/r-static 1) 178 | (lang/r-ap (lang/r-static (clojure.core/fn _&)) 179 | (lang/r-local 0))) 180 | [] 1)) := v) 181 | ) 182 | -------------------------------------------------------------------------------- /src/contrib/trace3.cljc: -------------------------------------------------------------------------------- 1 | (ns contrib.trace3 2 | (:require 3 | #?(:clj [contrib.triple-store :as ts]) 4 | [clojure.math :as math] 5 | [dustingetz.str] 6 | [hyperfiddle.electric3 :as e :refer [$]] 7 | [hyperfiddle.electric-dom3 :as dom] 8 | #?(:cljs [hyperfiddle.electric.impl.runtime3 :refer [Failure]]) 9 | [hyperfiddle.token-zoo0 :refer [CyclicToken StampedToken TokenNofail]] 10 | [missionary.core :as m]) 11 | #?(:clj (:import [hyperfiddle.electric.impl.runtime3 Failure])) 12 | #?(:cljs (:require-macros contrib.trace3))) 13 | 14 | (e/declare current) 15 | (declare !db db !measure measure !q q) 16 | (let [c (atom {})] 17 | (defn ->trace-id [nm] 18 | [nm (-> (swap! c update nm (fnil inc 0)) (get nm))])) 19 | (defn ->stamp ([] #?(:clj (System/currentTimeMillis) :cljs (.now js/Date))) ([_] (->stamp))) 20 | (let [!i (atom 0)] (defn ->id [_] (swap! !i inc))) 21 | 22 | #?(:clj 23 | (defn insert-trace [db trace] 24 | (if (::id trace) 25 | (cond-> db (not (ts/find db ::id (::id trace))) (ts/add (assoc trace :db/id (->id trace)))) 26 | (ts/add db (assoc trace :db/id (->id trace)))))) 27 | 28 | (defn save-trace [!db trace] #?(:clj (swap! !db insert-trace trace))) 29 | (defn push-trace [!q trace] #?(:cljs (swap! !q conj trace))) 30 | 31 | (defn save-trace! [trace !db !q] 32 | (if (instance? Failure !db) 33 | (push-trace !q trace) 34 | (save-trace !db trace))) 35 | 36 | (defn ->stable-trace-id [v] [v 0]) 37 | 38 | (letfn [(save [!db !q a b c] 39 | (save-trace! a !db !q) 40 | (save-trace! b !db !q) 41 | (save-trace! c !db !q))] 42 | (e/defn Trace 43 | ([nm F] ($ Trace nm identity F)) 44 | ([nm ->pretty F] ($ Trace nm ->trace-id ->pretty F)) 45 | ([nm ->trace-id ->pretty F] 46 | (let [nm (->trace-id nm)] 47 | (save-trace! {::id nm, ::parent current} !db !q) 48 | (save-trace! {::v ::mount, ::v-of nm, ::stamp (->stamp), ::pretty-v "🟢"} !db !q) 49 | (let [v (binding [current nm] ($ F))] 50 | (save-trace! {::v-of nm, ::stamp (->stamp v), ::v v, ::pretty-v (->pretty v)} !db !q) 51 | (e/on-unmount #(save-trace! {::v-of nm, ::stamp (->stamp), ::v ::unmount, ::pretty-v "🔴"} !db !q)) 52 | v))))) 53 | 54 | (defmacro trace 55 | ([nm form] `(trace ~nm identity ~form)) 56 | ([nm ->pretty form] `(trace ~nm ->trace-id ~->pretty ~form)) 57 | ([nm ->trace-id ->pretty form] `($ Trace ~nm ~->trace-id ~->pretty (e/fn [] ~form)))) 58 | 59 | (defn ->queue 60 | ([] #?(:clj clojure.lang.PersistentQueue/EMPTY :cljs #queue [])) 61 | ([& args] (into (->queue) args))) 62 | 63 | #?(:clj (defn save-traces [!db trace+] (swap! !db (fn [db] (reduce insert-trace db trace+))))) 64 | 65 | (e/defn SendClientTraces [ms] 66 | (e/client 67 | (when-some [spend! ($ CyclicToken (seq q))] 68 | (case ($ e/Task (m/sleep ms)) 69 | (let [[trace+] (swap-vals! !q (constantly []))] 70 | (spend! (e/server (save-traces !db trace+)))))))) 71 | 72 | (defmacro with-defaults [& body] 73 | `(let [!db# (e/server (atom (ts/->ts))), m# (e/server (atom (->queue nil nil))), q# (e/client (atom []))] 74 | (binding [!db !db#, db (e/server (e/watch !db#)), !measure m#, measure (e/server (e/watch m#)) 75 | !q q#, q (e/client (e/watch q#))] 76 | ($ SendClientTraces 80) 77 | ~@body))) 78 | 79 | #?(:clj (defn get-latest-pretty-v [db id] 80 | (->> (ts/find db ::v-of id) reverse first (ts/->node db) ::pretty-v))) 81 | 82 | (defn ->pretty [v] (if (nil? v) "␀" v)) 83 | 84 | (e/defn RenderPoint [e depth] 85 | (e/client 86 | (let [nd (e/server (ts/->node db e))] 87 | (dom/span 88 | (dom/props {:style {:margin-left (str (* 12 depth) "px")}}) 89 | (dom/text (e/server (-> (::id nd) first name symbol)))) 90 | (dom/span 91 | (dom/text (e/server (->pretty (get-latest-pretty-v db (::id nd)))))) 92 | (e/cursor [c (e/server (e/diff-by identity (ts/find db ::parent (::id nd))))] 93 | ($ RenderPoint c (inc depth)))))) 94 | 95 | (def pixel-secs 1000) 96 | 97 | (e/defn RenderPointHistory [vs-e origin] 98 | (dom/div 99 | (dom/props {:style {:position "relative"}}) 100 | (e/cursor [ve (e/diff-by identity vs-e)] 101 | (let [nd (e/server (ts/->node db ve)) 102 | ;; 200ms difference 103 | ;; 10px = 1sec = 1000ms 104 | ;; 10px/1000ms = offset/200ms 105 | ;; offset = 200ms*10px/1000ms = 2px 106 | offset (-> (e/server (::stamp nd)) (- origin) (* pixel-secs) (quot 1000))] 107 | (dom/span 108 | (dom/props {:style {:position "absolute", :left (str offset "px")} 109 | :title (e/server (dustingetz.str/pprint-str nd))}) 110 | (dom/text (->pretty (e/server (::pretty-v nd)))) 111 | (when-some [spend! (TokenNofail (dom/On "click" identity nil))] 112 | (spend! (e/server (swap! !measure (fn [m] (conj (pop m) ve))))))))))) 113 | 114 | (e/defn RenderHistory [e origin] 115 | (let [id (e/server (::id (ts/->node db e)))] 116 | ($ RenderPointHistory (e/server (ts/find db ::v-of id)) origin) 117 | (e/cursor [ce (e/server (e/diff-by identity (ts/find db ::parent id)))] 118 | ($ RenderHistory ce origin)))) 119 | 120 | #?(:clj (defn ->origin [db] (->> db :ave ::stamp keys (reduce min)))) 121 | 122 | (e/defn Header [s] (dom/strong (dom/text s))) 123 | 124 | (def grid-color "repeating-linear-gradient(to bottom, #fff 0, #fff 30px, #e5fff5 30px, #e5fff5 60px)") 125 | 126 | (defn time-str [ms] (if (> ms 1000) (str (-> ms (/ 10) math/round (/ 100)) "s") (str ms "ms"))) 127 | #?(:clj (defn measure-distance [db [starte ende]] 128 | (when ende 129 | (abs (- (::stamp (ts/->node db starte)) (::stamp (ts/->node db ende))))))) 130 | 131 | (e/defn Throttle [ms v] 132 | (let [[v2 spend!] ($ StampedToken v)] 133 | (when spend! (spend! ($ e/Task (m/sleep ms)))) 134 | (if spend! v2 v))) 135 | 136 | (e/defn TraceView [] 137 | ;; binding [db ($ Throttle 2000 db)] 138 | (e/client 139 | (dom/div 140 | (dom/props {:class "dstrace"}) 141 | (dom/div (dom/text "Distance: " (e/server (some-> (measure-distance db measure) time-str)))) 142 | (dom/div 143 | (dom/props {:style {:display "flex"}}) 144 | (dom/div 145 | (dom/props {:style {:display "inline-grid", :grid-template-columns "1fr 1fr", :min-width "400px" 146 | :background grid-color, :grid-auto-rows "30px"}}) 147 | ($ Header "Name") ($ Header "Value") 148 | (e/cursor [root-e (e/server (e/diff-by identity (ts/find db ::parent nil)))] 149 | ($ RenderPoint root-e 0))) 150 | (dom/div 151 | (dom/props {:style {:display "inline-grid", :overflow "scroll", :white-space "nowrap", :flex-grow 1 152 | :background grid-color, :grid-auto-rows "30px"}}) 153 | ($ Header "History") 154 | (let [origin (e/server (->origin @!db))] 155 | (e/cursor [root-e (e/server (e/diff-by identity (ts/find db ::parent nil)))] 156 | ($ RenderHistory root-e origin)))))))) 157 | -------------------------------------------------------------------------------- /src/contrib/triple_store.cljc: -------------------------------------------------------------------------------- 1 | (ns contrib.triple-store 2 | (:refer-clojure :exclude [find key]) 3 | (:require [clojure.set :as set] 4 | [contrib.assert :as ca] 5 | [contrib.data :refer [->box]])) 6 | 7 | ;; ts - triple store 8 | ;; e - entity (id of entity) 9 | ;; a - attribute (key of map) 10 | ;; v - value (val of map) 11 | ;; o - options 12 | ;; nd - node, the entity map 13 | ;; ch - cache 14 | 15 | ;; [{:db/id 1, :foo 1, :bar 1} 16 | ;; {:db/id 2, :foo 1, :bar 2}] 17 | ;; eav 1 :foo -> 1 18 | ;; ave :foo 1 -> (sorted-set 1 2) <- sorted so e.g. :parent e is well ordered 19 | ;; vea 1 1 -> #{:foo :bar} CURRENTLY NOT USED/FILLED 20 | 21 | (defrecord TripleStore [o eav ave]) 22 | 23 | (defn ->ts ([] (->ts {})) ([o] (->TripleStore o {} {}))) 24 | 25 | (defn add [ts nd] 26 | (let [e (get nd :db/id) 27 | -eav (->box (:eav ts)), -ave (->box (:ave ts))] 28 | (reduce-kv (fn [_ a v] 29 | (-eav (update (-eav) e assoc a v)) 30 | (-ave (update (-ave) a update v (fnil conj (sorted-set)) e))) 31 | nil nd) 32 | (->TripleStore (:o ts) (-eav) (-ave)))) 33 | 34 | (defn del [ts e] 35 | (let [nd (-> ts :eav (get e)) 36 | {:keys [o eav ave]} ts 37 | eav (dissoc eav e) 38 | ave (reduce-kv (fn [ave a v] (update ave a update v disj e)) ave nd)] 39 | (->TripleStore o eav ave))) 40 | 41 | (defn upd [ts e a f] 42 | (let [v0 (-> ts :eav (get e) (get a)) 43 | eav (update (:eav ts) e update a f) 44 | v1 (-> eav (get e) (get a)) 45 | ave (if (= v0 v1) 46 | (:ave ts) 47 | (let [ave (update (:ave ts) a update v1 (fnil conj (sorted-set)) e) 48 | ave (cond-> ave (contains? (get ave a) v0) (update a update v0 disj e))] 49 | (cond-> ave (not (seq (-> ave (get a) (get v0)))) (update a dissoc v0))))] 50 | (->TripleStore (:o ts) eav ave))) 51 | 52 | (defn asc 53 | ([ts e a v] (upd ts e a (fn [_] v))) 54 | ([ts e a v & avs] (apply asc (asc ts e a v) e avs))) 55 | 56 | ;;;;;;;;;;;;;;; 57 | ;;; HELPERS ;;; 58 | ;;;;;;;;;;;;;;; 59 | 60 | (defn ->node [ts e] (get (:eav ts) e)) 61 | (defn ? [ts e k] (get (->node ts e) k)) 62 | (defn find 63 | ([ts k v] (-> ts :ave (get k) (get v) not-empty)) 64 | ([ts k v & kvs] 65 | (not-empty (reduce set/intersection 66 | (into [] (comp (partition-all 2) (map (fn [[k v]] (-> ts :ave (get k) (get v))))) (list* k v kvs)))))) 67 | (defn find1 [ts & kvs] 68 | (let [vs (apply find ts kvs)] 69 | (ca/check #(= 1 (count %)) vs) 70 | (first vs))) 71 | (defn key [ts k] (when-some [vs (get (:ave ts) k)] (reduce into (vals vs)))) 72 | -------------------------------------------------------------------------------- /src/contrib/walk.cljc: -------------------------------------------------------------------------------- 1 | (ns contrib.walk 2 | "Like clojure.walk, but preserves metadata.") 3 | 4 | ;; NOTE already implemented slightly differently by `edamame.impl.read-fn`. We 5 | ;; should analyse borkdude’s impl and compare with this one. 6 | 7 | (defn has-meta? [o] #?(:clj (instance? clojure.lang.IMeta o) 8 | :cljs (satisfies? IMeta o))) 9 | 10 | (defn supports-with-meta? [o] #?(:clj (instance? clojure.lang.IObj o) 11 | :cljs (satisfies? IWithMeta o))) 12 | 13 | (defn walk [inner outer form] 14 | (cond 15 | (list? form) (outer form (apply list (map inner form))) 16 | (map-entry? form) (outer form (first {(inner (key form)) (inner (val form))})) 17 | (seq? form) (outer form (doall (map inner form))) ;; Must be after `list?` and `map-entry?` 18 | (record? form) (outer form (reduce (fn [r x] (conj r (inner x))) form form)) 19 | (coll? form) (outer form (into (empty form) (map inner form))) 20 | :else (outer form form))) 21 | 22 | (defn forward-metas [form form'] 23 | (if (and (has-meta? form') (supports-with-meta? form')) 24 | (with-meta form' (merge (meta form) (meta form'))) 25 | form')) 26 | 27 | (defn prewalk [f form] 28 | (if (reduced? form) 29 | (unreduced form) 30 | (unreduced (walk (partial prewalk f) forward-metas (f form))))) 31 | 32 | (defn postwalk [f form] 33 | (if (reduced? form) 34 | (unreduced form) 35 | (unreduced (walk (partial postwalk f) (fn [form form'] (forward-metas form (f (forward-metas form form')))) 36 | form)))) 37 | -------------------------------------------------------------------------------- /src/hyperfiddle/detest.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.detest 2 | (:import #?(:clj [clojure.lang IFn IDeref]) 3 | #?(:clj [clojure.lang ExceptionInfo]) 4 | #?(:cljs [goog.math Long]) 5 | [missionary Cancelled]) 6 | (:require [hyperfiddle.incseq.flow-protocol-enforcer :as fpe] 7 | #?(:cljs [contrib.data :refer [->box]]) 8 | [contrib.debug :as dbg] 9 | [clojure.string :as str])) 10 | 11 | ;; DETErministic TESTing, so naturally, DETEST 12 | 13 | #?(:clj 14 | (defn ->xorshift64 [seed] 15 | (let [!v (atom seed)] 16 | (fn step 17 | ([] (swap! !v (fn [v] 18 | (let [v (bit-xor v (bit-shift-left v 7))] 19 | (bit-xor v (unsigned-bit-shift-right v 9)))))) 20 | ([n] (if (zero? n) n (mod (step) n))))))) 21 | 22 | #?(:clj (defn random-seed [] (-> java.security.SecureRandom new .nextLong))) 23 | 24 | #?(:cljs 25 | (defn ->xorshift64 [seed] 26 | (let [ (->box seed) 27 | step (fn [] (let [^Long v () 28 | ^Long v (.xor v (.shiftLeft v 7))] 29 | ( (.xor v (.shiftRightUnsigned v 9)))))] 30 | (fn self 31 | ([] (let [^Long v (step)] (.toNumber v))) 32 | ([n] (if (zero? n) n (let [^Long v (step)] (mod (.toNumber v) n)))))))) 33 | 34 | #?(:cljs (defn random-seed [] (Long/fromBits (rand-int 0x100000000) (rand-int 0x100000000)))) 35 | 36 | (defprotocol Engine 37 | (exercise [this flow]) 38 | (->rng [this]) 39 | (->opts [this]) 40 | (->dbgf [this]) 41 | (roll [this] [this nm]) 42 | (add-proc [this proc]) 43 | (del-proc [this proc])) 44 | 45 | (defn instrument [nm ngn flow] 46 | (fn [step done] 47 | (let [it ((dbg/instrument* nm (->dbgf ngn) flow) step done)] 48 | (reify 49 | IFn 50 | (#?(:clj invoke :cljs -invoke) [_] (it)) 51 | (#?(:clj invoke :cljs -invoke) [_ n] ((it :process) n)) 52 | IDeref 53 | (#?(:clj deref :cljs -deref) [_] @it))))) 54 | 55 | (defn on-violate 56 | ([nm msg] (on-violate nm msg nil)) 57 | ([nm msg e] (throw (ex-info (str nm " flow protocol violation: " msg) {} e)))) 58 | 59 | (defn debug? [ngn] (-> ngn ->opts :debug)) 60 | 61 | (defn ->engine 62 | ([] (->engine {})) 63 | ([{:keys [seed] :as o}] 64 | (let [seed (or seed (random-seed)), rng (->xorshift64 seed), !proc* (atom []) 65 | !n (atom 0), dbgf (case (:debug o) 66 | (:steps) (fn [_] (swap! !n inc)) 67 | (:full) (fn [x] (swap! !n inc) (prn x)) 68 | #_else prn)] 69 | (reify Engine 70 | (add-proc [_ proc] (swap! !proc* conj proc)) 71 | (del-proc [_ proc] (swap! !proc* (fn [proc*] (filterv #(not= % proc) proc*)))) 72 | (roll [_] (rng)) 73 | (roll [_ n] (rng n)) 74 | (->rng [_] rng) 75 | (->opts [_] o) 76 | (->dbgf [_] dbgf) 77 | (exercise [this flow] 78 | (try (let [flow (fpe/enforce {:name 'root, :on-violate on-violate} 79 | (cond->> flow (debug? this) (dbg/instrument* 'root dbgf))) 80 | !s (atom nil) 81 | root (flow #(reset! !s :step) #(reset! !s :done))] 82 | (add-proc this root) 83 | (while (not= :done @!s) 84 | (let [proc* @!proc*, n (rng (count proc*)), proc (nth proc* n)] 85 | (if (= proc root) 86 | (case @!s 87 | (:done nil) (when (> 1 (rng 100)) (root)) 88 | (:step) (condp > (rng 100) 89 | 1 (root) 90 | 25 nil 91 | #_else (try (reset! !s nil) @root 92 | (catch Cancelled _) 93 | (catch ExceptionInfo e 94 | (when-not (str/starts-with? (ex-message e) "[DETEST OK] ") 95 | (throw e)))))) 96 | (proc (rng))))) 97 | (dotimes [_ (rng 10)] (root))) 98 | (catch #?(:clj Throwable :cljs :default) e 99 | (throw (ex-info (str "exercise failed") {:seed seed, :steps @!n} e))))))))) 100 | 101 | (defn minimize [ngn flow] 102 | (try (exercise ngn flow) 103 | (catch ExceptionInfo e 104 | (let [[n0] (), n1 (-> e ex-data :steps)] 105 | (when (or (nil? n0) (< n1 n0)) ( [n1 (-> e ex-data :seed)])))))) 106 | -------------------------------------------------------------------------------- /src/hyperfiddle/domlike.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.domlike " 2 | A mutable tree implementation with an API isomorphic to a subset of the DOM. 3 | ") 4 | 5 | (defn node " 6 | Return a fresh node. 7 | " [] 8 | (doto (object-array 3) 9 | (aset 2 []))) 10 | 11 | (defn parent " 12 | Return `node`'s current parent. 13 | " [^objects node] 14 | (aget node 0)) 15 | 16 | (defn set-parent " 17 | Assign `node`'s parent to `parent`. 18 | " [^objects node parent] 19 | (aset node 0 parent)) 20 | 21 | (defn index " 22 | Return `node`'s current index. 23 | " [^objects node] 24 | (aget node 1)) 25 | 26 | (defn set-index " 27 | Assign `node`'s index to `index`. 28 | " [^objects node index] 29 | (aset node 1 index)) 30 | 31 | (defn children " 32 | Return `node`'s current children. 33 | " [^objects node] 34 | (aget node 2)) 35 | 36 | (defn set-children " 37 | Assign `node`s children to `children`. 38 | " [^objects node children] 39 | (aset node 2 children)) 40 | 41 | (defn nth-child " 42 | Return `node`'s child in position `i`, or `nil` if out of bounds. 43 | " [node i] 44 | (nth (children node) i nil)) 45 | 46 | (defn remove-at [node i] 47 | (let [v (children node)] 48 | (set-children node 49 | (into (subvec v 0 i) 50 | (map (fn [c] (set-index c (dec (index c))) c)) 51 | (subvec v (inc i)))))) 52 | 53 | (defn remove-child " 54 | Remove `child` from `node`'s children and return the removed node. 55 | " [node child] 56 | (when-not (identical? node (parent child)) 57 | (throw (#?(:clj Error. :cljs js/Error.) "not a child"))) 58 | (remove-at node (index child)) 59 | (set-parent child nil) 60 | (set-index child nil) 61 | child) 62 | 63 | (defn replace-child " 64 | Replace `old` by `child` in `node`'s children and return the removed node. 65 | " [node child old] 66 | (when-not (identical? node (parent old)) 67 | (throw (#?(:clj Error. :cljs js/Error.) "not a child"))) 68 | (when-some [p (parent child)] 69 | (remove-at p (index child))) 70 | (set-parent child node) 71 | (set-index child (index old)) 72 | (set-children node 73 | (assoc (children node) 74 | (index old) child)) 75 | (set-parent old nil) 76 | (set-index old nil) 77 | old) 78 | 79 | (defn insert-before " 80 | Insert `child` before `sibling` in `node`s children and return the added node. 81 | " [node child sibling] 82 | (when-not (nil? sibling) 83 | (when (identical? child sibling) 84 | (throw (#?(:clj Error. :cljs js/Error.) "insert before self"))) 85 | (when-not (identical? node (parent sibling)) 86 | (throw (#?(:clj Error. :cljs js/Error.) "not a child")))) 87 | (when-some [p (parent child)] 88 | (remove-at p (index child))) 89 | (let [v (children node) 90 | i (if (nil? sibling) 91 | (count v) 92 | (index sibling))] 93 | (set-parent child node) 94 | (set-index child i) 95 | (set-children node 96 | (-> [] 97 | (into (subvec v 0 i)) 98 | (conj child) 99 | (into (map (fn [c] (set-index c (inc (index c))) c)) 100 | (subvec v i)))) 101 | child)) 102 | 103 | (defn append-child " 104 | Adds `child` at the end of `node`'s children and return the added node. 105 | " [node child] 106 | (insert-before node child nil)) 107 | 108 | (defn tree " 109 | Return a snapshot of the tree rooted at `node`. 110 | " [node] (into [node] (map tree) (children node))) -------------------------------------------------------------------------------- /src/hyperfiddle/electric/impl/array_fields.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.electric.impl.array-fields 2 | (:refer-clojure :exclude [get set]) 3 | #?(:cljs (:require-macros hyperfiddle.electric.impl.array-fields)) 4 | (:require [hyperfiddle.rcf :as rcf :refer [tests]])) 5 | #?(:clj (set! *warn-on-reflection* true)) 6 | (defmacro deffields [& fields] 7 | `(do ~@(for [[fld idx] (mapv vector fields (range))] 8 | `(def ~fld (int ~idx))) 9 | ~(count fields))) 10 | (defn get [^objects a k] (aget a (int k))) 11 | (defn set 12 | ([^objects a i v] (aset a (int i) v)) 13 | ([^objects a i v i2 v2] (aset a (int i) v) (aset a (int i2) v2)) 14 | ([^objects a i v i2 v2 i3 v3] (aset a (int i) v) (aset a (int i2) v2) (aset a (int i3) v3)) 15 | ([^objects a i v i2 v2 i3 v3 i4 v4] (aset a (int i) v) (aset a (int i2) v2) (aset a (int i3) v3) (aset a (int i4) v4)) 16 | ([^objects a i v i2 v2 i3 v3 i4 v4 & more] (set a i v i2 v2 i3 v3 i4 v4) (apply set a more))) 17 | (defn swap 18 | ([^objects a k f] (set a k (f (get a k)))) 19 | ([^objects a k f x] (set a k (f (get a k) x))) 20 | ([^objects a k f x y] (set a k (f (get a k) x y))) 21 | ([^objects a k f x y z] (set a k (f (get a k) x y z))) 22 | ([^objects a k f x y z & more] (set a k (apply f (get a k) x y z more)))) 23 | (defmacro fswap [O k f & args] `(swap (.-state- ~O) ~k ~f ~@args)) 24 | (defmacro fget [O k] `(get (.-state- ~O) ~k)) 25 | (defmacro fset [O & kvs] `(set (.-state- ~O) ~@kvs)) 26 | (defn getset [^objects a k v] (let [ret (get a k)] (when (not= ret v) (set a k v)) ret)) 27 | (defmacro fgetset [O k v] `(getset (.-state- ~O) ~k ~v)) 28 | (defn getswap [^objects a k f] (let [ret (get a k)] (swap a k f) ret)) 29 | (defn set= [^objects a i oldv newv] (if (= oldv (get a i)) (do (set a i newv) true) false)) 30 | (defmacro fset= [O i oldv newv] `(set= (.-state- ~O) ~i ~oldv ~newv)) 31 | (defn set-not= [^objects a i oldv newv] (if (not= oldv (get a i)) (do (set a i newv) true) false)) 32 | (defmacro fset-not= [O i oldv newv] `(set-not= (.-state- ~O) ~i ~oldv ~newv)) 33 | 34 | (defn copy [x y n] #?(:clj (System/arraycopy x 0 y 0 n) :cljs (dotimes [i n] (aset y i (aget x i)))) y) 35 | (defn overfit [k n] (loop [k (* 2 k)] (if (>= k n) k (recur (* 2 k))))) 36 | (defn ensure-fits ^objects [^objects a n] (let [l (alength a)] (cond-> a (< l n) (copy (object-array (overfit l n)) l)))) 37 | 38 | (defn rot 39 | ([^objects a i j] (let [tmp (get a i)] (set a i (get a j) j tmp))) 40 | ([^objects a i j k] (let [tmp (get a i)] (set a i (get a j) j (get a k) k tmp))) 41 | ([^objects a i j k l] (let [tmp (get a i)] (set a i (get a j) j (get a k) k (get a l) l tmp))) 42 | ([^objects a i j k l & more] 43 | (let [tmp (get a i)] 44 | (rot a i j k l) 45 | (loop [[i j :as more] (seq (cons l more))] 46 | (if j 47 | (do (set a i (get a j)) (recur (next more))) 48 | (set a i tmp)))))) 49 | 50 | 51 | ;;; TESTS ;;; 52 | (deftype P [state-]) 53 | (tests 54 | (deffields x y) 55 | (def aP (->P (object-array 2))) 56 | (let [^P aP aP] 57 | (fset aP x 1 y 2) := 2 58 | [(fget aP x) (fget aP y)] := [1 2] 59 | (fswap aP x inc) := 2 60 | (swap (.-state- aP) x inc) := 3 61 | (fgetset aP x 0) := 3 62 | (getset (.-state- aP) x 100) := 0 63 | (fget aP x) := 100 64 | (getswap (.-state- aP) x inc) := 100 65 | (fget aP x) := 101 66 | )) 67 | 68 | (tests 69 | (let [a (object-array [:a :b])] 70 | (rot a 0 1) 71 | (vec a) := [:b :a]) 72 | (let [a (object-array [:a :b :c])] 73 | (rot a 0 2 1) 74 | (vec a) := [:c :a :b]) 75 | (let [a (object-array [:a :b :c :d])] 76 | (rot a 0 2 1 3) 77 | (vec a) := [:c :d :b :a]) 78 | (let [a (object-array [:a :b :c :d :e :f :g])] 79 | (apply rot a (range 7)) 80 | (vec a) := [:b :c :d :e :f :g :a])) 81 | 82 | (tests 83 | (alength (ensure-fits (object-array 2) 9)) := 16 84 | ) 85 | -------------------------------------------------------------------------------- /src/hyperfiddle/electric/impl/destructure.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.electric.impl.destructure 2 | "Cross-platform (clj/cljs) destructuring. Adapted from clojurescript codebase" 3 | (:require [cljs.core] 4 | [hyperfiddle.electric.impl.runtime3 :as r])) 5 | 6 | (defn destructure* [bindings] 7 | (let [bents (partition 2 bindings) 8 | pb (fn pb [bvec b v] 9 | (let [pvec 10 | (fn [bvec b val] 11 | (let [gvec (gensym "vec__") 12 | gseq (gensym "seq__") 13 | gfirst (gensym "first__") 14 | has-rest (some #{'&} b)] 15 | (loop [ret (let [ret (conj bvec gvec val)] 16 | (if has-rest 17 | (conj ret gseq (list `seq gvec)) 18 | ret)) 19 | n 0 20 | bs b 21 | seen-rest? false] 22 | (if (seq bs) 23 | (let [firstb (first bs)] 24 | (cond 25 | (= firstb '&) (recur (pb ret (second bs) gseq) 26 | n 27 | (nnext bs) 28 | true) 29 | (= firstb :as) (pb ret (second bs) gvec) 30 | :else (if seen-rest? 31 | (throw #?(:clj (new Exception "Unsupported binding form, only :as can follow & parameter") 32 | :cljs (new js/Error "Unsupported binding form, only :as can follow & parameter"))) 33 | (recur (pb (if has-rest 34 | (conj ret 35 | gfirst `(first ~gseq) 36 | gseq `(next ~gseq)) 37 | ret) 38 | firstb 39 | (if has-rest 40 | gfirst 41 | (list `nth gvec n nil))) 42 | (inc n) 43 | (next bs) 44 | seen-rest?)))) 45 | ret)))) 46 | pmap 47 | (fn [bvec b v] 48 | (let [gmap (gensym "map__") 49 | defaults (:or b)] 50 | (loop [ret (-> bvec (conj gmap) (conj v) 51 | (conj gmap) (conj `(r/get-destructure-map ~gmap)) 52 | ((fn [ret] 53 | (if (:as b) 54 | (conj ret (:as b) gmap) 55 | ret)))) 56 | bes (let [transforms 57 | (reduce 58 | (fn [transforms mk] 59 | (if (keyword? mk) 60 | (let [mkns (namespace mk) 61 | mkn (name mk)] 62 | (cond (= mkn "keys") (assoc transforms mk #(keyword (or mkns (namespace %)) (name %))) 63 | (= mkn "syms") (assoc transforms mk #(list `quote (symbol (or mkns (namespace %)) (name %)))) 64 | (= mkn "strs") (assoc transforms mk str) 65 | :else transforms)) 66 | transforms)) 67 | {} 68 | (keys b))] 69 | (reduce 70 | (fn [bes entry] 71 | (reduce #(assoc %1 %2 ((val entry) %2)) 72 | (dissoc bes (key entry)) 73 | ((key entry) bes))) 74 | (dissoc b :as :or) 75 | transforms))] 76 | (if (seq bes) 77 | (let [bb (key (first bes)) 78 | bk (val (first bes)) 79 | local (if #?(:clj (instance? clojure.lang.Named bb) 80 | :cljs (cljs.core/implements? INamed bb)) 81 | (with-meta (symbol nil (name bb)) (meta bb)) 82 | bb) 83 | bv (if (contains? defaults local) 84 | (list 'get gmap bk (defaults local)) 85 | (list 'get gmap bk))] 86 | (recur 87 | (if (or (keyword? bb) (symbol? bb)) ;(ident? bb) 88 | (-> ret (conj local bv)) 89 | (pb ret bb bv)) 90 | (next bes))) 91 | ret))))] 92 | (cond 93 | (symbol? b) (-> bvec (conj (if (namespace b) (symbol (name b)) b)) (conj v)) 94 | (keyword? b) (-> bvec (conj (symbol (name b))) (conj v)) 95 | (vector? b) (pvec bvec b v) 96 | (map? b) (pmap bvec b v) 97 | :else (throw 98 | #?(:clj (new Exception (str "Unsupported binding form: " b)) 99 | :cljs (new js/Error (str "Unsupported binding form: " b))))))) 100 | process-entry (fn [bvec b] (pb bvec (first b) (second b)))] 101 | (if (every? symbol? (map first bents)) 102 | bindings 103 | (if-let [kwbs (seq (filter #(keyword? (first %)) bents))] 104 | (throw 105 | #?(:clj (new Exception (str "Unsupported binding key: " (ffirst kwbs))) 106 | :cljs (new js/Error (str "Unsupported binding key: " (ffirst kwbs))))) 107 | (reduce process-entry [] bents))))) 108 | -------------------------------------------------------------------------------- /src/hyperfiddle/electric/impl/event_store.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.electric.impl.event-store 2 | (:refer-clojure :exclude [empty read])) 3 | 4 | (def empty {::events [], ::reductions {}}) 5 | (defn reduct [es k init rf] 6 | (update es ::reductions assoc k {::rf rf, ::init init, ::v (reduce rf init (::events es))})) 7 | (defn read [es k] (-> es ::reductions (get k) ::v)) 8 | (defn act [es evt] 9 | (update es ::reductions (fn [pj*] (update-vals pj* (fn [pj] (update pj ::v (::rf pj) evt)))))) 10 | 11 | (defn add [es evt] 12 | (act (update es ::events conj evt) evt)) 13 | 14 | (comment 15 | (-> empty (add {:foo 1, :bar 2}) 16 | (reduct :foo+ 0 (fn [ac nx] (+ ac (:foo nx)))) 17 | (add {:foo 10, :bar 20}) 18 | (reduct :bar* 1 (fn [ac nx] (* ac (:bar nx)))) 19 | (read :bar*)) 20 | ) 21 | -------------------------------------------------------------------------------- /src/hyperfiddle/electric/impl/lang3.cljs: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.electric.impl.lang3) 2 | 3 | -------------------------------------------------------------------------------- /src/hyperfiddle/electric/impl/lang_3_walkthrough.md: -------------------------------------------------------------------------------- 1 | # Compiler walkthrough 2 | 3 | The electric compiler has 3 major components - expander, analyzer and emitter. 4 | Each stage has its own complications and separating them aids in debugging and 5 | reasoning. The final `compile` var calls them in the correct order to generate 6 | electric runtime code. 7 | 8 | ## Expander 9 | 10 | Expanding correctly across both clj and cljs is tricker than it should be. 11 | Having it as a separate phase helped honing in on the differences and finding 12 | the best solutions. 13 | 14 | The expander expands all macros to electric built-ins. The analyzer can use the 15 | expander to re-expand a built-in to a set of other built-ins. 16 | 17 | Electric Clojurescript macroexpansion is different from stock expansion: 18 | - cljs allows (defn foo) and (defmacro foo) to live alongside, since macroexpansion happens in a separate stage. 19 | - cljs prefers the macro version wherever it can since it generates code GCC can better optimize. 20 | - electric prefers defns since it generate smaller code which should be faster. 21 | 22 | For this reason we have our own analyzer. It's not a full analyzer, just enough to find macros and vars. 23 | 24 | We want to be able to source map electric code and clojure macroexpansion 25 | doesn't forward line/column information since it's stored in the seq's metadata. 26 | The expander takes care to forward metadata on re-expansion so we can later 27 | source map. 28 | 29 | ## Analyzer 30 | 31 | The analyzer is the hardest stage of all. It has to take in the expanded user 32 | code and figure out what electric code we need to generate. 33 | 34 | Since the analyzer is changing all the time, is complicated and I didn't 35 | know/understand all of its requirements I chose to keep all information in a 36 | single triple store. Using a triple store allows working with the data flexibly 37 | and in multiple passes. I built my own simple triple store to gain speed and 38 | customize it to my needs. The triple store has 3 parts 39 | 40 | - o - an options map that can carry arbitrary extra data 41 | - eav - the main index, for `{:db/id 1, :foo :bar}` it looks like `{1 {:db/id 1, :foo :bar}}`, 42 | i.e. we can get our hands on the inserted map through a single map lookup 43 | - ave - the key-value index which allows traversing the graph in arbitrary ways. 44 | For `:foo` from the map before it looks like `{:foo {:bar (sorted-set 1)}}`. The 45 | sorted set is (ab)used in the analyzer to keep track of node ordering in the graph. 46 | 47 | There's 4 main keys we use: 48 | - `:db/id`, used by the triple store internally, as the entity key. We refer to 49 | this value as `e` in the codebase. Function returning the entity ID end with 50 | a `-e` suffix. 51 | - `::type`, to categorize the nodes. 52 | - `::parent`, a universal backreference key, holding the parent's `:db/id` value. 53 | This allows traversing the graph both ways easily. Reading it we can go to the 54 | parent, querying it in the :ave index we can find all children. Since the :ave 55 | index uses a sorted set for the values we get the ordering for free, provided 56 | the children's `:db/ids` are sorted. This is a strength during initial analysis 57 | and poses some problems when doing graph rewrites, as one has to take care to 58 | preserve the ordering during rewrites. 59 | - `::uid`, used as a universal, unchanging ID. When I started implementing graph 60 | rewrites I realized backreferences can get stale. Instead of meticulously 61 | updating all of them I decided to create this unchanging ID which survives all 62 | rewrites. 63 | 64 | The analyzer uses `->id` and `->uid` to generate a monotonically-increasing 65 | integer. Together with the triple store's sorted maps we get node ordering for 66 | free. 67 | 68 | The analyzer operates in multiple passes over the triple store. The first pass 69 | is `analyze`, which takes the expanded user code, potentially re-expands some 70 | forms and produces the first triple store. There are some non-obvious node types: 71 | - `::mklocal` and `::bindlocal` - `let` expands to these, but also `e/letfn` uses 72 | these. `::mklocal` introduces a local and `::bindlocal` binds it. Separating the 73 | creation and binding of the local allows circular and forward references. E.g. 74 | in `e/letfn` if one defines `Foo` and `Bar` we can first introduce the 2 75 | locals through `::mklocal` and bind them with `::bindlocal` afterwards. 76 | - `::localref` - a reference to an electric local. E.g. the returning `x` in `(let [x 1] x)`. 77 | - `::lookup` - in electric all vars are dynamic and can be rebound. This node type 78 | is a lookup into the dynamic binding of the vars. We allow binding through 79 | non-symbolic keys, e.g. we use keywords for some private bindings and numbers 80 | for passing positional arguments. 81 | 82 | `analyze-electric` takes the output of `analyze` and performs deeper analysis 83 | and graph rewrites. The current passes are: 84 | - `compute-effect-order` - reachable nodes get an ::fx-order key with an 85 | increasing integer value denoting their evaluation order. The ordering is 86 | later used to generate side effecting code in correct order as required by the 87 | runtime. 88 | - `mark-used-ctors` - marks and orders all used constructors (e/fns desugar to 89 | ctors). Used means we perform DCE, e.g. in (let [x 1, y (e/ctor 1)] x) we 90 | won't compile the ctor. 91 | - `mark-used-calls2` - inside the marked ctors, marks and order all calls. It's 92 | safe to mark inside ctors since calls can't happen outside of a ctor. 93 | - `reroute-local-aliases` - if a local just aliases another one, reroutes the 94 | references to the origin. E.g. a similar clojure pass would rewrite 95 | `(let [x 1, y x] [y y])` to `(let [x 1] [x x])`. 96 | - `optimize-locals` - walking the code, finding all localrefs, decides whether the 97 | locals need to become runtime nodes. The compiler aggressively inlines when 98 | possible. This pass also has to handle closed over references (free variables). 99 | - `inline-locals` - inlines locals 100 | - `order-nodes` - orders nodes based on compute-effect-order ordering 101 | - `order-frees` - orders frees based on compute-effect-order ordering 102 | - `collapse-ap-with-only-pures` - `(r/ap (r/pure x) (r/pure y) (r/pure z))` can 103 | optimize to 2 cases: 104 | - `(r/ap (r/pure (fn* [] (x y z))))` if `x` is an impure fn 105 | - `(r/pure (x y z))` if `x` is a pure fn 106 | This pass handles both cases. We list pure fns in a hash-map. 107 | 108 | ## Emitter 109 | 110 | This is the simplest part of the compiler. It takes the final triple store as 111 | input has a straightforward mapping from the graph to the final runtime code. 112 | `emit` is the main var which is currently ~40 lines of simple code. `emit-ctor` 113 | is the glue which ties together `emit` and other parts of the emitter to 114 | generate code for a single ctor. 115 | -------------------------------------------------------------------------------- /src/hyperfiddle/electric/impl/pures_fns.clj: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.electric.impl.pures-fns) 2 | 3 | (def pure-fns 4 | '#{clojure.core/* 5 | clojure.core/+ 6 | clojure.core/- 7 | clojure.core// 8 | clojure.core/< 9 | clojure.core/<= 10 | clojure.core/= 11 | clojure.core/== 12 | clojure.core/> 13 | clojure.core/>= 14 | clojure.core/aget 15 | clojure.core/alength 16 | clojure.core/any? 17 | clojure.core/apply 18 | clojure.core/array-map 19 | clojure.core/assoc 20 | clojure.core/assoc-in 21 | clojure.core/atom 22 | clojure.core/boolean 23 | clojure.core/boolean? 24 | clojure.core/butlast 25 | clojure.core/cat 26 | clojure.core/char 27 | clojure.core/class 28 | clojure.core/class? 29 | clojure.core/coll? 30 | clojure.core/comp 31 | clojure.core/comparator 32 | clojure.core/compare 33 | clojure.core/complement 34 | clojure.core/completing 35 | clojure.core/concat 36 | clojure.core/conj 37 | clojure.core/cons 38 | clojure.core/constantly 39 | clojure.core/contains? 40 | clojure.core/count 41 | clojure.core/counted? 42 | clojure.core/cycle 43 | clojure.core/dec 44 | clojure.core/dedupe 45 | clojure.core/delay 46 | clojure.core/delay? 47 | clojure.core/disj 48 | clojure.core/dissoc 49 | clojure.core/distinct 50 | clojure.core/distinct? 51 | clojure.core/drop 52 | clojure.core/drop-last 53 | clojure.core/drop-while 54 | clojure.core/eduction 55 | clojure.core/empty 56 | clojure.core/empty? 57 | clojure.core/even? 58 | clojure.core/every-pred 59 | clojure.core/every? 60 | clojure.core/ex-cause 61 | clojure.core/ex-data 62 | clojure.core/ex-info 63 | clojure.core/ex-message 64 | clojure.core/ffirst 65 | clojure.core/filter 66 | clojure.core/filterv 67 | clojure.core/find 68 | clojure.core/first 69 | clojure.core/flatten 70 | clojure.core/fnext 71 | clojure.core/fnil 72 | clojure.core/format 73 | clojure.core/frequencies 74 | clojure.core/gensym 75 | clojure.core/get 76 | clojure.core/get-in 77 | clojure.core/group-by 78 | clojure.core/hash 79 | clojure.core/hash-map 80 | clojure.core/hash-set 81 | clojure.core/identical? 82 | clojure.core/identity 83 | clojure.core/inc 84 | clojure.core/instance? 85 | clojure.core/interleave 86 | clojure.core/interpose 87 | clojure.core/into 88 | clojure.core/iterate 89 | clojure.core/juxt 90 | clojure.core/keep 91 | clojure.core/keep-indexed 92 | clojure.core/key 93 | clojure.core/keys 94 | clojure.core/keyword 95 | clojure.core/last 96 | clojure.core/list* 97 | clojure.core/list? 98 | clojure.core/map 99 | clojure.core/map-indexed 100 | clojure.core/mapcat 101 | clojure.core/mapv 102 | clojure.core/max 103 | clojure.core/max-key 104 | clojure.core/merge 105 | clojure.core/merge-with 106 | clojure.core/meta 107 | clojure.core/min 108 | clojure.core/min-key 109 | clojure.core/mod 110 | clojure.core/name 111 | clojure.core/namespace 112 | clojure.core/next 113 | clojure.core/nfirst 114 | clojure.core/nil? 115 | clojure.core/nnext 116 | clojure.core/not 117 | clojure.core/not-empty 118 | clojure.core/not= 119 | clojure.core/nth 120 | clojure.core/nthnext 121 | clojure.core/nthrest 122 | clojure.core/odd? 123 | clojure.core/partial 124 | clojure.core/partition 125 | clojure.core/partition-all 126 | clojure.core/partition-by 127 | clojure.core/peek 128 | clojure.core/pop 129 | clojure.core/pos-int? 130 | clojure.core/pos? 131 | clojure.core/quot 132 | clojure.core/range 133 | clojure.core/reduce 134 | clojure.core/reduce-kv 135 | clojure.core/reductions 136 | clojure.core/remove 137 | clojure.core/repeat 138 | clojure.core/repeatedly 139 | clojure.core/rest 140 | clojure.core/reverse 141 | clojure.core/second 142 | clojure.core/select-keys 143 | clojure.core/seq 144 | clojure.core/seq? 145 | clojure.core/sequence 146 | clojure.core/str 147 | clojure.core/string? 148 | clojure.core/subs 149 | clojure.core/symbol 150 | clojure.core/take 151 | clojure.core/take-last 152 | clojure.core/take-nth 153 | clojure.core/take-while 154 | clojure.core/transduce 155 | clojure.core/type 156 | clojure.core/update 157 | clojure.core/update-in 158 | clojure.core/vals 159 | clojure.core/vec 160 | clojure.core/vector 161 | clojure.core/zero? 162 | clojure.core/zipmap 163 | clojure.core/list 164 | missionary.core/absolve 165 | missionary.core/ap 166 | missionary.core/compel 167 | missionary.core/cp 168 | missionary.core/dfv 169 | missionary.core/eduction 170 | missionary.core/group-by 171 | missionary.core/join 172 | missionary.core/latest 173 | missionary.core/mbx 174 | missionary.core/never 175 | missionary.core/none 176 | missionary.core/observe 177 | missionary.core/race 178 | missionary.core/rdv 179 | missionary.core/reduce 180 | missionary.core/reductions 181 | missionary.core/relieve 182 | missionary.core/sample 183 | missionary.core/seed 184 | missionary.core/sem 185 | missionary.core/sleep 186 | missionary.core/sp 187 | missionary.core/timeout 188 | missionary.core/via-call 189 | missionary.core/watch 190 | missionary.core/zip 191 | hyperfiddle.incseq/count 192 | hyperfiddle.incseq/mount-items 193 | hyperfiddle.incseq/combine 194 | hyperfiddle.incseq/compose 195 | hyperfiddle.incseq/cycle 196 | hyperfiddle.incseq/diff-by 197 | hyperfiddle.incseq/empty-diff 198 | hyperfiddle.incseq/fixed 199 | hyperfiddle.incseq/inverse 200 | hyperfiddle.incseq/items 201 | hyperfiddle.incseq/latest-concat 202 | hyperfiddle.incseq/latest-product 203 | hyperfiddle.incseq/patch-vec 204 | hyperfiddle.incseq/spine 205 | hyperfiddle.electric.impl.runtime3/bind 206 | hyperfiddle.electric.impl.runtime3/dispatch 207 | hyperfiddle.electric.impl.runtime3/drain 208 | hyperfiddle.electric.impl.runtime3/get-destructure-map 209 | hyperfiddle.electric.impl.runtime3/incseq 210 | hyperfiddle.electric.impl.runtime3/invariant 211 | hyperfiddle.electric.impl.runtime3/pure 212 | hyperfiddle.electric.impl.runtime3/effect 213 | hyperfiddle.electric.impl.runtime3/fixed-signals 214 | hyperfiddle.electric-dom3/attach! 215 | hyperfiddle.electric-dom3/await-element 216 | hyperfiddle.electric-dom3/await-elements 217 | }) 218 | -------------------------------------------------------------------------------- /src/hyperfiddle/electric3_contrib.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.electric3-contrib 2 | "Experimental operators under consideration for inclusion in Electric core" 3 | (:require [contrib.missionary-contrib :as mx] 4 | [hyperfiddle.electric3 :as e] 5 | [hyperfiddle.incseq :as i] 6 | [missionary.core :as m])) 7 | 8 | (defmacro If [amb-test left right] `(if (e/Some? ~amb-test) ~left ~right)) 9 | 10 | #_(defmacro verse-if [test then else] `(case (e/as-vec ~test) [] ~else ~then)) 11 | #_(defmacro verse-if [test then else] `(e/$ (e/one (e/amb ({} test (e/fn [] ~then)) (e/fn [] ~else))))) 12 | 13 | ; L: I don't think we need e/one but we could have e/take and e/drop, then the one operator is just (e/take 1 ,,,) 14 | 15 | (e/defn None? [xs] (zero? (e/Count xs))) 16 | (e/defn Nothing [& args] (e/amb)) 17 | (e/defn Outputs [xs] (e/join (i/items (e/pure xs)))) 18 | #_(e/defn Seq [xs] (If xs xs nil)) 19 | #_(e/defn Some "return first non-nothing in order" 20 | [xs] (first (e/as-vec xs))) ; todo optimize 21 | 22 | (e/defn Sleep 23 | ([ms x] (e/Task (m/sleep ms x))) 24 | ([ms] (Sleep ms nil))) 25 | 26 | (e/defn Throttle [ms x] (e/input (mx/throttle ms (Outputs x)))) 27 | 28 | (e/defn Latch-stale [x] 29 | (let [!cache (atom [])] 30 | (when (e/Some? x) (reset! !cache (e/as-vec x))) 31 | (e/diff-by {} (e/watch !cache)))) 32 | 33 | (e/defn ^:deprecated Offload-reset "Deprecated. Promoted to `e/Offload-reset`." [f] (e/Offload-reset f)) 34 | (e/defn ^:deprecated Offload-latch "Deprecated. Promoted to `e/Offload-latch`." [f] (e/Offload-latch f)) 35 | 36 | #?(:clj (defonce *tap nil)) ; for repl & survive refresh 37 | #?(:clj (def >tap (m/signal 38 | (m/relieve {} 39 | (m/observe 40 | (fn [!] 41 | (! *tap) 42 | (let [! (fn [x] (def *tap x) (! x))] 43 | (clojure.core/add-tap !) 44 | #(clojure.core/remove-tap !)))))))) 45 | 46 | (e/defn Tap [] (e/server (e/input >tap))) 47 | 48 | (e/defn UnglitchC [x else] 49 | (e/client 50 | (let [[x clock] (e/with-cycle [[p c] [::init 0]] 51 | [x (if (= p x) c (inc c))])] 52 | (e/Reconcile (if (= clock (e/server (identity clock))) x else))))) 53 | 54 | (e/defn UnglitchS [x else] 55 | (e/server 56 | (let [[x clock] (e/with-cycle [[p c] [::init 0]] 57 | [x (if (= p x) c (inc c))])] 58 | (e/Reconcile (if (= clock (e/client (identity clock))) x else))))) 59 | -------------------------------------------------------------------------------- /src/hyperfiddle/electric_dom3_events.cljc: -------------------------------------------------------------------------------- 1 | ;; This ns only exists to prevent code duplication. 2 | ;; Required in dom3 and electric core (to pause Clock on DOM hidden) 3 | 4 | (ns hyperfiddle.electric-dom3-events 5 | (:require [missionary.core :as m])) 6 | 7 | #?(:cljs (defn with-listener 8 | ([n e f] (with-listener n e f nil)) 9 | ([n e f o] (.addEventListener n e f o) #(.removeEventListener n e f o)))) 10 | 11 | #?(:cljs 12 | (defn listen "Takes the same arguments as `addEventListener` and returns an uninitialized 13 | missionary flow that handles the listener's lifecycle producing `(f e)`. 14 | Relieves backpressure. `opts` can be a clojure map." 15 | ([node event-type] (listen node event-type identity)) 16 | ([node event-type f] (listen node event-type f {})) 17 | ([node event-type f opts] 18 | (->> (m/observe (fn [!] (with-listener node event-type #(! (f %)) (clj->js opts)))) 19 | (m/relieve {}))))) 20 | 21 | #?(:cljs 22 | (defn listen-some "Takes the same arguments as `addEventListener` and returns an uninitialized 23 | missionary flow that handles the listener's lifecycle producing `(f e)` unless 24 | the result is `nil`. Relieves backpressure. `opts` can be a clojure map." 25 | ([node event-type] (listen-some node event-type identity)) 26 | ([node event-type f] (listen-some node event-type f {})) 27 | ([node event-type f opts] 28 | (->> (m/observe (fn [!] 29 | (let [! #(some-> (f %) !), opts (clj->js opts)] 30 | (.addEventListener node event-type ! opts) 31 | #(.removeEventListener node event-type ! opts)))) 32 | (m/relieve {})) 33 | ;; alternative implementation 34 | #_(m/eduction (filter some?) (listen node typ f opts))))) 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/hyperfiddle/electric_fulcro_dom_adapter.cljc: -------------------------------------------------------------------------------- 1 | ;; TODO Port to v3 if needed 2 | (ns hyperfiddle.electric-fulcro-dom-adapter 3 | (:require [hyperfiddle.electric :as e] 4 | [hyperfiddle.electric-dom2] 5 | #?(:cljs [goog.object :as gobj]) 6 | #?(:cljs [com.fulcrologic.fulcro.components :as comp :refer [defsc]]) 7 | #?(:cljs [com.fulcrologic.fulcro.dom :as dom :refer [div]])) 8 | #?(:cljs (:require-macros [hyperfiddle.electric-fulcro-dom-adapter]))) 9 | 10 | (def ^:private !props "Contains props of all fulcro->electric instances, keyed by `id`" 11 | ;; Only needed under Electric IC first iteration. Next electric version will 12 | ;; allow server and client to inject arguments. 13 | (atom {})) 14 | 15 | (e/def props (e/watch !props)) ; A single top level watch for better perfs 16 | (e/defn GetProps [id] (get props id)) 17 | 18 | #?(:cljs 19 | (defsc ElectricBridge [this props] 20 | {:componentDidMount (fn [^js this] 21 | (let [reactor ((::electric-program (comp/props this))) 22 | process (reactor 23 | #(js/console.log "Reactor success:" %) 24 | #(js/console.error "Reactor failure:" %))] 25 | (swap! !props assoc-in [(::id (comp/props this)) ::react-ref] (gobj/get this "container")) 26 | (comp/set-state! this {:process process}))) 27 | :componentWillUnmount (fn [this] 28 | ((:process (comp/get-state this))) 29 | (swap! !props dissoc (::id (comp/props this))))} 30 | (div (-> props 31 | (dissoc ::electric-program) 32 | (assoc :ref (fn [r] (gobj/set this "container" r))))))) 33 | 34 | #?(:cljs (def ui-electric-bridge (comp/factory ElectricBridge))) 35 | 36 | (defmacro run-electric! 37 | "Runs an electric program inside a fulcro component. Only one instance of an 38 | electric program can run on a given fulcro page. i.e. one can have multiple 39 | `run-electric!` calls on a fulcro page, but they must all run different 40 | electric programs. 41 | 42 | - `props`: a props map passed to a fulcro-dom wrapper div. Electric will mount 43 | inside of this div. 44 | - `Electric-Entrypoint-Fn`: fully-qualified symbol referring to an Electric e/def or e/defn to run. 45 | - `args-map`: an arbitrary clojure map passed to `Electric-Entrypoint-Fn` as 46 | first argument. Use it to convey reactive values from fulcro to electric. 47 | 48 | Example: 49 | ``` 50 | (ns fulcro-electric-example 51 | (:require [hyperfiddle.electric :as e] 52 | [hyperfiddle.electric-dom2 :as dom] 53 | [hyperfiddle.electric-fulcro-dom-adapter :as fulcro-adapter] 54 | [com.fulcrologic.fulcro.dom :as fulcro-dom]) 55 | 56 | (e/defn ElectricApp [ring-request] ; starts on the server 57 | (e/client 58 | (let [{:keys [this OtherPage ::fulcro-adapter/react-ref]} (fulcro-adapter/GetProps. `ElectricApp)] 59 | (binding [dom/node react-ref] 60 | (dom/button 61 | (dom/on! \"click\" (fn [_e] (dr/change-route this (dr/path-to OtherPage)))) 62 | (dom/text \"Go to OtherPage\")))))) 63 | 64 | (defsc OtherPage [] 65 | {:route-segment [\"page-1\"]]} 66 | (fulcro-dom/div \"page-1\") 67 | 68 | (defsc MainPage [this] 69 | (run-electric! {:id \"my-electric-app-in-main-page\"} fulcro-electric-example/ElectricApp {:this this, :OtherPage OtherPage})) 70 | ``` 71 | " 72 | [props Electric-Entrypoint-Fn args-map] 73 | (assert (qualified-symbol? Electric-Entrypoint-Fn) "Electric-Entrypoint-Fn must be a fully qualified symbol resolving to an e/def or e/defn.") 74 | `(do 75 | (swap! !props update '~Electric-Entrypoint-Fn merge ~args-map) 76 | (ui-electric-bridge (merge ~props {::id '~Electric-Entrypoint-Fn ::electric-program (fn [] (e/boot-client {} ~Electric-Entrypoint-Fn nil))})))) 77 | 78 | -------------------------------------------------------------------------------- /src/hyperfiddle/electric_httpkit_adapter3.clj: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.electric-httpkit-adapter3 2 | "Provide a `wrap-electric-websocket` HTTPKit compatible middleware, starting and 3 | managing an Electric Server. This is a variant of 4 | `hyperfiddle.electric-ring-adapter` made compatible with HTTPKit." 5 | (:require 6 | [clojure.tools.logging :as log] 7 | [hyperfiddle.electric-ring-adapter3 :as ering] 8 | [org.httpkit.server :as httpkit] 9 | [ring.websocket :as ws]) 10 | (:import 11 | (org.httpkit.server AsyncChannel))) 12 | 13 | (defrecord HTTPKitSocket [^AsyncChannel channel] 14 | ering/Socket 15 | (open? [_] (httpkit/open? channel)) 16 | (close [_this code] (.serverClose channel code)) 17 | (close [_this code _reason] (.serverClose channel code)) ; HTTPKit doesn't support close reason 18 | (send [_this value] (httpkit/send! channel {:body value})) 19 | (send [_this value success-cb failure-cb] 20 | (if (httpkit/send! channel {:body value}) 21 | (success-cb) 22 | (failure-cb (ex-info "Can't send message to client, remote channel is closed" {})))) 23 | 24 | ;; ping and pong are not exposed by HTTPKit. Instead ping is replaced by a 25 | ;; special "HEARTBEAT" message that the client will echo. HTTPKit will 26 | ;; automatically answer client pings with an immediate echo pong. 27 | ering/Pingable 28 | (ping [this] (ering/ping this "HEARTBEAT")) 29 | (ping [this value] (assert (= "HEARTBEAT" value)) (ering/send this value)) 30 | (pong [this] (throw (ex-info "Pong is not supported" {}))) 31 | (pong [this value] (throw (ex-info "Pong with arbitrary data is not supported" {}))) 32 | ) 33 | 34 | (defn reject-websocket-handler 35 | "Will accept socket connection upgrade and immediately close the socket on 36 | connection, with given `code` and `reason`. Use this to cleanly reject a 37 | websocket connection." 38 | ;; Rejecting the HTTP 101 Upgrade request would also prevent the socket to 39 | ;; open, but for security reasons, the client is never informed of the HTTP 40 | ;; 101 failure cause. 41 | [code reason] 42 | {:on-open (fn [socket] (ering/close (HTTPKitSocket. socket) code reason))}) 43 | 44 | (def STATUS-CODE 45 | "Map HTTPKit custom WS status names to the actual RFC-defined status code, if it 46 | can be mapped. Fully qualify the status name otherwise." 47 | {:server-close ::server-close 48 | :client-close ::client-close 49 | :normal 1000 50 | :going-away 1001 51 | :protocol-error 1002 52 | :unsupported 1003 53 | :no-status-received 1005 54 | :abnormal 1006 55 | :invalid-payload-data 1007 56 | :policy-violation 1008 57 | :message-too-big 1009 58 | :mandatory-extension 1010 59 | :internal-server-error 1011 60 | :tls-handshake 1015 61 | :unknown ::unknown}) 62 | 63 | (defmethod ering/handle-close-status-code ::server 64 | [_ring-req _socket _status-code & [_reason]] 65 | (log/debug "HTTPKit server closed the websocket connection")) 66 | 67 | (defmethod ering/handle-close-status-code ::client 68 | [_ring-req _socket _status-code & [_reason]] 69 | (log/debug "Websocket client closed the connection for an unknown reason")) 70 | 71 | (defmethod ering/handle-close-status-code ::unknown 72 | [_ring-req _socket _status-code & [_reason]] 73 | (log/debug "HTTPKit websocket connection closed for an unknown reason")) 74 | 75 | (defn httpkit-ws-handler 76 | "Return a map of HTTPkit-compatible handlers, describing how to start and manage an Electric server process, hooked onto a websocket." 77 | ([boot-fn] (httpkit-ws-handler boot-fn nil)) 78 | ([boot-fn ring-req] 79 | (let [{:keys [on-open on-close on-ping #_on-pong #_on-error on-message]} (ering/electric-ws-handler boot-fn ring-req)] 80 | (-> {:init (fn [_socket]) ; called pre handshake, no use case 81 | :on-open on-open 82 | :on-close (fn [socket status-code] 83 | (ering/handle-close-status-code ring-req socket (or (STATUS-CODE status-code) status-code)) 84 | (on-close socket status-code)) 85 | :on-ping on-ping 86 | ;; :on-pong on-pong ; unsupported by HTTPKit 87 | ;; :on-error on-error ; unsupported by HTTPKit 88 | :on-receive on-message} 89 | (update-vals 90 | (fn [f] 91 | (fn [async-channel & args] 92 | (apply f (HTTPKitSocket. async-channel) args)))))))) 93 | 94 | (defn wrap-electric-websocket 95 | "An HTTPKit-compatible ring middleware, starting an Electric server program defined by `electric-boot-fn` on websocket connection. 96 | E.g.: ``` 97 | (-> ring-handler 98 | (wrap-electric-websocket (fn [ring-req] (hyperfiddle.entrypoint/boot-server {} my-ns/MyElectricDefn (e/server ring-req)))) 99 | (wrap-cookies) 100 | (wrap-params) 101 | ) 102 | ``` 103 | " 104 | [next-handler electric-boot-fn] 105 | (fn [ring-request] 106 | (if (ws/upgrade-request? ring-request) 107 | (httpkit/as-channel ring-request 108 | (httpkit-ws-handler (partial electric-boot-fn ring-request) ring-request)) 109 | (next-handler ring-request)))) 110 | -------------------------------------------------------------------------------- /src/hyperfiddle/electric_scroll0.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.electric-scroll0 2 | (:require [clojure.math :as math] 3 | [contrib.data :refer [clamp window]] 4 | [contrib.missionary-contrib :as mx] 5 | [hyperfiddle.electric3 :as e] 6 | [hyperfiddle.electric-dom3 :as dom] 7 | [hyperfiddle.rcf :as rcf :refer [tests with tap %]] 8 | [missionary.core :as m] 9 | [hyperfiddle.electric-local-def3 :as l])) 10 | 11 | #?(:cljs (defn scroll-state [scrollable] 12 | (->> (m/observe 13 | (fn [!] 14 | (let [sample (fn [] (! [(.. scrollable -scrollTop) ; optimization - detect changes (pointless) 15 | (.. scrollable -scrollHeight) ; snapshot height to detect layout shifts in flipped mode 16 | (.. scrollable -clientHeight)]))] ; measured viewport height (scrollbar length) 17 | (sample) #_(! [0 0 0]) ; don't emit 0 when flow is rebuilt 18 | (.addEventListener scrollable "scroll" sample #js {"passive" true}) 19 | #(.removeEventListener scrollable "scroll" sample)))) 20 | (mx/throttle 16) ; RAF interval 21 | (m/relieve {})))) 22 | 23 | #?(:cljs (defn resize-observer [node] 24 | (->> 25 | (m/observe (fn [!] (! [(.-clientHeight node) 26 | (.-clientWidth node)]) 27 | (let [obs (new js/ResizeObserver 28 | (fn [entries] 29 | (let [content-box-size (-> entries (aget 0) .-contentBoxSize (aget 0))] 30 | (! [(.-blockSize content-box-size) 31 | (.-inlineSize content-box-size)]))))] 32 | (.observe obs node) #(.unobserve obs node)))) 33 | (mx/throttle 100) ; Optimization – (un)mounting rows is expensive. 100ms is usually considered "minimum human-perceptible latency". 34 | (m/relieve {})))) 35 | 36 | #?(:cljs (defn compute-overquery [overquery-factor record-count offset limit] 37 | (let [q-limit (* limit overquery-factor) 38 | occluded (clamp (- q-limit limit) 0 record-count) 39 | q-offset (clamp (- offset (math/floor (/ occluded overquery-factor))) 0 record-count)] 40 | [q-offset q-limit]))) 41 | 42 | #?(:cljs (defn compute-scroll-window [row-height record-count clientHeight scrollTop overquery-factor] 43 | (let [padding-top 0 ; e.g. sticky header row 44 | limit (math/ceil (/ (- clientHeight padding-top) row-height)) ; aka page-size 45 | offset (int (/ (clamp scrollTop 0 (* record-count row-height)) ; prevent overscroll past the end 46 | row-height))] 47 | (compute-overquery overquery-factor record-count offset limit)))) 48 | 49 | (e/defn Scroll-window ; returns [offset, limit] 50 | [row-height record-count node 51 | #_& {:keys [overquery-factor] 52 | :or {overquery-factor 1}}] 53 | (e/client 54 | ((fn [_] (set! (.-scrollTop dom/node) 0)) record-count) ; scroll to top on search or navigate 55 | ; backlog: don't touch scrollTop when records are inserted (e.g., live chat view) 56 | (let [[clientHeight] (e/input (resize-observer node)) 57 | [scrollTop scrollHeight #_clientHeight] (e/input (scroll-state node))] ; smooth scroll has already happened, cannot quantize 58 | (compute-scroll-window row-height record-count clientHeight scrollTop overquery-factor)))) 59 | 60 | (e/defn Spool2 [cnt xs! offset limit] ; legacy 61 | (->> xs! 62 | (map vector (cycle (range limit))) 63 | (window cnt offset limit) 64 | (e/diff-by first))) 65 | 66 | (e/defn Spool [cnt xs! offset limit] ; legacy 67 | (->> (map-indexed vector xs!) 68 | (window cnt offset limit) 69 | (e/diff-by #(mod (first %) limit)))) 70 | 71 | (defn index-ring 72 | "Return a vector of numbers of size `size` containing numbers [0..size[ shifted by `offset`, in the direction of sgn(offset). 73 | Contrary to usual \"sliding window\" shifting, where all slots shift one place left or right, this ring shifts as a tape. 74 | Values shift, not slots. Allowing for stable indexing of a sliding window. 75 | e.g.: Usual : [1 2 3] -> [2 3 4] ; shift one right - all values changed, as if all slots shifted. 76 | Tape: [1 2 3] -> [4 2 3] ; 4 replaced 1 at slot 0. Slots 1 and 2 untouched. 77 | Example: assuming size = 5, and offset in [0,1,2,3,4,5] 78 | Offset 79 | 0 -> [0 1 2 3 4] ; 5 slots, initial state 80 | 1 -> [5 1 2 3 4] ; shift one right – 5 replaces 0 at slot 0, 1 2 3 4 untouched 81 | 2 -> [5 6 2 3 4] ; shift one right – 6 replaces 1 at slot 1, 5 and the rest untouched 82 | 3 -> [5 6 7 3 4] 83 | 4 -> [5 6 7 8 4] 84 | 5 -> [5 6 7 8 9] ; offset = size, full window slide, all values shifted, but no slot shifted. 85 | " 86 | [size offset] 87 | (let [start (- size offset)] 88 | (mapv #(+ offset (mod % size)) (range start (+ size start))))) 89 | 90 | (tests 91 | (let [size 7] 92 | (mapv #(index-ring size %) (range (inc size))) 93 | := [[0 1 2 3 4 5 6] 94 | [7 1 2 3 4 5 6] 95 | [7 8 2 3 4 5 6] 96 | [7 8 9 3 4 5 6] 97 | [7 8 9 10 4 5 6] 98 | [7 8 9 10 11 5 6] 99 | [7 8 9 10 11 12 6] 100 | [7 8 9 10 11 12 13]] 101 | 102 | (range 0 (- (inc size)) -1) := '(0 -1 -2 -3 -4 -5 -6 -7) ; for reference 103 | (mapv #(index-ring size %) (range 0 (- (inc size)) -1)) 104 | := [[ 0 1 2 3 4 5 6] 105 | [ 0 1 2 3 4 5 -1] 106 | [ 0 1 2 3 4 -2 -1] 107 | [ 0 1 2 3 -3 -2 -1] 108 | [ 0 1 2 -4 -3 -2 -1] 109 | [ 0 1 -5 -4 -3 -2 -1] 110 | [ 0 -6 -5 -4 -3 -2 -1] 111 | [-7 -6 -5 -4 -3 -2 -1]])) 112 | 113 | (let [index-ring index-ring] ; FIXME without this let, below rcf test crashes at the repl 114 | (e/defn IndexRing [size offset] (e/diff-by {} (index-ring size offset)))) 115 | 116 | (tests 117 | (let [size 7 118 | !offset (atom 0)] 119 | (with ((l/single {} (e/Tap-diffs tap (IndexRing size (e/watch !offset)))) {} {}) 120 | % := {:degree 7, :permutation {}, :grow 7, :shrink 0, :change {0 0, 1 1, 2 2, 3 3, 4 4, 5 5, 6 6}, :freeze #{}} 121 | (swap! !offset inc) 122 | % := {:degree 7, :permutation {}, :grow 0, :shrink 0, :change {0 7}, :freeze #{}} 123 | (swap! !offset + 2) 124 | % := {:degree 7, :permutation {}, :grow 0, :shrink 0, :change {1 8, 2 9}, :freeze #{}} 125 | (reset! !offset size) ; check offset = size 126 | % := {:degree 7, :permutation {}, :grow 0, :shrink 0, :change {3 10, 4 11, 5 12, 6 13}, :freeze #{}} 127 | (swap! !offset inc) ; check offset > size 128 | % := {:degree 7, :permutation {}, :grow 0, :shrink 0, :change {0 14}, :freeze #{}} 129 | ;; negative offset, no use case for now but check for correctness 130 | (reset! !offset 0) ; reset to initial state 131 | % := {:degree 7, :permutation {}, :grow 0, :shrink 0, :change {0 0, 1 1, 2 2, 3 3, 4 4, 5 5, 6 6}, :freeze #{}} 132 | (swap! !offset dec) ; check offset < 0 133 | % := {:degree 7, :permutation {}, :grow 0, :shrink 0, :change {6 -1}, :freeze #{}} 134 | (reset! !offset -7) 135 | % := {:degree 7, :permutation {}, :grow 0, :shrink 0, :change {0 -7, 1 -6, 2 -5, 3 -4, 4 -3, 5 -2}, :freeze #{}} 136 | ))) 137 | 138 | ;; NOTE: output is weird when size isn't divisible by step 139 | ;; For now it's up to the user to provide correct values 140 | (let [ring (fn ring [size offset step] 141 | (let [start (- size offset)] 142 | (mapv #(+ offset (mod % size)) (range start (+ size start) step))))] 143 | (e/defn Ring [size offset step] 144 | (e/diff-by {} (ring size offset step)))) 145 | -------------------------------------------------------------------------------- /src/hyperfiddle/electric_svg3.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.electric-svg3 2 | "SVG support is experimental, API subject to change" 3 | (:refer-clojure :exclude [filter set symbol use]) 4 | (:require [hyperfiddle.electric-dom3 :as dom] 5 | [hyperfiddle.electric-dom3-props :as props]) 6 | #?(:cljs (:require-macros [hyperfiddle.electric-svg3]))) 7 | 8 | (defn element* [tag forms] (dom/element* props/SVG-NS tag forms)) 9 | 10 | (defmacro element [tag & body] (element* tag body)) 11 | 12 | ;;;;;;;;;;; 13 | ;; Sugar ;; 14 | ;;;;;;;;;;; 15 | 16 | (defmacro a [& body] (element* :a body)) 17 | (defmacro altGlyph [& body] (element* :altGlyph body)) 18 | (defmacro altGlyphDef [& body] (element* :altGlyphDef body)) 19 | (defmacro altGlyphItem [& body] (element* :altGlyphItem body)) 20 | (defmacro animate [& body] (element* :animate body)) 21 | (defmacro animateMotion [& body] (element* :animateMotion body)) 22 | (defmacro animateTransform [& body] (element* :animateTransform body)) 23 | (defmacro circle [& body] (element* :circle body)) 24 | (defmacro clipPath [& body] (element* :clipPath body)) 25 | (defmacro color-profile [& body] (element* :color-profile body)) 26 | (defmacro cursor [& body] (element* :cursor body)) 27 | (defmacro defs [& body] (element* :defs body)) 28 | (defmacro desc [& body] (element* :desc body)) 29 | (defmacro ellipse [& body] (element* :ellipse body)) 30 | (defmacro feBlend [& body] (element* :feBlend body)) 31 | (defmacro feColorMatrix [& body] (element* :feColorMatrix body)) 32 | (defmacro feComponentTransfer [& body] (element* :feComponentTransfer body)) 33 | (defmacro feComposite [& body] (element* :feComposite body)) 34 | (defmacro feConvolveMatrix [& body] (element* :feConvolveMatrix body)) 35 | (defmacro feDiffuseLighting [& body] (element* :feDiffuseLighting body)) 36 | (defmacro feDisplacementMap [& body] (element* :feDisplacementMap body)) 37 | (defmacro feDistantLight [& body] (element* :feDistantLight body)) 38 | (defmacro feFlood [& body] (element* :feFlood body)) 39 | (defmacro feFuncA [& body] (element* :feFuncA body)) 40 | (defmacro feFuncB [& body] (element* :feFuncB body)) 41 | (defmacro feFuncG [& body] (element* :feFuncG body)) 42 | (defmacro feFuncR [& body] (element* :feFuncR body)) 43 | (defmacro feGaussianBlur [& body] (element* :feGaussianBlur body)) 44 | (defmacro feImage [& body] (element* :feImage body)) 45 | (defmacro feMerge [& body] (element* :feMerge body)) 46 | (defmacro feMergeNode [& body] (element* :feMergeNode body)) 47 | (defmacro feMorphology [& body] (element* :feMorphology body)) 48 | (defmacro feOffset [& body] (element* :feOffset body)) 49 | (defmacro fePointLight [& body] (element* :fePointLight body)) 50 | (defmacro feSpecularLighting [& body] (element* :feSpecularLighting body)) 51 | (defmacro feSpotLight [& body] (element* :feSpotLight body)) 52 | (defmacro feTile [& body] (element* :feTile body)) 53 | (defmacro feTurbulence [& body] (element* :feTurbulence body)) 54 | (defmacro filter [& body] (element* :filter body)) 55 | (defmacro font [& body] (element* :font body)) 56 | (defmacro font-face [& body] (element* :font-face body)) 57 | (defmacro font-face-format [& body] (element* :font-face-format body)) 58 | (defmacro font-face-name [& body] (element* :font-face-name body)) 59 | (defmacro font-face-src [& body] (element* :font-face-src body)) 60 | (defmacro font-face-uri [& body] (element* :font-face-uri body)) 61 | (defmacro foreignObject [& body] (element* :foreignObject body)) 62 | (defmacro g [& body] (element* :g body)) 63 | (defmacro glyph [& body] (element* :glyph body)) 64 | (defmacro glyphRef [& body] (element* :glyphRef body)) 65 | (defmacro hkern [& body] (element* :hkern body)) 66 | (defmacro image [& body] (element* :image body)) 67 | (defmacro line [& body] (element* :line body)) 68 | (defmacro linearGradient [& body] (element* :linearGradient body)) 69 | (defmacro marker [& body] (element* :marker body)) 70 | (defmacro mask [& body] (element* :mask body)) 71 | (defmacro metadata [& body] (element* :metadata body)) 72 | (defmacro missing-glyph [& body] (element* :missing-glyph body)) 73 | (defmacro mpath [& body] (element* :mpath body)) 74 | (defmacro path [& body] (element* :path body)) 75 | (defmacro pattern [& body] (element* :pattern body)) 76 | (defmacro polygon [& body] (element* :polygon body)) 77 | (defmacro polyline [& body] (element* :polyline body)) 78 | (defmacro radialGradient [& body] (element* :radialGradient body)) 79 | (defmacro rect [& body] (element* :rect body)) 80 | (defmacro script [& body] (element* :script body)) 81 | (defmacro set [& body] (element* :set body)) 82 | (defmacro stop [& body] (element* :stop body)) 83 | (defmacro style [& body] (element* :style body)) 84 | (defmacro svg [& body] (element* :svg body)) 85 | (defmacro switch [& body] (element* :switch body)) 86 | (defmacro symbol [& body] (element* :symbol body)) 87 | (defmacro text [& body] (element* :text body)) 88 | (defmacro textPath [& body] (element* :textPath body)) 89 | (defmacro title [& body] (element* :title body)) 90 | (defmacro tref [& body] (element* :tref body)) 91 | (defmacro tspan [& body] (element* :tspan body)) 92 | (defmacro use [& body] (element* :use body)) 93 | (defmacro view [& body] (element* :view body)) 94 | (defmacro vkern [& body] (element* :vkern body)) 95 | 96 | 97 | -------------------------------------------------------------------------------- /src/hyperfiddle/electric_tokens.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.electric-tokens 2 | (:require [clojure.datafy :refer [datafy]] 3 | [hyperfiddle.electric3 :as e])) 4 | 5 | (defn- fn-name [f] 6 | (when (fn? f) 7 | #?(:cljs (.-name f) 8 | :clj (-> f class .getName ((requiring-resolve 'clojure.main/demunge)))))) 9 | 10 | (def ansi-color-no-color 0) 11 | (def ansi-color-256 {:ansi.color-256/red 196, :ansi.color-256/green 46, :ansi.color-256/orange 202}) ; use with "\033[38;5;m\033[0m" 12 | (def diff-color {::added :ansi.color-256/green, ::retracted :ansi.color-256/red, ::changed :ansi.color-256/orange}) 13 | 14 | (defn- colorize [color string] 15 | #?(:clj (str "\033[38;5;" (ansi-color-256 color ansi-color-no-color) "m" string "\033[0m") 16 | :cljs string #_(str "\u001B[38;5;" (ansi-color-256 color ansi-color-no-color) "m" string "\u001B[m"))) 17 | 18 | (defn- ansi-bold [string] #?(:clj (str "\033[1m" string "\033[0m") 19 | :cljs string #_(str "\u001B[1m" string "\u001B[m"))) 20 | 21 | (defn token-trace 22 | ([t] (token-trace 0 t)) 23 | ([level t] 24 | (if-not t 25 | "nil\n" 26 | (let [{:keys [::e/name ::e/children ::e/diff ::e/hash]} (datafy t)] 27 | (apply str (cond-> (or name (fn-name t)) (not (e/token? t)) (ansi-bold)) 28 | " " 29 | (colorize (diff-color diff) (some-> diff clojure.core/name not-empty)) 30 | (cond (= ::changed diff) (str " #" hash) 31 | (and t (not (e/token? t))) (str " #" (clojure.core/hash t))) 32 | "\n" 33 | (map (fn [child] 34 | (let [level (inc level)] 35 | (str (apply str (repeat (max 0 (dec level)) "│ ")) "├─ " 36 | (token-trace level child)))) 37 | children)))))) 38 | 39 | (defn- token-diff-1 [ta tb] 40 | (cond (= ta tb) nil ; account for nil 41 | (nil? ta) ::added 42 | (nil? tb) ::retracted 43 | () ::changed)) 44 | 45 | (defn- pad [v n coll] (concat coll (repeat n v))) 46 | 47 | (defn- pad-colls [coll-a coll-b] 48 | (let [ca (count coll-a) 49 | cb (count coll-b)] 50 | (cond (= ca cb) [coll-a coll-b] 51 | (< ca cb) [(pad nil (- cb ca) coll-a) coll-b] 52 | (> ca cb) [coll-a (pad nil (- ca cb) coll-b)]))) 53 | 54 | (defn token-diff [ta tb] 55 | (let [diff (token-diff-1 ta tb)] 56 | (case diff 57 | ::added (vary-meta tb assoc ::e/diff diff) 58 | ::retracted nil 59 | ::changed (-> (vary-meta tb update ::e/children #(apply map token-diff (pad-colls (::e/children (meta ta)) %))) 60 | (vary-meta assoc ::e/diff diff)) 61 | (if (e/token? ta) 62 | (-> (vary-meta ta update ::e/children #(apply map token-diff (pad-colls % (::e/children (meta tb))))) 63 | (vary-meta assoc ::e/diff diff)) 64 | ta)))) 65 | 66 | (comment 67 | (def left (e/->Token "left1" 68 | (e/->Token "left1.1") 69 | (e/->Token "left1.2"))) 70 | 71 | (def right-1 (e/->Token "right1.1" #(prn "hello"))) 72 | (def right-2 (e/->Token "right1.2")) 73 | 74 | (def right (e/->Token "right" right-1 right-2)) 75 | 76 | (def ta 77 | (e/->Token "root" 78 | left 79 | right)) 80 | 81 | (def tb 82 | (e/->Token "root" 83 | left 84 | (e/->Token "right" 85 | (e/->Token "right1.3") 86 | right-2) 87 | )) 88 | (datafy ta) 89 | 90 | (println (token-trace ta)) 91 | (println (token-trace tb)) 92 | 93 | (println (token-trace (token-diff ta ta))) 94 | (println (token-trace (token-diff ta tb))) 95 | 96 | ) 97 | -------------------------------------------------------------------------------- /src/hyperfiddle/incseq/arrays_impl.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.incseq.arrays-impl 2 | (:refer-clojure :exclude [int-array])) 3 | 4 | (defn int-array ^ints [n] 5 | #?(:clj (make-array Integer/TYPE n) 6 | :cljs (let [a (make-array n)] 7 | (dotimes [i n] (aset a i 0)) a))) 8 | 9 | (defn acopy [source source-offset target target-offset length] 10 | #?(:clj (System/arraycopy source source-offset target target-offset length) 11 | :cljs (dotimes [i length] 12 | (aset target (+ target-offset i) 13 | (aget source (+ source-offset i)))))) 14 | 15 | (defn aget-aset [^objects arr i x] 16 | (let [y (aget arr i)] 17 | (aset arr i x) y)) 18 | 19 | (defn weight-tree [size] 20 | (let [o (loop [o 1] 21 | (if (< o size) 22 | (recur (bit-shift-left o 1)) o)) 23 | n (bit-shift-left o 1) 24 | arr (int-array n)] 25 | (loop [f (unchecked-subtract o size) 26 | o o 27 | n n] 28 | (when (< 1 o) 29 | (loop [i (unchecked-subtract n f)] 30 | (when (< i n) 31 | (aset arr i 1) 32 | (recur (unchecked-inc i)))) 33 | (recur (bit-shift-right f 1) 34 | (bit-shift-right o 1) o))) arr)) -------------------------------------------------------------------------------- /src/hyperfiddle/incseq/diff_impl.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.incseq.diff-impl 2 | (:require [hyperfiddle.incseq.perm-impl :as p] 3 | [hyperfiddle.rcf :refer [tests]])) 4 | 5 | (defn empty-diff [n] 6 | {:degree n :grow 0 :shrink 0 :permutation {} :change {} :freeze #{}}) 7 | 8 | (defn empty-diff? [{:keys [grow shrink permutation change freeze]}] 9 | (-> (unchecked-add grow shrink) 10 | (unchecked-add (count permutation)) 11 | (unchecked-add (count change)) 12 | (unchecked-add (count freeze)) 13 | (zero?))) 14 | 15 | (def patch-vec 16 | (let [grow! (fn [v n] 17 | (reduce conj! v (repeat n nil))) 18 | shrink! (fn [v n] 19 | (loop [i 0, v v] 20 | (if (< i n) 21 | (recur (inc i) 22 | (pop! v)) v))) 23 | change! (fn [r c] 24 | (reduce-kv assoc! r c)) 25 | cycles! (partial p/decompose 26 | (fn [v c] 27 | (let [i (nth c 0) 28 | x (nth v i)] 29 | (loop [v v 30 | i i 31 | k 1] 32 | (let [j (nth c k) 33 | v (assoc! v i (nth v j)) 34 | k (unchecked-inc-int k)] 35 | (if (< k (count c)) 36 | (recur v j k) 37 | (assoc! v j x)))))))] 38 | (fn 39 | ([] []) 40 | ([v d] 41 | (-> v 42 | (transient) 43 | (grow! (:grow d)) 44 | (cycles! (:permutation d)) 45 | (shrink! (:shrink d)) 46 | (change! (:change d)) 47 | (persistent!)))))) 48 | 49 | (defn unlink [p i j] (let [k (get p j)] (cond-> (dissoc p i j) (not= i k) (assoc i k)))) 50 | 51 | (defn remove-shrunk-reorders [p i j] 52 | (reduce (fn [p k] (if-some [[_ v] (find p k)] (cond-> p (>= v i) (unlink k v)) p)) p (range i (inc j)))) 53 | 54 | (defn remove-change-reorders [p changeset] 55 | (-> (fn [p k] (if-some [[_ v] (find p k)] (if (changeset v) (recur (unlink p k v) k) p) p)) 56 | (reduce p changeset))) 57 | 58 | (defn combine 59 | ([x] x) 60 | ([x y] 61 | (let [px (:permutation x), py (:permutation y) 62 | dx (:degree x), dy (:degree y) 63 | cx (:change x), cy (:change y) 64 | fx (:freeze x), fy (:freeze y) 65 | degree (unchecked-add dy (:shrink x)) 66 | size-before (unchecked-subtract dx (:grow x)) 67 | size-between (unchecked-subtract dy (:grow y)) 68 | size-after (unchecked-subtract dy (:shrink y))] 69 | (loop [i size-after 70 | d degree 71 | p (p/compose py 72 | (p/split-swap size-between 73 | (unchecked-subtract degree dy) 74 | (unchecked-subtract degree dx)) px) 75 | c (reduce-kv assoc! 76 | (reduce-kv 77 | (fn [r i j] 78 | (if (contains? cx j) 79 | (assoc! r i (cx j)) r)) 80 | (reduce dissoc! (transient cx) 81 | (vals py)) py) cy) 82 | f (reduce conj! 83 | (reduce-kv 84 | (fn [r i j] 85 | (if (contains? fx j) 86 | (conj! r i) r)) 87 | (reduce disj! (transient fx) 88 | (vals py)) py) fy)] 89 | (if (< i d) 90 | (let [j (p i i)] 91 | (if (< j size-before) 92 | (recur (unchecked-inc i) d p (dissoc! c i) (disj! f i)) 93 | (recur i (unchecked-dec d) 94 | (p/compose (p/rotation i d) 95 | p (p/rotation d j)) (dissoc! c d) (disj! f d)))) 96 | (let [c (persistent! (dissoc! c d))] 97 | {:degree d 98 | :permutation (-> p (remove-shrunk-reorders size-after d) (remove-change-reorders (set (keys c)))) 99 | :grow (unchecked-subtract d size-before) 100 | :shrink (unchecked-subtract d size-after) 101 | :change c 102 | :freeze (persistent! (disj! f d))}))))) 103 | ([x y & zs] (reduce combine (combine x y) zs))) 104 | 105 | (defn subdiff [{:keys [grow shrink degree permutation change freeze]} size offset] 106 | (let [global-degree (unchecked-add-int size grow) 107 | shift (unchecked-subtract-int global-degree (unchecked-add-int degree offset)) 108 | +offset (partial + offset)] 109 | {:grow grow 110 | :shrink shrink 111 | :degree global-degree 112 | :permutation (p/compose 113 | (p/split-swap (unchecked-add-int offset (unchecked-subtract-int degree shrink)) shrink shift) 114 | (into {} (map (juxt (comp +offset key) (comp +offset val))) permutation) 115 | (p/split-swap (unchecked-add-int offset (unchecked-subtract-int degree grow)) shift grow)) 116 | :change (into {} (map (juxt (comp +offset key) val)) change) 117 | :freeze (into #{} (map +offset) freeze)})) 118 | 119 | (tests "sequence diffs" 120 | (patch-vec [:a :b :c] 121 | {:grow 1 122 | :degree 4 123 | :permutation (p/rotation 3 1) 124 | :shrink 2 125 | :change {1 :e}}) := 126 | [:a :e] 127 | (patch-vec [:a :e] 128 | {:grow 2 129 | :degree 4 130 | :permutation (p/rotation 1 3) 131 | :shrink 1 132 | :change {0 :f 1 :g 2 :h}}) := 133 | [:f :g :h] 134 | 135 | (patch-vec [:a :b :c] 136 | {:grow 1 137 | :degree 4 138 | :permutation {} 139 | :shrink 1 140 | :change {0 :f, 1 :g, 2 :h}}) := 141 | [:f :g :h] 142 | ) 143 | -------------------------------------------------------------------------------- /src/hyperfiddle/incseq/fixed_impl.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.incseq.fixed-impl 2 | (:require [hyperfiddle.incseq.arrays-impl :as a]) 3 | (:import #?(:clj (clojure.lang IFn IDeref)))) 4 | 5 | (def slot-notifier 0) 6 | (def slot-terminator 1) 7 | (def slot-processes 2) 8 | (def slot-ready 3) 9 | (def slot-push 4) 10 | (def slot-live 5) 11 | (def slot-value 6) 12 | (def slot-results 7) 13 | (def slots 8) 14 | 15 | (deftype EmptySeq [t] 16 | IFn 17 | (#?(:clj invoke :cljs -invoke) [_]) 18 | IDeref 19 | (#?(:clj deref :cljs -deref) [_] 20 | (t) {:grow 0 21 | :shrink 0 22 | :degree 0 23 | :permutation {} 24 | :change {} 25 | :freeze #{}})) 26 | 27 | (defn empty-seq [n t] 28 | (n) (->EmptySeq t)) 29 | 30 | (defn nop []) 31 | 32 | (defn input-ready [^objects state item] 33 | ((locking state 34 | (let [^objects processes (aget state slot-processes) 35 | ^ints ready (aget state slot-ready) 36 | arity (alength processes) 37 | item (int item)] 38 | (if-some [i (aget state slot-push)] 39 | (do (aset state slot-push (identity (rem (unchecked-inc-int i) arity))) 40 | (aset ready i item) nop) 41 | (do (aset state slot-push (identity (rem 1 arity))) 42 | (if-some [cb (aget state slot-notifier)] 43 | (do (aset ready 0 item) cb) 44 | (loop [item item 45 | i (rem 1 arity)] 46 | (if (neg? item) 47 | (aset state slot-live (dec (aget state slot-live))) 48 | (try @(aget processes item) (catch #?(:clj Throwable :cljs :default) _))) 49 | (let [item (aget ready i)] 50 | (if (== arity item) 51 | (do (aset state slot-push nil) 52 | (if (zero? (aget state slot-live)) 53 | (aget state slot-terminator) nop)) 54 | (do (aset ready i arity) 55 | (recur item (rem (unchecked-inc-int i) arity))))))))))))) 56 | 57 | (defn item-spawn [^objects state item flow] 58 | (let [^objects results (aget state slot-results) 59 | ^objects processes (aget state slot-processes) 60 | arity (alength processes)] 61 | (aset results item state) 62 | (aset processes item 63 | (flow #(input-ready state item) 64 | #(input-ready state (unchecked-subtract-int item arity))))) 65 | state) 66 | 67 | (defn cancel [^objects state] 68 | (let [^objects processes (aget state slot-processes)] 69 | (dotimes [item (alength processes)] ((aget processes item))))) 70 | 71 | (defn transfer [^objects state] 72 | (let [^objects results (aget state slot-results) 73 | ^objects processes (aget state slot-processes) 74 | ^ints ready (aget state slot-ready) 75 | arity (alength processes) 76 | item (aget ready 0)] 77 | (aset ready 0 arity) 78 | ((locking state 79 | (loop [item item 80 | i (rem 1 arity)] 81 | (if (nil? (aget state slot-notifier)) 82 | (if (neg? item) 83 | (aset state slot-live (dec (aget state slot-live))) 84 | (try @(aget processes item) (catch #?(:clj Throwable :cljs :default) _))) 85 | (let [diff (aget state slot-value)] 86 | (aset state slot-value 87 | (if (neg? item) 88 | (do (aset state slot-live (dec (aget state slot-live))) 89 | (update diff :freeze conj (unchecked-add-int arity item))) 90 | (try (let [r @(aget processes item)] 91 | (if (= (aget results item) (aset results item r)) 92 | diff (update diff :change assoc item r))) 93 | (catch #?(:clj Throwable :cljs :default) e 94 | (aset state slot-notifier nil) 95 | (cancel state) e)))))) 96 | (let [item (aget ready i)] 97 | (if (== arity item) 98 | (do (aset state slot-push nil) 99 | (if (zero? (aget state slot-live)) 100 | (aget state slot-terminator) nop)) 101 | (do (aset ready i arity) 102 | (recur item (rem (unchecked-inc-int i) arity)))))))) 103 | (let [x (aget state slot-value)] 104 | (aset state slot-value 105 | {:grow 0 106 | :shrink 0 107 | :degree arity 108 | :permutation {} 109 | :change {} 110 | :freeze #{}}) 111 | (if (nil? (aget state slot-notifier)) 112 | (throw x) x)))) 113 | 114 | (deftype Ps [state] 115 | IFn 116 | (#?(:clj invoke :cljs -invoke) [_] 117 | (cancel state)) 118 | IDeref 119 | (#?(:clj deref :cljs -deref) [_] 120 | (transfer state))) 121 | 122 | (defn flow 123 | ([] empty-seq) 124 | ([item & items] 125 | (let [items (into [item] items)] 126 | (fn [n t] 127 | (let [state (object-array slots) 128 | arity (count items) 129 | ready (a/int-array arity)] 130 | (dotimes [i arity] (aset ready i arity)) 131 | (aset state slot-notifier n) 132 | (aset state slot-terminator t) 133 | (aset state slot-processes (object-array arity)) 134 | (aset state slot-ready ready) 135 | (aset state slot-live (identity arity)) 136 | (aset state slot-value 137 | {:grow arity 138 | :degree arity 139 | :shrink 0 140 | :permutation {} 141 | :change {} 142 | :freeze #{}}) 143 | (aset state slot-results (object-array arity)) 144 | (reduce-kv item-spawn state items) 145 | (->Ps state)))))) -------------------------------------------------------------------------------- /src/hyperfiddle/incseq/flow_protocol_enforcer.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.incseq.flow-protocol-enforcer 2 | #?(:clj (:import [clojure.lang IDeref IFn])) 3 | #?(:cljs (:require-macros [hyperfiddle.incseq.flow-protocol-enforcer :refer [cannot-throw]]))) 4 | 5 | (defn %violated 6 | ([nm msg] (println nm "flow protocol violation:" msg) #?(:cljs (.error js/console) :clj (prn (Throwable.)))) 7 | ([nm msg e] 8 | (println nm "flow protocol violation:" msg) 9 | (#?(:clj prn :cljs js/console.error) e))) 10 | 11 | (defmacro cannot-throw [f nm violated] 12 | `(try (~f) (catch ~(if (:js-globals &env) :default 'Throwable) e# 13 | (~violated ~nm ~(str f " cannot throw") e#)))) 14 | 15 | (def diff? (every-pred :grow :degree :shrink :change :permutation :freeze)) 16 | 17 | (defn is-array? [v] #?(:clj (-> v class .isArray) :cljs (array? v))) 18 | (defn pretty [v] (cond-> v (is-array? v) vec)) 19 | 20 | (defn enforce 21 | ([flow] (enforce {} flow)) 22 | ([{nm :name, f :transfer, violated :on-violate :as o :or {violated %violated}} flow] 23 | (fn [step done] 24 | (let [!should-step? (atom ::init), !done? (atom false), !threw? (atom false) 25 | step (fn [] 26 | (when @!done? (violated nm "step after done")) 27 | (when @!threw? (violated nm "step after throw")) 28 | (if (first (swap-vals! !should-step? not)) (cannot-throw step nm violated) (violated nm "double step"))) 29 | done (fn [] (if (first (reset-vals! !done? true)) (violated nm "done called twice") (cannot-throw done nm violated))) 30 | cancel (try (flow step done) 31 | (catch #?(:clj Throwable :cljs :default) e (violated nm "flow process creation threw" e))) 32 | check-transfer (if f 33 | (fn [t v] 34 | (when (and (= :ok t) (not (f v))) 35 | (violated nm (str "transferred value doesn't satisfy " f ": " (pretty v))))) 36 | (fn [_ _]))] 37 | (when (and (:initialized o) (= ::init @!should-step?)) (violated nm "missing initial step")) 38 | (reify 39 | IFn (#?(:clj invoke :cljs -invoke) [_] (cannot-throw cancel nm violated)) 40 | IDeref (#?(:clj deref :cljs -deref) [_] 41 | (let [should-step (first (swap-vals! !should-step? not)) 42 | [t v] (try [:ok @cancel] (catch #?(:clj Throwable :cljs :default) e [:ex e]))] 43 | (check-transfer t v) 44 | (when (= :ex t) (reset! !threw? true)) 45 | (when should-step (violated nm "transfer without step" (when (= :ex t) v))) 46 | (when (and (not @!should-step?) (= :ex t)) (violated nm "step in exceptional transfer")) 47 | (if (= :ex t) (throw v) v)))))))) 48 | 49 | (defn incseq [nm flow] (enforce {:name nm, :initialized true, :transfer #'diff?} flow)) 50 | (defn initialized [nm flow] (enforce {:name nm, :initialized true} flow)) 51 | (defn uninitialized [nm flow] (enforce {:name nm} flow)) 52 | -------------------------------------------------------------------------------- /src/hyperfiddle/incseq/mount_impl.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.incseq.mount-impl 2 | (:require [hyperfiddle.incseq.perm-impl :as p])) 3 | 4 | (defn permute-keys [rf r p m] 5 | (reduce-kv (fn [r k v] (rf r (p k k) v)) r m)) 6 | 7 | (defn mount [append-child replace-child insert-before remove-child nth-child] 8 | (letfn [(append [element degree grow permutation change] 9 | (let [q (p/inverse permutation)] 10 | (loop [i (- degree grow) 11 | c change 12 | p permutation] 13 | (if (== i degree) 14 | (do (permute-keys replace element p c) 15 | (p/rotations rotate element p)) 16 | (let [j (q i i) 17 | e (c j)] 18 | (recur (inc i) (dissoc c j) 19 | (if (< j i) 20 | (do (insert-before element e (nth-child element j)) 21 | (p/compose p (p/rotation j i))) 22 | (do (append-child element e) p)))))))) 23 | (replace [element i e] 24 | (replace-child element e (nth-child element i))) 25 | (rotate [element i j] 26 | (insert-before element (nth-child element i) 27 | (nth-child element (if (< i j) (inc j) j))) 28 | element)] 29 | (fn [element {:keys [grow shrink degree permutation change]}] 30 | (let [size-after (- degree shrink)] 31 | (loop [d degree 32 | p permutation] 33 | (if (== d size-after) 34 | (append element d grow p change) 35 | (let [i (dec d) 36 | j (p i i)] 37 | (remove-child element (nth-child element j)) 38 | (recur i (p/compose p (p/rotation i j)))))))))) -------------------------------------------------------------------------------- /src/hyperfiddle/incseq/perm_impl.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.incseq.perm-impl 2 | (:refer-clojure :exclude [cycle]) 3 | (:require [clojure.set])) 4 | 5 | (def inverse clojure.set/map-invert) 6 | 7 | (defn transposition [i j] 8 | {i j, j i}) 9 | 10 | (defn cycle 11 | ([xs] 12 | (let [n (count xs) 13 | f (nth xs 0)] 14 | (loop [j f 15 | i 1 16 | p (transient {})] 17 | (let [k (nth xs i) 18 | i (unchecked-inc i) 19 | p (assoc! p j k)] 20 | (if (< i n) 21 | (recur k i p) 22 | (persistent! (assoc! p k f)))))))) 23 | 24 | (defn rotation [i j] 25 | (case (compare i j) 26 | -1 (cycle (range i (inc j) +1)) 27 | 0 {} 28 | +1 (cycle (range i (dec j) -1)))) 29 | 30 | (defn split-swap [i l r] 31 | (let [l (int l) 32 | r (int r)] 33 | (case l 34 | 0 {} 35 | (case r 36 | 0 {} 37 | (let [j (unchecked-add-int i l) 38 | k (unchecked-add-int j r)] 39 | (zipmap (range i k) 40 | (concat (range j k) 41 | (range i j)))))))) 42 | 43 | (defn arrange [v p] 44 | (persistent! 45 | (reduce-kv 46 | (fn [r i j] 47 | (assoc! r i (nth v j))) 48 | (transient v) p))) 49 | 50 | (defn decompose [rf r p] 51 | (loop [p p 52 | r r] 53 | (case p 54 | {} r 55 | (let [[i j] (first p)] 56 | (let [c (loop [c [i] 57 | j j] 58 | (let [c (conj c j) 59 | j (p j)] 60 | (if (== i j) 61 | c (recur c j)))) 62 | r (rf r c)] 63 | (if (reduced? r) 64 | @r (recur (apply dissoc p c) r))))))) 65 | 66 | (defn compose 67 | ([] {}) 68 | ([x] x) 69 | ([x y] 70 | (reduce-kv 71 | (fn [r i j] 72 | (let [k (y j j)] 73 | (if (== i k) 74 | (dissoc r i) 75 | (assoc r i k)))) 76 | y x)) 77 | ([x y & zs] 78 | (reduce compose (compose x y) zs))) 79 | 80 | (defn order [p] 81 | (loop [o 1, q p] 82 | (case q 83 | {} o 84 | (recur (unchecked-inc o) 85 | (compose p q))))) 86 | 87 | (defn involution? [p] 88 | (and (not= {} p) (= {} (compose p p)))) 89 | 90 | (defn transposition? [p] 91 | (= 2 (count p))) 92 | 93 | (defn recompose [cycles] 94 | (->> cycles 95 | (eduction (map cycle)) 96 | (reduce compose (compose)))) 97 | 98 | (defn split-long-swap [o l c r] 99 | (->> (range o (+ o (min l r))) 100 | (eduction (map (fn [i] (transposition i (+ l c i))))) 101 | (reduce compose {}) 102 | (compose 103 | (case (compare l r) 104 | -1 (split-swap (+ o l) (+ l c) (- r l)) 105 | 0 {} 106 | +1 (split-swap (+ o r) (- l r) (+ c r)))))) 107 | 108 | (defn cycle-transpositions [rf r cycle] 109 | (loop [r r 110 | i (unchecked-dec-int (count cycle))] 111 | (if (zero? i) 112 | r (let [x (cycle 0) 113 | y (cycle i) 114 | r (if (< x y) 115 | (rf r x y) 116 | (rf r y x))] 117 | (if (reduced? r) 118 | @r (recur r (unchecked-dec-int i))))))) 119 | 120 | (defn transpositions [rf r p] 121 | (decompose (partial cycle-transpositions rf) r p)) 122 | 123 | (defn transposition-rotations [rf r i j] 124 | (let [k (dec j) 125 | r (rf r i j)] 126 | (if (reduced? r) 127 | @r (if (== k i) 128 | r (unreduced 129 | (rf r k i)))))) 130 | 131 | ;; TODO generate optimal sequence if possible 132 | (defn rotations [rf r p] 133 | (decompose 134 | (->> rf 135 | (partial transposition-rotations) 136 | (partial cycle-transpositions)) 137 | r p)) -------------------------------------------------------------------------------- /src/hyperfiddle/input_zoo0.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.input-zoo0 2 | (:require clojure.string 3 | [hyperfiddle.electric3 :as e] 4 | [hyperfiddle.electric-dom3 :as dom])) 5 | 6 | ;; Crude uncontrolled inputs, rarely useful 7 | 8 | (e/defn Input* [& {:keys [maxlength type parse] :as props 9 | :or {maxlength 100 type "text" parse identity}}] 10 | (e/client ; explicit site on all controls for compat with neutral callers 11 | (dom/input (dom/props (-> props (dissoc :parse) 12 | (assoc :maxLength maxlength :type type))) 13 | (dom/On "input" #(-> % .-target .-value (subs 0 maxlength) parse) "")))) 14 | 15 | (e/defn Checkbox* [& {:keys [id label parse] :as props 16 | :or {id (random-uuid) parse identity}}] 17 | (e/client 18 | (e/amb 19 | (dom/input (dom/props {:type "checkbox", :id id}) 20 | (dom/props (dissoc props :id :label :parse)) 21 | (dom/On "change" #(-> % .-target .-checked parse) false)) 22 | (e/When label (dom/label (dom/props {:for id}) (dom/text label)))))) 23 | 24 | (e/defn InputSubmitCreate! 25 | "optimistic, cancel & retry are forwarded to optimistic list item's InputSubmit! 26 | buffers (dirty), commit, discard bundled as enter/esc" 27 | [& {:keys [maxlength type parse] :as props 28 | :or {maxlength 100 type "text" parse identity}}] 29 | (e/client 30 | (dom/input (dom/props (-> props (dissoc :parse) (assoc :maxLength maxlength :type type))) 31 | (letfn [(read! [node] (not-empty (subs (.-value node) 0 maxlength))) 32 | (read-clear! [node] (when-some [v (read! node)] (set! (.-value node) "") v)) 33 | (submit! [e] (let [k (.-key e)] 34 | (cond 35 | (= "Enter" k) (parse (read-clear! (.-target e))) 36 | (= "Escape" k) (do (set! (.-value dom/node) "") nil) 37 | () nil)))] 38 | #_(PendingMonitor) ; the optimistic list item is responsible for pending/retry affordances 39 | (dom/On-all "keydown" submit!))))) 40 | 41 | (e/defn InputSubmitCreate?! 42 | "transactional chat input with busy state. Supports rapid submit, sending 43 | concurrent in-flight submits to the server which race. ?! marks this control 44 | as an anti-pattern because it has no error handling: rejected edits are silently 45 | lost. Fixing this requires form semantics, see upcoming tutorial." 46 | [& {:keys [maxlength type parse] :as props 47 | :or {maxlength 100 type "text" parse identity}}] 48 | (e/client 49 | (dom/input (dom/props (-> props (dissoc :parse) (assoc :maxLength maxlength :type type))) 50 | (letfn [(read! [node] (not-empty (subs (.-value node) 0 maxlength))) 51 | (read-clear! [node] (when-some [v (read! node)] (set! (.-value node) "") v)) 52 | (submit! [e] (let [k (.-key e)] 53 | (cond 54 | (= "Enter" k) (parse (read-clear! (.-target e))) 55 | (= "Escape" k) (do (set! (.-value dom/node) "") nil) 56 | () nil)))] 57 | (let [edits (dom/On-all "keydown" submit!)] ; concurrent pending submits 58 | (dom/props {:aria-busy (pos? (e/Count edits))}) 59 | edits))))) -------------------------------------------------------------------------------- /src/hyperfiddle/kvs.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.kvs) 2 | 3 | (defprotocol KVS 4 | (insert! [_ k v]) 5 | (update! [_ k f]) 6 | (remove! [_ k])) -------------------------------------------------------------------------------- /src/hyperfiddle/token_zoo0.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.token-zoo0 2 | ; mirror bootstrap structure of electric3 for copy/paste 3 | (:require [hyperfiddle.electric3 :as e])) 4 | 5 | (let [->token (fn [!t] 6 | (fn token 7 | ([] (token nil)) 8 | ([ret] (reset! !t nil) ret))) 9 | step (fn [!t v on?] (when (on? v) (compare-and-set! !t nil (->token !t))))] 10 | (hyperfiddle.electric3/defn TokenNofail 11 | ([v] (TokenNofail v some?)) 12 | ([v on?] (let [!t (atom nil)] 13 | (step !t v on?) 14 | (e/watch !t))))) 15 | 16 | (let [->spend-fn (fn [!spend!] (fn f ([] (f nil)) ([ret] (reset! !spend! nil) ret))) 17 | step (fn [!spend! _spend! v on?] (when (on? v) (compare-and-set! !spend! nil (->spend-fn !spend!))))] 18 | (hyperfiddle.electric3/defn CyclicToken ; todo retry 19 | ([v] (CyclicToken v some?)) 20 | ([v on?] (let [!spend! (atom nil), spend! (e/watch !spend!)] (step !spend! spend! v on?) spend!)))) 21 | 22 | (let [->spend-fn (fn [!held] (fn f ([] (f nil)) ([ret] (swap! !held assoc 1 nil) ret))) 23 | step (fn [!held v on?] 24 | (let [[_ spend! :as held] @!held] 25 | (when (and (not spend!) (on? v)) 26 | (compare-and-set! !held held [v (->spend-fn !held)]))))] 27 | (hyperfiddle.electric3/defn StampedToken ; todo retry 28 | ([v] (StampedToken v some?)) 29 | ([v on?] (let [!held (atom [nil nil])] (step !held v on?) (e/watch !held))))) 30 | 31 | (let [->spend-fn (fn [!held] (fn f ([] (f nil)) ([ret] (swap! !held assoc 1 nil) ret))) 32 | step (fn [!held _held v on?] 33 | (let [[_ next! :as held] @!held] 34 | (when (and (not next!) (on? v)) 35 | (compare-and-set! !held held [v (->spend-fn !held)]))))] 36 | (hyperfiddle.electric3/defn StampedCyclicToken ; todo retry 37 | ([v] (StampedCyclicToken v some?)) 38 | ([v on?] (let [!held (atom [nil nil]), held (e/watch !held)] (step !held held v on?) held)))) 39 | 40 | (letfn [(->unlatch-fn [!latched?] (fn f ([] (f nil)) ([v] (reset! !latched? false) v))) 41 | (->latch-fn [!latched? unlatch!] (fn f ([] (reset! !latched? unlatch!)) ([_] (f))))] 42 | (hyperfiddle.electric3/defn Latchable [v] 43 | (let [!latched? (atom false), unlatch! (->unlatch-fn !latched?)] 44 | [(if (e/watch !latched?) (e/Snapshot v) v) (->latch-fn !latched? unlatch!)]))) 45 | 46 | (let [->spend-fn (fn [t !d] (when t (fn t2 ([] (t2 nil)) ([d] (reset! !d d) (t d)))))] 47 | (e/defn WithDataSlot 48 | ([t] (WithDataSlot t nil)) 49 | ([t init-v] 50 | (let [!d (atom init-v)] 51 | [(->spend-fn t !d) (e/watch !d)])))) 52 | -------------------------------------------------------------------------------- /test/cljs/analyzer_testing_auto_alias.cljc: -------------------------------------------------------------------------------- 1 | (ns cljs.analyzer-testing-auto-alias) 2 | 3 | (defmacro auto-aliased [x] `(def ~x)) 4 | -------------------------------------------------------------------------------- /test/contrib/missionary_contrib_test.edn: -------------------------------------------------------------------------------- 1 | ;; FIXME revisit the entire use case/design 2 | (ns contrib.missionary-contrib-test 3 | (:require [clojure.core.async :as a] 4 | [contrib.missionary-core-async :as mc] 5 | [hyperfiddle.electric :as p] 6 | [hyperfiddle.electric-local-def :as l] 7 | [hyperfiddle.rcf :as rcf :refer [tests tap % with]] 8 | [missionary.core :as m]) 9 | (:import missionary.Cancelled)) 10 | 11 | ;; TODO port to standard lib 12 | (defn chan-write 13 | "Return a task writing `val` onto `chan`. Produces true if 14 | val was succesfully written, false if chan was closed." 15 | [chan val] 16 | (fn [success failure] 17 | (let [cancel-chan (a/chan)] 18 | (a/go (let [[v port] (a/alts! [[chan val] cancel-chan])] 19 | (if (= port cancel-chan) 20 | (failure (Cancelled.)) 21 | (success v)))) 22 | #(a/close! cancel-chan)))) 23 | 24 | ;; TODO port to standard lib 25 | (defn onto-chan 26 | "Transfer values from flow to chan. Produces a flow of values that couldn't 27 | be transfered onto chan." 28 | [flow chan] 29 | (m/ap (let [val (m/?> flow)] ; for each successive values of `flow` 30 | (if-not (m/? (chan-write chan val)) ; try to write it onto chan 31 | val ; if write failed, emit val 32 | (m/amb) ; emit nothing 33 | )))) 34 | 35 | ;; These tests are CLJ only. These tests could be made to pass in JS but added 36 | ;; value is low as constraints are external to the domain. m/? is not defined 37 | ;; outside of sp/ap/cp in JS and we rely on future and Thread/sleep. 38 | #?(:clj 39 | (tests 40 | "Read a value from a channel" 41 | (let [c (a/chan) 42 | t (mc/chan-read! c)] 43 | (a/put! c 1) 44 | (m/? t) := 1))) 45 | 46 | #?(:clj 47 | (tests 48 | "Reading a value from a channel blocks until a value is available." 49 | (let [c (a/chan) 50 | t (mc/chan-read! c)] 51 | (future (tap (m/? t))) ; don't block main (repl) thread 52 | (a/put! c 1) 53 | % := 1))) 54 | 55 | #?(:clj 56 | (tests 57 | "Write a value to a channel" 58 | (let [c (a/chan) 59 | t (chan-write c 1)] 60 | (a/take! c tap) 61 | (m/? t) := true 62 | % := 1))) 63 | 64 | #?(:clj 65 | (tests 66 | "Writing a value to a channel blocks if the channel is full." 67 | (let [c (a/chan) 68 | t (chan-write c 2)] 69 | (a/put! c 1) := true 70 | (Thread/sleep 100) 71 | (future (m/? t) := true) 72 | (Thread/sleep 100) 73 | (a/take! c tap) 74 | (a/take! c tap) 75 | % := 1 76 | % := 2))) 77 | 78 | #?(:clj 79 | (tests 80 | "Turn a channel into a discrete flow" 81 | (let [c (a/chan) 82 | f (mc/chan->ap c) 83 | it (f #(tap :ready) #(tap :done))] 84 | (a/put! c 1) 85 | ;; chan-read rely on a go block, which will run its body in another thread. 86 | ;; We can not assume flow is immediately ready. Hence we await for :ready with % 87 | % := :ready 88 | @it := 1))) 89 | 90 | #?(:clj 91 | (tests 92 | "Put values of a flow onto a channel, and read it back as a flow." 93 | (def c (a/chan)) 94 | (future (tap (m/? (m/reduce conj ; just run the flow until it terminates 95 | (m/ap (m/amb= (m/?> (onto-chan (m/seed [1 2]) c)) 96 | (m/?> (mc/chan->ap c)))))))) 97 | (a/close! c) 98 | % := [1 2])) 99 | 100 | #?(:clj 101 | (tests 102 | "When transferring values from a discrete flow to a channel, values are not lost if the channel is closed." 103 | (def input (a/chan)) 104 | (def c (a/chan)) 105 | (future (m/? (m/reduce {} nil ; just run the flow until it terminates 106 | (m/ap (m/amb= (tap [:success (m/?> (mc/chan->ap c))]) 107 | (tap [:failure (m/?> (onto-chan (mc/chan->ap input) c))])))))) 108 | (a/>!! input 1) ; put 1 on input, which transfers to a flow, then is put onto c. 109 | (Thread/sleep 100) ; even if >!! is blocking, go blocks might race with the next instruction 110 | (a/close! c) 111 | (a/>!! input 2) ; put 2 on input, which will be put onto c, but c is closed. 112 | % := [:success 1] ; c got 1 113 | % := [:failure 2] ; onto-chan failed to write 2 on c and returned it. 114 | )) 115 | 116 | #?(:clj 117 | (tests 118 | "Using a core.async channel from Electric" 119 | (def c (a/to-chan [1 2 3])) 120 | (with (l/run (tap (mc/use-channel c))) 121 | % := nil 122 | % := 1 123 | % := 2 124 | % := 3))) 125 | 126 | ;; TODO WIP 127 | (comment 128 | #?(:clj 129 | (tests 130 | "Putting values on a channel from Electric" 131 | (def !a (atom 0)) 132 | (def c (a/chan)) 133 | (with (l/run (tap (new (onto-chan (p/fn [] (p/watch !a)) c)))) 134 | (a/go-loop [x (a/ (ts/->ts) (ts/add {:db/id 1, :foo 2}) (ts/->node 1) :foo) := 2 7 | (-> (ts/->ts) (ts/add {:db/id 1, :foo 1}) (ts/add {:db/id 2, :foo 1}) :ave :foo (get 1)) := #{1 2} 8 | ;; (-> (ts/->ts) (ts/add {:db/id 1, :foo 2, :bar 2}) :vea (get 2) (get 1)) := #{:foo :bar} 9 | (-> (ts/->ts) (ts/add {:db/id 1, :foo 2, :bar 2}) (ts/->node 1) (select-keys [:foo :bar :baz])) := {:foo 2, :bar 2} 10 | 11 | (-> (ts/->ts) (ts/add {:db/id '_}) (ts/upd '_ :x (fnil inc 0)) (ts/upd '_ :x (fnil inc 0)) (ts/->node '_) :x) := 2 12 | 13 | (-> (ts/->ts) (ts/add {:db/id 1}) (ts/asc 1 :x 2) (ts/asc 1 :x 2) :ave :x (get 2)) := #{1} 14 | (-> (ts/->ts) (ts/add {:db/id 1}) (ts/asc 1 :x 2 :y 3) :eav (get 1)) := {:db/id 1, :x 2, :y 3} 15 | 16 | (-> (ts/->ts) (ts/add {:db/id 1, :foo 1, :bar 1}) (ts/add {:db/id 2, :foo 1, :bar 1}) (ts/find :foo 1 :bar 1)) := #{1 2} 17 | 18 | (-> (ts/->ts) (ts/add {:db/id 1, :foo 1}) (ts/asc 1 :foo 2) :ave :foo) := {2 #{1}} 19 | 20 | (let [ts (-> (ts/->ts) (ts/add {:db/id 1, :foo 1}) (ts/add {:db/id 2, :foo 2}))] 21 | (count (->> ts :ave :foo vals (reduce into))) := 2 22 | (let [ts (ts/del ts 2)] 23 | (ts/->node ts 2) := nil 24 | (count (->> ts :ave :foo vals (reduce into))) := 1 25 | ))) 26 | -------------------------------------------------------------------------------- /test/hyperfiddle/browser_test_setup.clj: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.browser-test-setup 2 | (:require [cljs.analyzer :as ana])) 3 | 4 | ;; makes sure CI fails when there are undeclared var warnings 5 | ;; useful to test goog requires 6 | (defn blow-up-tests-on-warnings {:shadow.build/stage :compile-prepare} [build-state] 7 | (defmethod ana/error-message :undeclared-var [_warning-type info] 8 | (throw (ex-info (str "undeclared var: " (:prefix info) "/" (:suffix info)) info))) 9 | build-state) 10 | -------------------------------------------------------------------------------- /test/hyperfiddle/detest/incseq_test.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.detest.incseq-test 2 | (:import #?(:clj [clojure.lang IFn IDeref]) 3 | [missionary Cancelled]) 4 | (:require [hyperfiddle.detest :as dt] 5 | [hyperfiddle.incseq :as i] 6 | [hyperfiddle.incseq.diff-impl :as d] 7 | [hyperfiddle.incseq.perm-impl :as p] 8 | [clojure.set :as set] 9 | [contrib.assert :as ca] 10 | [contrib.debug :as dbg] 11 | [contrib.data :refer [->box]] 12 | [clojure.test :as t] 13 | [missionary.core :as m] 14 | [hyperfiddle.incseq.flow-protocol-enforcer :as fpe])) 15 | 16 | (defn pick [ngn v] 17 | (if (seq v) 18 | (let [i (dt/roll ngn (count v))] 19 | [(nth v i) (reduce-kv (fn [ac j x] (if (= i j) ac (conj ac x))) [] v)]) 20 | v)) 21 | 22 | (defn %next-diff [prev-size ngn next-fn] 23 | (let [grow (dt/roll ngn 10), degree (+ prev-size grow) 24 | shrink (dt/roll ngn prev-size), size (- degree shrink) 25 | to-rot (vec (range (max size prev-size) degree)) 26 | stay-space (vec (range #_size (min size prev-size))) 27 | perm-vec (loop [to-rot to-rot, stay stay-space, ret []] 28 | (if (seq to-rot) 29 | (let [[i stay] (pick ngn stay), [j to-rot] (pick ngn to-rot)] 30 | (recur to-rot stay (conj ret i j))) 31 | ret)) 32 | ;; _ (prn 'perm-vec perm-vec) 33 | perm (if (seq perm-vec) (p/recompose #{perm-vec}) {}) 34 | ;; perm (p/rotation (dt/roll ngn degree) (dt/roll ngn degree)) 35 | inv (set/map-invert perm) 36 | ;; _ (prn 'inv inv) 37 | change (reduce (fn [m i] #_(prn 'i i (inv i i)) (let [i (inv i i)] (cond-> m (< i size) (assoc i (next-fn ngn))))) 38 | {} (range prev-size (+ prev-size grow))) 39 | ;; _ (prn 'change-grown change) 40 | change (reduce (fn [m i] (cond-> m (zero? (dt/roll ngn 2)) (assoc i (next-fn ngn)))) 41 | change (range size))] 42 | {:grow grow :degree degree :shrink shrink :permutation perm :change change :freeze #{}})) 43 | 44 | (defn %rand-incseq [ngn next-incseq-fn] 45 | (cond->> (fn [step done] 46 | (step) 47 | (let [!should-step? (atom false), !v (atom (d/empty-diff 0)), !done? (atom false), !cancelled? (atom false) 48 | ;; !dbg (atom []) 49 | fin #(when-not (first (reset-vals! !done? true)) (done)) 50 | proc 51 | (reify 52 | IFn 53 | (#?(:clj invoke :cljs -invoke) [_] 54 | (let [cancelled? (first (reset-vals! !cancelled? true))] 55 | (when (and @!should-step? (not @!done?) (not cancelled?)) 56 | (swap! !should-step? not) (step)))) 57 | (#?(:clj invoke :cljs -invoke) [this n] 58 | (if @!done? 59 | (dt/del-proc ngn this) 60 | (if (> 1 (mod n 100)) 61 | (when @!should-step? (fin)) 62 | (when (and @!should-step? (not @!done?)) (swap! !should-step? not) (step))))) 63 | IDeref 64 | (#?(:clj deref :cljs -deref) [_] 65 | (cond 66 | @!done? 67 | (throw (ex-info "transfer after done" {})) 68 | 69 | @!cancelled? 70 | (do (fin) (throw (Cancelled.))) 71 | 72 | @!should-step? 73 | (throw (ex-info "transfer without step" {})) 74 | 75 | :else 76 | (do (swap! !should-step? not) 77 | (if (> 1 (dt/roll ngn 100)) 78 | (do (fin) (throw (ex-info "[DETEST OK] random incseq throw" {}))) 79 | (do (condp > (dt/roll ngn 100) 80 | 1 (fin) 81 | 25 (do (swap! !should-step? not) (step)) 82 | #_else nil) 83 | (swap! !v next-incseq-fn ngn) 84 | #_(let [nx (swap! !v next-incseq-fn ngn)] 85 | (prn 'state (swap! !dbg i/patch-vec nx)) 86 | nx)))))))] 87 | (dt/add-proc ngn proc) 88 | proc)) 89 | (dt/debug? ngn) (dt/instrument 'rand-incseq ngn))) 90 | 91 | (defn next-diff [prev-diff ngn] 92 | (%next-diff (- (:degree prev-diff) (:shrink prev-diff)) ngn dt/roll)) 93 | 94 | (defn rand-incseq [ngn] (%rand-incseq ngn next-diff)) 95 | 96 | (defn next-lc-diff [prev-diff ngn] 97 | (%next-diff (- (:degree prev-diff) (:shrink prev-diff)) ngn rand-incseq)) 98 | 99 | (defn rand-lc-incseq [ngn] (%rand-incseq ngn next-lc-diff)) 100 | 101 | (t/deftest detest-latest-product 102 | (dotimes [_ 20] 103 | (let [ngn (dt/->engine)] 104 | (t/is (nil? 105 | (dt/exercise ngn (i/latest-product vector (rand-incseq ngn) (rand-incseq ngn)))))))) 106 | 107 | (t/deftest detest-latest-concat 108 | (dotimes [_ 100] 109 | (let [ngn (dt/->engine)] 110 | (t/is (nil? 111 | (dt/exercise ngn (i/latest-concat (rand-lc-incseq ngn)))))))) 112 | 113 | (comment 114 | ;; how to repro and debug print 115 | (let [ngn (dt/->engine {:seed -6858028806708848032, :debug :full})] 116 | (dt/exercise ngn (i/latest-concat (rand-lc-incseq ngn)))) 117 | ;; how to minimize when a flow test fails often 118 | (let [ (->box)] 119 | (dotimes [_ 1000] 120 | (let [ngn (dt/->engine {:debug :steps})] 121 | (dt/minimize ngn (i/latest-concat (rand-lc-incseq ngn))))) 122 | ()) 123 | ) 124 | 125 | (t/deftest detest-fixed 126 | (dotimes [_ 500] 127 | (let [ngn (dt/->engine)] 128 | (t/is (nil? 129 | (dt/exercise ngn (i/fixed (rand-incseq ngn) (rand-incseq ngn) (rand-incseq ngn) 130 | (rand-incseq ngn) (rand-incseq ngn) (rand-incseq ngn) 131 | (rand-incseq ngn) (rand-incseq ngn) (rand-incseq ngn)))))))) 132 | 133 | (t/deftest detest-diff-by 134 | (dotimes [_ 1000] 135 | (let [ngn (dt/->engine)] 136 | (t/is (nil? 137 | (dt/exercise ngn (m/reductions i/patch-vec [] (rand-incseq ngn)))))))) 138 | 139 | (t/deftest detest-items 140 | (dotimes [_ 500] 141 | (let [ngn (dt/->engine)] 142 | (t/is (nil? 143 | (dt/exercise ngn (i/items (rand-incseq ngn)))))))) 144 | -------------------------------------------------------------------------------- /test/hyperfiddle/electric/impl/cljs_analyzer2_test.clj: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.electric.impl.cljs-analyzer2-test 2 | (:require [clojure.test :as t] 3 | [cljs.env] 4 | [cljs.analyzer] 5 | [hyperfiddle.electric.impl.cljs-analyzer2 :as ana])) 6 | 7 | (comment 8 | (time (let [!a (atom {})] (ana/analyze-nsT !a {} 'cljs.core))) 9 | (-> @!a ::ana/nses (get 'cljs.core) ::ana/defs count) 10 | ) 11 | 12 | (t/deftest ns-expansion 13 | (let [ns$ 'hyperfiddle.electric.impl.cljs-file-to-analyze 14 | !a (ana/->!a) 15 | _ (ana/analyze-nsT !a {} ns$) 16 | a @!a] 17 | (t/is (nil? (ana/find-var a 'non ns$))) 18 | (t/is (nil? (ana/find-var a 'first ns$))) 19 | (t/is (= 'cljs.core/next (::ana/name (ana/find-var a 'nxt ns$)))) 20 | (t/are [x] (some? (ana/find-var a x ns$)) 21 | 'foo 22 | 'bar 23 | 'baz 24 | 'an-fn 25 | 'behind-require 26 | 'str 27 | 'behind-alias 28 | 'behind-require-macros 29 | 'behind-require-macro-alias 30 | 'behind-required-refer 31 | 'behind-required-rename 32 | 'behind-require-macro-refer 33 | 'behind-require-macro-rename 34 | 'behind-include-macros 35 | 'behind-refer-macros 36 | 'behind-use 37 | 'behind-use-renamed 38 | 'behind-use-macro 39 | 'behind-use-macro-renamed 40 | 'behind-auto-alias 41 | 'behind-auto-alias-alias 42 | 'behind-auto-alias-refer 43 | 'nxt))) 44 | 45 | (t/deftest runtime-vars 46 | (let [ns$ 'hyperfiddle.electric.impl.cljs-file-to-analyze, 47 | !a (ana/->!a) 48 | _ (ana/analyze-nsT !a {} ns$) 49 | a @!a] 50 | (t/are [x] (nil? (ana/find-var a x ns$)) 51 | 'hyperfiddle.electric.impl.cljs-file-to-analyze.runtime/only-macro 52 | 'run/only-macro 53 | 'only-macro 54 | 'next) ; renamed in :refer-clojure 55 | (t/are [x] (some? (ana/find-var a x ns$)) 56 | 'hyperfiddle.electric.impl.cljs-file-to-analyze.runtime/macro-and-runtime 57 | 'run/macro-and-runtime 58 | 'macro-and-runtime 59 | 'hyperfiddle.electric.impl.cljs-file-to-analyze.runtime/only-runtime 60 | 'run/only-runtime 61 | 'only-runtime))) 62 | 63 | (t/deftest local-shadowing 64 | (let [ns$ 'hyperfiddle.electric.impl.cljs-file-to-analyze 65 | !a (ana/->!a) 66 | _ (ana/analyze-nsT !a {} ns$) 67 | a @!a] 68 | (t/are [x] (nil? (ana/find-var a x ns$)) 69 | 'shadowed-by-let 70 | 'shadowed-by-let-destructure 71 | 'shadowed-by-fn 72 | 'shadowed-by-fn-destructure 73 | 'shadowed-by-letfn-fn-name 74 | 'shadowed-by-letfn-other-fn-name 75 | 'shadowed-by-letfn-local))) 76 | 77 | (t/deftest defs-match-official-cljs-analyzer 78 | (let [ns$ 'cljs.analyzer 79 | !a (ana/->!a) 80 | _ (ana/analyze-nsT !a {} ns$) 81 | a @!a 82 | c (cljs.env/ensure 83 | (cljs.analyzer/analyze-file "cljs/core.cljs") 84 | (cljs.analyzer/analyze-file "cljs/analyzer.cljc") 85 | @cljs.env/*compiler*)] 86 | (t/are [ns$] (= (into #{} (keep (fn [[k v]] (when-not (:anonymous v) k))) 87 | (-> c :cljs.analyzer/namespaces (get ns$) :defs)) 88 | (set (-> a ::ana/nses (get ns$) ::ana/defs keys))) 89 | 'cljs.core 90 | 'cljs.analyzer))) 91 | 92 | (t/deftest clojure-core-var-found-as-cljs-core-var 93 | (let [ns$ 'cljs.analyzer 94 | !a (ana/->!a) 95 | _ (ana/analyze-nsT !a {} ns$) 96 | a @!a] 97 | (t/is (some? (ana/find-var a 'clojure.core/vector ns$))))) 98 | 99 | (t/deftest non-required-var-can-be-found ; e.g. a macro from another ns might have expanded to it 100 | (let [ns$ 'cljs.source-map 101 | !a (ana/->!a) 102 | _ (ana/analyze-nsT !a {} ns$) 103 | a @!a] 104 | (t/is (some? (ana/find-var a 'cljs.source-map/encode 'cljs.core))))) 105 | 106 | (t/deftest npm-shadow-extension 107 | (let [ns$ 'hyperfiddle.electric.impl.cljs-file-to-analyze 108 | !a (ana/->!a) 109 | _ (ana/analyze-nsT !a {} ns$) 110 | a @!a] 111 | (t/is (boolean (ana/js-call? a 'jslib/foo ns$))) 112 | (t/is (boolean (ana/js-call? a 'js/alert ns$))) 113 | (t/is (boolean (ana/js-call? a 'js-referred ns$))) 114 | (t/is (boolean (ana/js-call? a 'js-renamed ns$))) 115 | (t/is (not (ana/js-call? a 'not-js-referred ns$))) 116 | (t/is (not (ana/js-call? a 'run/only-runtime ns$))))) 117 | 118 | (t/deftest imports 119 | (let [a (ana/add-import (atom {}) 'foo '[Foo [java.util X Y]])] 120 | (t/is (= '{::ana/nses {foo {::ana/imports #{java.util.X X Foo java.util.Y Y}}}} a)) 121 | (t/is (some? (ana/imported? a 'X.XXX 'foo))) 122 | (t/is (some? (ana/imported? a 'java.util.X.XXX 'foo))) 123 | (t/is (some? (ana/imported? a 'Foo.Bar 'foo))))) 124 | -------------------------------------------------------------------------------- /test/hyperfiddle/electric/impl/cljs_file_to_analyze.cljs: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.electric.impl.cljs-file-to-analyze 2 | "docstring" {:attr :map} 3 | (:require 4 | [hyperfiddle.electric.impl.cljs-file-to-analyze.require :as req :refer [refdef renameme] :rename {renameme renamed}] 5 | [hyperfiddle.electric.impl.cljs-file-to-analyze.include :as inc :include-macros true] 6 | [hyperfiddle.electric.impl.cljs-file-to-analyze.refer-macros :refer-macros [refmac]] 7 | [hyperfiddle.electric.impl.cljs-file-to-analyze.runtime :as run :refer [only-macro only-runtime macro-and-runtime]] 8 | [clojure.analyzer-testing-auto-alias :as auto-alias :refer [auto-aliased]] 9 | ["some-js-lib" :as jslib :refer [js-referred js-to-rename] :rename {js-to-rename js-renamed}]) 10 | (:require-macros [hyperfiddle.electric.impl.cljs-file-to-analyze.macro-ns :as reqmac :refer [reqmacrefer reqmacrename] :rename {reqmacrename reqmacrenamed}]) 11 | (:use [hyperfiddle.electric.impl.cljs-file-to-analyze.use :only [useme renameme] :rename {renameme use-renamed}]) 12 | (:use-macros [hyperfiddle.electric.impl.cljs-file-to-analyze.use-macros :only [useme-mac renameme-mac] :rename {renameme-mac use-renamed-mac}]) 13 | (:refer-clojure :exclude [first] :rename {next nxt})) 14 | 15 | (def foo 1) 16 | 17 | (do (def bar 2) (def baz 3)) 18 | 19 | (do (defn an-fn [])) 20 | 21 | (hyperfiddle.electric.impl.cljs-file-to-analyze.require/macrodef behind-require) 22 | (req/macrodef behind-alias) 23 | (hyperfiddle.electric.impl.cljs-file-to-analyze.macro-ns/reqmacrodef behind-require-macros) 24 | (reqmac/reqmacrodef behind-require-macro-alias) 25 | (refdef behind-required-refer) 26 | (renamed behind-required-rename) 27 | (reqmacrefer behind-require-macro-refer) 28 | (reqmacrenamed behind-require-macro-rename) 29 | (inc/include behind-include-macros) 30 | (refmac behind-refer-macros) 31 | (useme behind-use) 32 | (use-renamed behind-use-renamed) 33 | (useme-mac behind-use-macro) 34 | (use-renamed-mac behind-use-macro-renamed) 35 | (clojure.analyzer-testing-auto-alias/auto-aliased behind-auto-alias) 36 | (auto-alias/auto-aliased behind-auto-alias-alias) 37 | (auto-aliased behind-auto-alias-refer) 38 | 39 | (let [useme inc] (useme shadowed-by-let)) 40 | (let [{:keys [useme]} {:useme inc}] (useme shadowed-by-let-destructure)) 41 | (fn [useme] (useme shadowed-by-fn)) 42 | (fn [{:keys [useme]}] (useme shadowed-by-fn-destructure)) 43 | (letfn [(useme [] (useme shadowed-by-letfn-fn-name)) 44 | (foooo [] (useme shadowed-by-letfn-other-fn-name))]) 45 | (letfn [(foo [useme] (useme shadowed-by-letfn-local))]) 46 | -------------------------------------------------------------------------------- /test/hyperfiddle/electric/impl/cljs_file_to_analyze/include.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.electric.impl.cljs-file-to-analyze.include) 2 | 3 | (defmacro include [v] `(def ~v)) 4 | -------------------------------------------------------------------------------- /test/hyperfiddle/electric/impl/cljs_file_to_analyze/macro_ns.clj: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.electric.impl.cljs-file-to-analyze.macro-ns) 2 | 3 | (defmacro reqmacrodef [v] `(def ~v)) 4 | (defmacro reqmacrefer [v] `(def ~v)) 5 | (defmacro reqmacrename [v] `(def ~v)) 6 | -------------------------------------------------------------------------------- /test/hyperfiddle/electric/impl/cljs_file_to_analyze/refer_macros.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.electric.impl.cljs-file-to-analyze.refer-macros) 2 | 3 | (defmacro refmac [v] `(def ~v)) 4 | -------------------------------------------------------------------------------- /test/hyperfiddle/electric/impl/cljs_file_to_analyze/require.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.electric.impl.cljs-file-to-analyze.require 2 | #?(:cljs (:require-macros hyperfiddle.electric.impl.cljs-file-to-analyze.require))) 3 | 4 | (defmacro macrodef [sym] `(def ~sym)) 5 | (defmacro refdef [sym] `(def ~sym)) 6 | (defmacro renameme [sym] `(def ~sym)) 7 | -------------------------------------------------------------------------------- /test/hyperfiddle/electric/impl/cljs_file_to_analyze/runtime.clj: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.electric.impl.cljs-file-to-analyze.runtime) 2 | 3 | (defmacro only-macro []) 4 | (defmacro macro-and-runtime []) 5 | -------------------------------------------------------------------------------- /test/hyperfiddle/electric/impl/cljs_file_to_analyze/runtime.cljs: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.electric.impl.cljs-file-to-analyze.runtime 2 | (:require-macros hyperfiddle.electric.impl.cljs-file-to-analyze.runtime)) 3 | 4 | (defn macro-and-runtime []) 5 | (defn only-runtime []) 6 | -------------------------------------------------------------------------------- /test/hyperfiddle/electric/impl/cljs_file_to_analyze/use.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.electric.impl.cljs-file-to-analyze.use) 2 | 3 | (defmacro useme [x] `(def ~x)) 4 | (defmacro renameme [x] `(def ~x)) 5 | -------------------------------------------------------------------------------- /test/hyperfiddle/electric/impl/cljs_file_to_analyze/use_macros.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.electric.impl.cljs-file-to-analyze.use-macros) 2 | 3 | (defmacro useme-mac [x] `(def ~x)) 4 | (defmacro renameme-mac [x] `(def ~x)) 5 | -------------------------------------------------------------------------------- /test/hyperfiddle/electric/impl/compiler_test_clj.clj: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.electric.impl.compiler-test-clj) 2 | 3 | (def cannot-be-unsited) 4 | -------------------------------------------------------------------------------- /test/hyperfiddle/electric/impl/compiler_test_cljs.cljs: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.electric.impl.compiler-test-cljs) 2 | 3 | (def cannot-be-unsited) 4 | -------------------------------------------------------------------------------- /test/hyperfiddle/electric/impl/expand_macro.clj: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.electric.impl.expand-macro) 2 | 3 | (defmacro twice [x] `[~x ~x]) 4 | -------------------------------------------------------------------------------- /test/hyperfiddle/electric/impl/expand_require_referred.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.electric.impl.expand-require-referred 2 | #?(:cljs (:require-macros hyperfiddle.electric.impl.expand-require-referred))) 3 | 4 | (defmacro referred [] :referred) 5 | 6 | (defn referred-fn []) 7 | -------------------------------------------------------------------------------- /test/hyperfiddle/electric/impl/expand_unloaded.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.electric.impl.expand-unloaded) 2 | -------------------------------------------------------------------------------- /test/hyperfiddle/electric/impl/mount_point_test.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.electric.impl.mount-point-test 2 | (:require [hyperfiddle.incseq :as i] 3 | [hyperfiddle.incseq.diff-impl :as d] 4 | [missionary.core :as m] 5 | [hyperfiddle.kvs :as kvs] 6 | [hyperfiddle.electric.impl.runtime3 :as r] 7 | [hyperfiddle.electric.impl.mount-point :as mp] 8 | [clojure.test :refer [deftest is]]) 9 | (:import #?(:clj (java.util LinkedList)) 10 | missionary.Cancelled)) 11 | 12 | (defn frame [peer slot rank & tags] 13 | (let [tags-array (object-array (count tags)) 14 | frame (r/->Frame peer slot rank nil nil nil tags-array nil)] 15 | (reduce (fn [i tag] 16 | (when tag 17 | (aset tags-array i 18 | (r/create-call 19 | (r/->Slot frame i) 20 | :client (r/effect tag)))) 21 | (inc i)) 0 tags) frame)) 22 | 23 | (defn queue [] 24 | #?(:clj (let [q (LinkedList.)] 25 | (fn 26 | ([] (.remove q)) 27 | ([x] (.add q x) nil))) 28 | :cljs (let [q (make-array 0)] 29 | (fn 30 | ([] 31 | (when (zero? (alength q)) 32 | (throw (js/Error. "No such element."))) 33 | (.shift q)) 34 | ([x] (.push q x) nil))))) 35 | 36 | (deftest sibling-tags 37 | (let [q (queue) 38 | _ (r/make-peer :client {} nil 39 | {:root (fn ([] {0 (r/ctor :root 0)}) 40 | ([idx] 41 | (case idx 42 | 0 (r/cdef 0 [] [nil nil nil] nil (fn [frame] (q frame) (r/pure nil))))))} 43 | :root nil) 44 | f (q) 45 | mp (doto (mp/create (r/frame-peer f)) 46 | (kvs/insert! (r/tag f 0) :foo) 47 | (kvs/insert! (r/tag f 1) :bar) 48 | (kvs/insert! (r/tag f 2) :baz)) 49 | ps (mp #(q :step) #(q :done))] 50 | (is (= (q) :step)) 51 | (is (= @ps {:grow 3 52 | :degree 3 53 | :shrink 0 54 | :permutation {} 55 | :change {0 :foo, 1 :bar, 2 :baz} 56 | :freeze #{}})) 57 | (kvs/update! mp (r/tag f 1) (constantly :BAR)) 58 | (is (= (q) :step)) 59 | (kvs/remove! mp (r/tag f 0)) 60 | (is (= @ps {:grow 0 61 | :degree 3 62 | :shrink 1 63 | :permutation {0 1, 1 2, 2 0} 64 | :change {0 :BAR} 65 | :freeze #{}})) 66 | (kvs/remove! mp (r/tag f 1)) 67 | (is (= (q) :step)) 68 | (kvs/remove! mp (r/tag f 2)) 69 | (is (= @ps {:grow 0 70 | :degree 2 71 | :shrink 2 72 | :permutation {} 73 | :change {} 74 | :freeze #{}})) 75 | (ps) 76 | (is (= (q) :step)) 77 | (is (thrown? Cancelled @ps)) 78 | (is (= (q) :done)))) 79 | 80 | (deftest sibling-tags-insert-after-read 81 | (let [q (queue) 82 | _ (r/make-peer :client {} nil 83 | {:root (fn ([] {0 (r/ctor :root 0)}) 84 | ([idx] 85 | (case idx 86 | 0 (r/cdef 0 [] [nil nil] nil (fn [frame] (q frame) (r/pure nil))))))} 87 | :root nil) 88 | f (q) 89 | mp (mp/create (r/frame-peer f)) 90 | ps (mp #(q :step) #(q :done))] 91 | (is (= (q) :step)) 92 | (is (= @ps (d/empty-diff 0))) 93 | (kvs/insert! mp (r/tag f 0) :foo) 94 | (kvs/insert! mp (r/tag f 1) :bar) 95 | (is (= (q) :step)) 96 | (is (= @ps {:grow 2 97 | :degree 2 98 | :shrink 0 99 | :permutation {} 100 | :change {0 :foo 101 | 1 :bar} 102 | :freeze #{}})))) 103 | 104 | (deftest cousin-tags-insert-after-read 105 | (let [q (queue) 106 | _ ((m/reduce (constantly nil) 107 | (r/peer-root 108 | (r/make-peer :client {} nil 109 | {:root (fn ([] {0 (r/ctor :root 0)}) 110 | ([idx] 111 | (case idx 112 | 0 (r/cdef 0 [] [nil] nil 113 | (fn [frame] 114 | (q frame) 115 | (r/define-call frame 0 116 | (r/effect (m/observe 117 | (fn [!] 118 | (! {:grow 2 119 | :degree 2 120 | :shrink 0 121 | :permutation {} 122 | :change {0 (r/ctor :root 1) 123 | 1 (r/ctor :root 1)} 124 | :freeze #{}}) 125 | #(q :dispose))))) 126 | (r/call frame 0))) 127 | 1 (r/cdef 0 [] [nil] nil 128 | (fn [frame] 129 | (q frame) 130 | (r/pure nil))))))} 131 | :root nil))) {} {}) 132 | f (q) 133 | f1 (q) 134 | f2 (q) 135 | mp (mp/create (r/frame-peer f)) 136 | ps (mp #(q :step) #(q :done))] 137 | (is (= (q) :step)) 138 | (is (= @ps (i/empty-diff 0))) 139 | (kvs/insert! mp (r/tag f1 0) :foo) 140 | (kvs/insert! mp (r/tag f2 0) :bar) 141 | (is (= (q) :step)) 142 | (is (= @ps {:grow 2 143 | :degree 2 144 | :shrink 0 145 | :permutation {} 146 | :change {1 :bar 147 | 0 :foo} 148 | :freeze #{}})))) 149 | 150 | (deftest add-item-in-parent-frame 151 | (let [q (queue) 152 | _ ((m/reduce {} nil 153 | (r/peer-root 154 | (r/make-peer :client {} nil 155 | {:root (fn ([] {0 (r/ctor :root 0)}) 156 | ([idx] 157 | (case idx 158 | 0 (r/cdef 0 [] [nil nil] nil 159 | (fn [frame] 160 | (q frame) 161 | (r/define-call frame 0 162 | (r/effect (m/observe 163 | (fn [!] 164 | (! {:grow 1 165 | :degree 1 166 | :shrink 0 167 | :permutation {} 168 | :change {0 (r/ctor :root 1)} 169 | :freeze #{}}) 170 | #(q :dispose))))) 171 | (r/call frame 0))) 172 | 1 (r/cdef 0 [] [nil] nil 173 | (fn [frame] 174 | (q frame) 175 | (r/pure nil))))))} 176 | :root nil))) q q) 177 | root (q) 178 | child (q) 179 | mp (doto (mp/create (r/frame-peer root)) 180 | (kvs/insert! (r/tag root 1) 3) 181 | (kvs/insert! (r/tag child 0) 0)) 182 | ps (mp #(q :step) #(q :done))] 183 | (is (= (q) :step)) 184 | (is (= @ps {:degree 2, :permutation {}, :grow 2, :shrink 0, :change {0 0, 1 3}, :freeze #{}})))) -------------------------------------------------------------------------------- /test/hyperfiddle/goog_calls_test3.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.goog-calls-test3 2 | (:require [hyperfiddle.electric3 :as e] 3 | [hyperfiddle.electric.impl.lang3 :as lang] 4 | #?(:cljs [goog.color]) 5 | #?(:cljs [goog.math :as gm]) 6 | #?(:cljs [goog.string.format]) 7 | #?(:cljs [goog.string :refer (format)])) 8 | #?(:cljs (:import [goog Uri] 9 | [goog.events EventType]))) 10 | 11 | (e/defn Main [] 12 | (e/client 13 | (list 14 | (goog.color/hslToHex 0.5 0.5 0.5) 15 | (Uri. "http://example.com") 16 | EventType.CLICK 17 | goog.events.EventType.CLICK 18 | (gm/clamp -1 0 5) 19 | (format "%4d" 12) 20 | (js/matchMedia (e/watch (atom "(max-width: 600px)")))))) 21 | -------------------------------------------------------------------------------- /test/hyperfiddle/incseq/diff_impl_test.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.incseq.diff-impl-test 2 | (:require [hyperfiddle.incseq.diff-impl :as d] 3 | [hyperfiddle.incseq.perm-impl :as p] 4 | [hyperfiddle.incseq :as i] 5 | [clojure.test :refer [deftest is]] 6 | [clojure.test.check :as tc] 7 | [clojure.test.check.properties :as tc-prop] 8 | [clojure.test.check.generators :as tc-gen] 9 | [clojure.test.check.clojure-test :as tct])) 10 | 11 | (deftest combine-simple 12 | (is (= (d/combine 13 | {:grow 1 14 | :degree 1 15 | :shrink 0 16 | :permutation {} 17 | :change {0 :a} 18 | :freeze #{}} 19 | {:grow 0 20 | :degree 1 21 | :shrink 1 22 | :permutation {} 23 | :change {} 24 | :freeze #{}}) 25 | {:grow 0 26 | :degree 0 27 | :shrink 0 28 | :permutation {} 29 | :change {} 30 | :freeze #{}})) 31 | (is (= (d/combine 32 | {:grow 1 33 | :degree 4 34 | :shrink 2 35 | :permutation (p/rotation 3 1) 36 | :change {1 :e} 37 | :freeze #{}} 38 | {:grow 2 39 | :degree 4 40 | :shrink 1 41 | :permutation (p/rotation 1 3) 42 | :change {0 :f 1 :g 2 :h} 43 | :freeze #{}}) 44 | {:grow 2 45 | :degree 5 46 | :shrink 2 47 | :permutation (p/compose (p/transposition 2 4) (p/transposition 1 3)) 48 | :change {0 :f, 1 :g, 2 :h} 49 | :freeze #{}}))) 50 | 51 | (deftest combine-grow-dont-move 52 | (is (= (d/combine 53 | {:grow 2 54 | :degree 2 55 | :shrink 0 56 | :permutation {} 57 | :change {0 :x1, 1 :y1} 58 | :freeze #{}} 59 | {:grow 3 60 | :shrink 0 61 | :degree 5 62 | :permutation {0 2, 1 3, 2 4, 3 0, 4 1} 63 | :change {0 :x0, 1 :y0, 2 :z0} 64 | :freeze #{}}) 65 | {:grow 5 66 | :degree 5 67 | :shrink 0 68 | :permutation {} 69 | :change {3 :x1, 4 :y1, 0 :x0, 1 :y0, 2 :z0} 70 | :freeze #{}}))) 71 | 72 | (deftest grow-permutation-simplifies 73 | (is (= (d/combine ;; [:z] -> [:z] -> [:x :y :z] 74 | {:degree 1, :permutation {}, :grow 0, :shrink 0, :change {}, :freeze #{}} ; [:z] 75 | {:grow 2, :shrink 0, :degree 3, :permutation {0 1, 1 2, 2 0}, :change {0 :x, 1 :y}, :freeze #{}}) 76 | {:degree 3, :permutation {2 0, 0 2}, :grow 2, :shrink 0, :change {0 :x, 1 :y}, :freeze #{}}))) 77 | 78 | (deftest shrink-permutation-simplifies 79 | (is (= (d/combine ;; [:w :x :y :z] -> [:w :x :y :z] -> [:z] 80 | {:degree 4, :grow 0, :shrink 0, :permutation {}, :change {}, :freeze #{}} 81 | {:degree 4, :grow 0, :shrink 3, :permutation {1 0, 2 1, 3 2, 0 3}, :change {}, :freeze #{}}) 82 | {:degree 4, :permutation {0 3, 3 0}, :grow 0, :shrink 3, :change {}, :freeze #{}}))) 83 | 84 | (def patch-vecing-differ-result-returns-same-vector 85 | (tc-prop/for-all [a (tc-gen/fmap vec (tc-gen/set tc-gen/small-integer)) 86 | b (tc-gen/fmap vec (tc-gen/set tc-gen/small-integer))] 87 | (let [a (into [] (distinct) a), b (into [] (distinct) b) 88 | diff-seq (i/->seq-differ identity)] 89 | (= b (d/patch-vec a (do (diff-seq a) (diff-seq b))))))) 90 | 91 | (tct/defspec patch-vecing-differ-result-returns-same-vector-spec 100 patch-vecing-differ-result-returns-same-vector) 92 | 93 | (def d-combine 94 | (tc-prop/for-all [a (tc-gen/fmap vec (tc-gen/set tc-gen/small-integer)) 95 | b (tc-gen/fmap vec (tc-gen/set tc-gen/small-integer)) 96 | c (tc-gen/fmap vec (tc-gen/set tc-gen/small-integer))] 97 | (let [a (into [] (distinct) a), b (into [] (distinct) b), c (into [] (distinct) c) 98 | diff-seq (i/->seq-differ identity) 99 | _ (diff-seq a), a->b (diff-seq b), b->c (diff-seq c)] 100 | (= c (d/patch-vec a (d/combine a->b b->c)))))) 101 | 102 | (tct/defspec d-combine-spec 100 d-combine) 103 | -------------------------------------------------------------------------------- /test/hyperfiddle/incseq/fixed_impl_test.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.incseq.fixed-impl-test 2 | (:require [hyperfiddle.incseq.fixed-impl :refer [flow]] 3 | [clojure.test :refer [deftest is]]) 4 | #?(:clj (:import [clojure.lang IFn IDeref]))) 5 | 6 | (defn queue [] 7 | #?(:clj (let [q (java.util.LinkedList.)] 8 | (fn 9 | ([] (.remove q)) 10 | ([x] (.add q x) nil))) 11 | :cljs (let [q (make-array 0)] 12 | (fn 13 | ([] 14 | (when (zero? (alength q)) 15 | (throw (js/Error. "No such element."))) 16 | (.shift q)) 17 | ([x] (.push q x) nil))))) 18 | 19 | (deftype Ps [cancel transfer] 20 | IFn 21 | (#?(:clj invoke :cljs -invoke) [_] 22 | (cancel)) 23 | IDeref 24 | (#?(:clj deref :cljs -deref) [_] 25 | (transfer))) 26 | 27 | (deftest zero 28 | (let [q (queue) 29 | ps ((flow) #(q :step) #(q :done))] 30 | (is (= (q) :step)) 31 | @ps := {:grow 0 32 | :degree 0 33 | :shrink 0 34 | :permutation {} 35 | :change {} 36 | :freeze #{}})) 37 | 38 | (deftest one 39 | (let [q (queue) 40 | ps ((flow (fn [n t] (q n) (n) (->Ps #(q :cancel) q))) 41 | #(q :step) #(q :done)) 42 | n (q)] 43 | (q) := :step 44 | (q 0) 45 | @ps := {:grow 1, :degree 1, :shrink 0, :permutation {}, :change {0 0}, :freeze #{}} 46 | (n) 47 | (q) := :step 48 | (q 1) 49 | @ps := {:grow 0, :shrink 0, :degree 1, :permutation {}, :change {0 1}, :freeze #{}})) 50 | 51 | (deftest two 52 | (let [q (queue) 53 | ps ((flow 54 | (fn [n t] (q n) (->Ps #(q :cancel) q)) 55 | (fn [n t] (q n) (->Ps #(q :cancel) q))) 56 | #(q :step) #(q :done)) 57 | n1 (q) 58 | n2 (q)] 59 | (n1) 60 | (q) := :step 61 | (n2) 62 | (q 0) 63 | (q :a) 64 | @ps := {:grow 2 65 | :degree 2 66 | :shrink 0 67 | :permutation {} 68 | :change {0 0, 1 :a} 69 | :freeze #{}} 70 | (n1) 71 | (q) := :step 72 | (n2) 73 | (q 1) 74 | (q :b) 75 | @ps := {:grow 0 76 | :degree 2 77 | :shrink 0 78 | :permutation {} 79 | :change {0 1, 1 :b} 80 | :freeze #{}})) 81 | -------------------------------------------------------------------------------- /test/hyperfiddle/incseq/items_impl_test.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.incseq.items-impl-test 2 | (:require [hyperfiddle.incseq.diff-impl :as d] 3 | [hyperfiddle.incseq.items-impl :as ii] 4 | [clojure.test :as t]) 5 | (:import #?(:clj [clojure.lang IFn IDeref]) 6 | [missionary Cancelled])) 7 | 8 | (t/deftest basic 9 | (let [q #?(:clj (let [q (java.util.LinkedList.)] 10 | (fn 11 | ([] (.remove q)) 12 | ([x] (.add q x) nil))) 13 | :cljs (let [q (make-array 0)] 14 | (fn 15 | ([] 16 | (when (zero? (alength q)) 17 | (throw (js/Error. "No such element."))) 18 | (.shift q)) 19 | ([x] (.push q x) nil)))) 20 | ps ((ii/flow (fn [step done] 21 | (q [step done]) 22 | (step) 23 | (reify 24 | IFn 25 | (#?(:clj invoke :cljs -invoke) [_] 26 | (q :cancel)) 27 | IDeref 28 | (#?(:clj deref :cljs -deref) [_] 29 | (q))))) 30 | #(q :step) #(q :done)) 31 | [step done] (q) 32 | _ (t/is (= (q) :step)) 33 | _ (q (assoc (d/empty-diff 2) 34 | :change {0 :foo 1 :bar} 35 | :grow 2)) 36 | diff @ps 37 | _ (t/is (= (dissoc diff :change) 38 | (assoc (dissoc (d/empty-diff 2) :change) 39 | :freeze #{0 1} 40 | :grow 2))) 41 | [item0 item1] (map (:change diff) [0 1]) 42 | ps0 (item0 #(q :step0) #(q :done0)) 43 | _ (t/is (= (q) :step0)) 44 | _ (t/is (= @ps0 :foo)) 45 | ps1 (item1 #(q :step1) #(q :done1)) 46 | _ (t/is (= (q) :step1)) 47 | _ (step) 48 | _ (t/is (= (hash-set (q) (q)) #{:step :step0})) 49 | _ (q (assoc (d/empty-diff 2) 50 | :permutation {0 1 1 0} 51 | :change {1 :foo 0 :BAR})) 52 | _ (t/is (= @ps (assoc (d/empty-diff 2) :permutation {0 1 1 0}))) 53 | _ (t/is (= @ps1 :BAR)) 54 | _ (t/is (= @ps0 :foo)) 55 | _ (ps0) 56 | _ (t/is (= (q) :step0)) 57 | ps0- (item0 #(q :step0-) #(q :done0-)) 58 | _ (t/is (= (q) :step0-)) 59 | _ (t/is (= nil (try @ps0 (catch Cancelled _)))) 60 | _ (t/is (= (q) :done0)) 61 | _ (step) 62 | _ (t/is (= (hash-set (q) (q)) #{:step :step1})) 63 | _ (q (assoc (d/empty-diff 2) 64 | :change {1 :FOO})) 65 | _ (t/is (= @ps0- :FOO)) 66 | _ (t/is (= nil (try (item1 #(q :step1-) #(q :done1-)) 67 | (catch #?(:clj Error :cljs js/Error) _)))) 68 | _ (step) 69 | _ (t/is (= (hash-set (q)) #{:step0-})) 70 | _ (q (assoc (d/empty-diff 2) 71 | :freeze #{0 1})) 72 | _ (t/is (= @ps1 :BAR)) 73 | _ (t/is (= (q) :done1)) 74 | _ (t/is (= @ps0- :FOO)) 75 | _ (t/is (= (q) :done0-)) 76 | _ (t/is (= @ps (d/empty-diff 2))) 77 | _ (done) 78 | _ (t/is (= (q) :done))])) 79 | -------------------------------------------------------------------------------- /test/hyperfiddle/incseq/latest_product_impl_test.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.incseq.latest-product-impl-test 2 | (:require [hyperfiddle.incseq.latest-product-impl :refer [flow]] 3 | [clojure.test :refer [deftest is testing]]) 4 | #?(:clj (:import (clojure.lang IFn IDeref)))) 5 | 6 | (defn queue [] 7 | #?(:clj (let [q (java.util.LinkedList.)] 8 | (fn 9 | ([] (.remove q)) 10 | ([x] (.add q x) nil))) 11 | :cljs (let [q (make-array 0)] 12 | (fn 13 | ([] 14 | (when (zero? (alength q)) 15 | (throw (js/Error. "No such element."))) 16 | (.shift q)) 17 | ([x] (.push q x) nil))))) 18 | 19 | (deftype Ps [cancel transfer] 20 | IFn 21 | (#?(:clj invoke :cljs -invoke) [_] 22 | (cancel)) 23 | IDeref 24 | (#?(:clj deref :cljs -deref) [_] 25 | (transfer))) 26 | 27 | (deftest large-input 28 | (let [d1 {:grow 32 :degree 32 :shrink 0 29 | :permutation {} 30 | :change (zipmap (range 32) (range 32)) 31 | :freeze #{}} 32 | d2 {:grow 1 :degree 33 :shrink 0 33 | :permutation {} 34 | :change {32 32} 35 | :freeze #{}} 36 | q (queue) 37 | ps ((flow identity 38 | (fn [step done] 39 | (q step) 40 | (step) 41 | (->Ps #(q :cancel) q))) 42 | #(q :step) #(q :done)) 43 | step (q)] 44 | (is (= (q) :step)) 45 | (q d1) 46 | (is (= @ps d1)) 47 | (step) 48 | (is (= (q) :step)) 49 | (q d2) 50 | (is (= @ps d2)))) -------------------------------------------------------------------------------- /test/hyperfiddle/incseq/mount_impl_test.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.incseq.mount-impl-test 2 | (:require [hyperfiddle.domlike :as d] 3 | [hyperfiddle.incseq.mount-impl :refer [mount]] 4 | [clojure.test :refer [deftest is]])) 5 | 6 | (def mount-items (mount d/append-child d/replace-child d/insert-before d/remove-child d/nth-child)) 7 | 8 | (deftest grow-shrink-same 9 | (let [p (d/node) 10 | a (d/node) 11 | b (d/node)] 12 | (d/append-child p a) 13 | (d/append-child p b) 14 | (is (= (d/tree p) 15 | (d/tree (mount-items p 16 | {:grow 2 17 | :degree 4 18 | :shrink 2 19 | :permutation {0 2, 1 3, 2 0, 3 1} 20 | :change {0 a, 1 b} 21 | :freeze #{}})))))) 22 | 23 | (deftest append-unordered 24 | (let [p (d/node) 25 | a (d/node) 26 | b (d/node)] 27 | (= [p [a] [b]] 28 | (d/tree 29 | (mount-items p 30 | {:grow 2 31 | :degree 2 32 | :shrink 0 33 | :permutation {0 1, 1 0} 34 | :change {0 a, 1 b} 35 | :freeze #{}}))))) -------------------------------------------------------------------------------- /test/hyperfiddle/incseq/perm_impl_test.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.incseq.perm-impl-test 2 | (:require [hyperfiddle.incseq.perm-impl :as p] 3 | [clojure.test :refer [deftest is]])) 4 | 5 | (deftest suite 6 | (is (= (p/decompose conj #{} {0 1, 1 4, 2 3, 3 2, 4 0}) 7 | #{[0 1 4] [2 3]})) 8 | 9 | (is (= (p/recompose #{[0 1 4] [2 3]}) 10 | {0 1, 1 4, 2 3, 3 2, 4 0})) 11 | 12 | (is (= (p/decompose conj #{} (p/inverse {0 1, 1 4, 2 3, 3 2, 4 0})) 13 | #{[1 0 4] [3 2]})) 14 | 15 | (is (= (p/recompose #{[1 0 4] [3 2]}) 16 | {0 4, 1 0, 2 3, 3 2, 4 1})) 17 | 18 | (is (= (p/arrange [0 1 2 3 4] {0 1, 1 4, 2 3, 3 2, 4 0}) 19 | [1 4 3 2 0])) 20 | 21 | (is (= (p/arrange [:a :b :c :d :e] {0 1, 1 4, 2 3, 3 2, 4 0}) 22 | [:b :e :d :c :a])) 23 | 24 | (is (= (p/compose 25 | (p/cycle [1 3 2 4]) 26 | (p/cycle [1 4 2 3])) 27 | {})) 28 | 29 | (is (= (p/inverse (p/split-swap 4 2 3)) 30 | (p/split-swap 4 3 2))) 31 | 32 | (is (= (p/order {}) 1)) 33 | 34 | (is (= (p/order (p/cycle [2 3])) 2)) 35 | 36 | (is (= (p/order (p/cycle [2 3 4])) 3)) 37 | 38 | (is (= (p/order (p/compose (p/cycle [0 1]) (p/cycle [2 3 4]))) 6)) 39 | 40 | (is (= (p/involution? {}) false)) 41 | 42 | (is (= (p/involution? (p/cycle [2 3])) true)) 43 | 44 | (is (= (p/involution? (p/cycle [2 3 4])) false)) 45 | 46 | (is (= (p/transposition? (p/cycle [2 3])) true)) 47 | 48 | (is (= (p/transposition? (p/cycle [2 3 4])) false)) 49 | 50 | (is (= (p/rotations (fn [r i j] (p/compose (p/rotation i j) r)) 51 | {} {0 1, 1 4, 2 3, 3 2, 4 0}) 52 | {0 1, 1 4, 2 3, 3 2, 4 0}))) -------------------------------------------------------------------------------- /test/hyperfiddle/js_calls_test3.cljs: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.js-calls-test3 2 | (:require [hyperfiddle.electric-local-def3 :as l] 3 | [hyperfiddle.rcf :as rcf :refer [tests tap % with]] 4 | ["./js_calls_test3" :as call-test])) 5 | 6 | ;;; Goal: confirm Electric and CLJS have the same js function call semantics. 7 | 8 | (call-test/install) ; required for later tests 9 | 10 | ;;; The two tests blocks should be identical in intent and result. 11 | 12 | ;; CLJS 13 | (tests 14 | "js scoped call in cljs" 15 | call-test/scope.fn := call-test/scope.fn 16 | (call-test/scope.fn) := "value" 17 | (.fn call-test/scope) := "value" 18 | (js/hyperfiddle.js_calls_test3.scope.fn) := "value" ; requires `(call-test/install)` 19 | (let [fn (.-fn call-test/scope)] 20 | (undefined? (fn)) := true ; fn lost its `this` context 21 | ((.bind fn call-test/scope)) := "value" ; re-set `this` context to `scope` 22 | )) 23 | 24 | ;; Electric 25 | (tests 26 | "js scoped call in electric" 27 | (with ((l/single {} 28 | (let [fn (.-fn call-test/scope)] 29 | (tap [call-test/scope.fn 30 | (call-test/scope.fn) ; direct access 31 | (.fn call-test/scope) ; two-step access 32 | (js/hyperfiddle.js_calls_test3.scope.fn) ; global access, requires `(call-test/install)` 33 | (undefined? (fn)) 34 | ((.bind fn call-test/scope))]))) tap tap) 35 | % := [call-test/scope.fn "value" "value" "value" true "value"])) 36 | 37 | -------------------------------------------------------------------------------- /test/hyperfiddle/js_calls_test3.js: -------------------------------------------------------------------------------- 1 | // Test that when electric calls scope.fn(), fn is called with `scope` bound as `this`, thus returning `"value"` 2 | // See `js_calls_test3.cljs` 3 | 4 | // How to reproduce from JS console: 5 | 6 | // ```js 7 | // scope.fn(); // => "value" 8 | // var fn = scope.fn; 9 | // fn(); // => undefined 10 | 11 | // fn.bind(scope)(); // => "value 12 | // ``` 13 | 14 | export var scope = { 15 | value: "value", 16 | fn: function(){ 17 | return this.value; 18 | } 19 | }; 20 | 21 | export function install(){ 22 | globalThis.hyperfiddle.js_calls_test3.scope = scope; 23 | } 24 | -------------------------------------------------------------------------------- /test/hyperfiddle/missionary_test.cljc: -------------------------------------------------------------------------------- 1 | (ns hyperfiddle.missionary-test 2 | (:require [missionary.core :as m] 3 | [hyperfiddle.rcf :refer [tests tap % with]]) 4 | (:import (missionary Cancelled))) 5 | 6 | 7 | (tests 8 | "flow cancel before transfer" 9 | (def !x (atom 0)) 10 | (def >x (m/watch !x)) 11 | (def !it (>x (fn [] (tap ::notify)) 12 | (fn [] (tap ::terminate)))) 13 | % := ::notify 14 | (!it) 15 | @!it :throws Cancelled 16 | % := ::terminate) 17 | 18 | (tests 19 | "pentagram of death - via Kenny Tilton" 20 | (def !aa (atom 1)) 21 | (def !a7 (atom 7)) 22 | (with 23 | ((m/reactor 24 | (let [client [] (d/client {:server-type :dev-local :storage-dir :mem :system "test"})) 6 | (defn v! [db e a] (d/q '[:find ?v :in $ ?e ?a :where [?e ?a ?v]] (d/db db) e a)) 7 | (defn pull! [c e] (d/pull (d/db c) '[*] e)) 8 | 9 | (defmacro with-fresh-connection [sym & body] 10 | `(let [client# (->client) 11 | g# (d/create-database client# {:db-name "testing"}) 12 | ~sym (d/connect client# {:db-name "testing"})] 13 | (do ~@body))) 14 | 15 | (rcf/tests 16 | "a ref survives even if the entity it refers to gets retracted" 17 | (with-fresh-connection c 18 | (let [txs [[{:db/ident :aref :db/valueType :db.type/ref :db/cardinality :db.cardinality/one} 19 | {:db/ident :bar :db/valueType :db.type/string :db/cardinality :db.cardinality/one}] 20 | [[:db/add "foo" :aref "-2"] 21 | [:db/add "-2" :bar "asdf"]] 22 | [[:db/retractEntity "-2"]]] 23 | ids (apply merge (map (comp :tempids #(d/transact c {:tx-data %})) txs))] 24 | (v! c (ids "foo") :aref) := [[_]]))) 25 | --------------------------------------------------------------------------------