diff --git a/.github/actions/setup-build-deps/action.yml b/.github/actions/setup-build-deps/action.yml deleted file mode 100644 index a1fb456a..00000000 --- a/.github/actions/setup-build-deps/action.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: "Setup Build Dependencies" -description: Setup Build Dependencies - -runs: - using: "composite" - steps: - - name: Get IC WASM version - shell: bash - run: echo "ic_wasm_version=$(cat .ic-wasm-version | xargs)" >> "$GITHUB_ENV" - - - name: Cache IC WASM - uses: actions/cache@v4 - with: - path: /usr/local/bin/ic-wasm - key: ic-wasm-cache-${{ env.ic_wasm_version }} - - - name: Install IC WASM - shell: bash - run: | - if command -v ic-wasm - then - echo "IC WASM restored from cache" - else - echo "IC WASM not restored from cache, downloading:" - curl -L https://github.com/dfinity/ic-wasm/releases/download/${{ env.ic_wasm_version }}/ic-wasm-x86_64-unknown-linux-gnu.tar.xz \ - | tar -xJ --strip-components=1 -C /usr/local/bin ic-wasm-x86_64-unknown-linux-gnu/ic-wasm - chmod +x /usr/local/bin/ic-wasm - fi - echo "IC WASM version" - ic-wasm --version diff --git a/.github/actions/setup-dfx/action.yml b/.github/actions/setup-dfx/action.yml deleted file mode 100644 index 6a7b0e19..00000000 --- a/.github/actions/setup-dfx/action.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: "Setup DFX" -description: Setup DFX - -runs: - using: "composite" - steps: - - name: Get DFX version - shell: bash - run: echo "dfx_version=$(cat dfx.json | jq -r .dfx)" >> "$GITHUB_ENV" - - - name: Cache DFX - uses: actions/cache@v4 - with: - path: /usr/local/bin/dfx - key: dfx-cache-${{ env.dfx_version }} - - - name: Install DFX - shell: bash - run: | - if command -v dfx - then - echo "DFX restored from cache" - else - echo "DFX not restored from cache, running install script:" - DFXVM_INIT_YES=1 DFX_VERSION=${{ env.dfx_version }} sh -ci "$(curl -fsSL https://sdk.dfinity.org/install.sh)" - fi - echo "DFX version" - source "$HOME/.local/share/dfx/env" - dfx --version - - - name: Configure DFX - shell: bash - run: | - mkdir -p $HOME/.config/dfx - cat <$HOME/.config/dfx/networks.json - { - "local": { - "bind": "127.0.0.1:8080", - "type": "ephemeral", - "replica": { - "subnet_type": "system" - } - } - } - EOF diff --git a/.github/actions/setup-nodejs/action.yml b/.github/actions/setup-nodejs/action.yml deleted file mode 100644 index 701c6bb0..00000000 --- a/.github/actions/setup-nodejs/action.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: "Setup nodejs" -description: Setup nodejs - -runs: - using: "composite" - steps: - - uses: actions/setup-node@v4 - with: - node-version-file: ".node-version" - cache: "npm" diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml deleted file mode 100644 index 3f893463..00000000 --- a/.github/workflows/docker-image.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Docker Image CI - -on: - push: - branches: ["main"] - pull_request: - branches: ["main"] - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Build the release binary image - run: make release - - name: Print the git commit and the binary hash - run: make hashes diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml deleted file mode 100644 index 964ccdcc..00000000 --- a/.github/workflows/e2e-tests.yml +++ /dev/null @@ -1,77 +0,0 @@ -name: e2e Tests - -on: - push: - branches: ["main"] - pull_request: - branches: ["main"] - -jobs: - e2e_tests: - runs-on: ubuntu-latest - steps: - - name: Add dfx to PATH - run: echo "$HOME/.local/share/dfx/bin" >> $GITHUB_PATH - - - uses: actions/checkout@v4 - - uses: ./.github/actions/setup-nodejs - - uses: ./.github/actions/setup-dfx - - uses: ./.github/actions/setup-build-deps - - - name: Get Playwright version - id: playwright-version - run: echo "playwright_version=$(cat package-lock.json | jq -r '.dependencies."@playwright/test".version')" >> $GITHUB_ENV - - - name: Cache Playwright dependencies - uses: actions/cache@v4 - id: playwright-cache - with: - path: ~/.cache/ms-playwright - key: ${{ runner.os }}-playwright-${{ env.playwright_version }} - - - name: Install NPM dependencies - run: npm ci - - - name: Install Playwright dependencies - if: steps.playwright-cache.outputs.cache-hit != 'true' - run: npm run install:e2e - - - name: Start DFX - run: dfx start --background - - - name: Import local minter - run: ./e2e/import_local_minter.sh - - - name: Get NNS extension version - shell: bash - run: echo "nns_extension_version=$(cat .nns-extension-version | xargs)" >> "$GITHUB_ENV" - - - name: Set up NNS canisters - run: | - dfx extension install nns --version ${{ env.nns_extension_version }} - dfx nns install - - - name: Deploy canister - run: | - dfx canister create --all - make e2e_build - make local_deploy - - - name: Top up canisters - run: dfx --identity local-minter ledger fabricate-cycles --all --cycles 1000000000000000 - - - name: Run e2e tests - run: npm run test:e2e - - - name: Upload test results - uses: actions/upload-artifact@v4 - if: ${{ !cancelled() }} - with: - name: test-results - path: | - test-results/ - playwright-report/ - retention-days: 7 - - - name: Stop DFX - run: dfx stop diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml deleted file mode 100644 index be42afc0..00000000 --- a/.github/workflows/lint-and-test.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Lint and Test - -on: - push: - branches: ["main"] - pull_request: - branches: ["main"] - -jobs: - lint_and_test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/setup-nodejs - - uses: ./.github/actions/setup-build-deps - - - name: Install NPM dependencies - run: npm ci - - - name: Build frontend - run: NODE_ENV=production npm run build --quiet - - - name: Build canister - run: | - ./build.sh bucket - ./build.sh taggr - - - name: Rust lint - run: cargo clippy --tests --benches -- -D clippy::all - - - name: Rust test - run: cargo test -- --test-threads 1 - - - name: Check Rust formatting - run: cargo fmt --all -- --check - - - name: Check Frontend formatting - run: npm run format:check diff --git a/.gitignore b/.gitignore index ce566dbd..1375e84d 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,8 @@ dist/ # e2e tests test-results/ playwright-report/ +e2e/icp_ledger/ledger.wasm.gz +e2e/icp_ledger/ledger.did # reviews /reviews diff --git a/.icp-ledger-version b/.icp-ledger-version new file mode 100644 index 00000000..ce0af95a --- /dev/null +++ b/.icp-ledger-version @@ -0,0 +1 @@ +920df8055260f443ac3335cc0f2b06e285a688b4 diff --git a/.nns-extension-version b/.nns-extension-version deleted file mode 100644 index 7d856835..00000000 --- a/.nns-extension-version +++ /dev/null @@ -1 +0,0 @@ -0.5.4 diff --git a/Dockerfile b/Dockerfile index 5bce024f..684c18eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,8 @@ RUN apt-get -yq update && \ build-essential pkg-config libssl-dev llvm-dev liblmdb-dev clang cmake rsync libunwind-dev jq xz-utils && \ rm -rf /var/lib/apt/lists/* +WORKDIR /app + # Install Node.js COPY .node-version ./ RUN curl --fail -sSf https://raw.githubusercontent.com/creationix/nvm/${NVM_VERSION}/install.sh | bash && \ @@ -36,6 +38,7 @@ RUN mkdir -p /opt/ic-wasm && \ chmod +x /opt/ic-wasm/ic-wasm # Install dfx +ENV HOME=/root COPY dfx.json ./ RUN DFXVM_INIT_YES=1 DFX_VERSION=$(cat dfx.json | jq -r .dfx) sh -ci "$(curl -fsSL https://internetcomputer.org/install.sh)" ENV PATH=${HOME}/.local/share/dfx/bin:${PATH} @@ -46,4 +49,7 @@ RUN npm ci COPY . . +# Test deps: Playwright (Chromium + system libs) for the e2e step. +RUN npx playwright install chromium --with-deps + ENTRYPOINT [ "./release.sh" ] diff --git a/Makefile b/Makefile index d3b343bd..101b7e93 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,8 @@ +# Prefer podman if installed, else fall back to docker. Override with CONTAINER=docker. +CONTAINER ?= $(shell command -v podman >/dev/null 2>&1 && echo podman || echo docker) + start: - ulimit -n 65000 && dfx start --background -qqqq 2>&1 | grep -v sgymv & + ulimit -n 65000 && dfx start --background -qqqq & cycles: dfx --identity local-minter ledger fabricate-cycles --all --cycles 1000000000000000 @@ -10,7 +13,7 @@ staging_deploy: FEATURES=staging dfx --identity prod deploy --network $(if $(CANISTER),$(CANISTER),staging) taggr local_deploy: - FEATURES=dev dfx deploy + FEATURES=dev dfx deploy taggr dev_build: FEATURES=dev ./build.sh bucket @@ -59,6 +62,8 @@ e2e_build: e2e_test: npm run install:e2e dfx canister create --all + ./e2e/import_local_minter.sh + ./e2e/install_icp_ledger.sh make e2e_build make start || true # don't fail if DFX is already running npm run test:e2e @@ -69,10 +74,24 @@ podman_machine: podman machine rm -f || true CONTAINERS_MACHINE_PROVIDER=qemu podman machine init --cpus 4 --memory 4096 --now +tests: + $(CONTAINER) build --quiet -t taggr . >/dev/null + mkdir -p $(shell pwd)/test-results $(shell pwd)/playwright-report + $(CONTAINER) run --rm \ + --shm-size=1g \ + -v $(shell pwd)/test-results:/app/test-results \ + -v $(shell pwd)/playwright-report:/app/playwright-report \ + taggr tests + release: - $(if $(PODMAN),podman,docker) build -t taggr . - mkdir -p $(shell pwd)/release-artifacts - $(if $(PODMAN),podman,docker) run --rm -v $(shell pwd)/release-artifacts:/target/wasm32-unknown-unknown/release taggr + $(CONTAINER) build --quiet -t taggr . >/dev/null + mkdir -p $(shell pwd)/release-artifacts $(shell pwd)/test-results $(shell pwd)/playwright-report + $(CONTAINER) run --rm \ + --shm-size=1g \ + -v $(shell pwd)/release-artifacts:/app/target/wasm32-unknown-unknown/release \ + -v $(shell pwd)/test-results:/app/test-results \ + -v $(shell pwd)/playwright-report:/app/playwright-report \ + taggr make hashes hashes: diff --git a/README.md b/README.md index 4dc11595..1683565a 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,29 @@ Assume you want to verify a new upgrade proposal with code commit `` and 4. `make release` 5. Verify that the printed hash matches the `` value from the release page. +`make release` runs the full validation pipeline (lints, Rust tests, Playwright e2e) inside the container and only produces a hash if everything passes. A failing release therefore cannot be hashed — the printed hash is a signal that the wasm is both reproducible and tested. Podman is used automatically if installed; otherwise Docker. Override with `CONTAINER=docker make release`. + +Outputs of a successful run: + +- `release-artifacts/taggr.wasm.gz` — the production wasm. +- `test-results/` and `playwright-report/` — Playwright traces and the HTML report (open `playwright-report/index.html` to inspect any failures). + +Note: the first run is slow (Chromium install layer is several hundred MB) and `dfx nns install` downloads the NNS canisters on every run. + ## Release proposal To propose a release, follow the steps above first. If they were successful, you'll find a binary `taggr.wasm.gz` in the `release-artifacts` directory. Use the printed code commit and the binary to submit a new release proposal. +## Running tests during development + +For day-to-day iteration, skip the prod build and just run the test suite: + + make tests + +Same image as `make release` and the same checks (lints, Rust tests, Playwright e2e), but stops before the deterministic prod build. Use this while iterating; use `make release` when you actually want a hash. + ## Backups Make sure you have [installed cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html). diff --git a/dfx.json b/dfx.json index bfe01aa8..0f6dba12 100644 --- a/dfx.json +++ b/dfx.json @@ -13,6 +13,19 @@ "networks": ["local", "ic", "staging", "staging2"] } ] + }, + "icp_ledger": { + "type": "custom", + "candid": "e2e/icp_ledger/ledger.did", + "wasm": "e2e/icp_ledger/ledger.wasm.gz", + "specified_id": "ryjl3-tyaaa-aaaaa-aaaba-cai", + "remote": { + "id": { + "ic": "ryjl3-tyaaa-aaaaa-aaaba-cai", + "staging": "ryjl3-tyaaa-aaaaa-aaaba-cai", + "staging2": "ryjl3-tyaaa-aaaaa-aaaba-cai" + } + } } }, "networks": { diff --git a/docs/LOCAL_DEVELOPMENT.md b/docs/LOCAL_DEVELOPMENT.md index d204d578..b8290cf2 100644 --- a/docs/LOCAL_DEVELOPMENT.md +++ b/docs/LOCAL_DEVELOPMENT.md @@ -45,21 +45,7 @@ Install DFX DFX_VERSION=$(cat dfx.json | jq -r .dfx) sh -ci "$(curl -fsSL https://internetcomputer.org/install.sh)" ``` -The remaining steps are only necessary for deploying NNS canisters locally. This makes it easier to test new account creation with Internet Identity, to make ICP transfers to those accounts or to run Taggr e2e tests without a Docker container. Alternatively, you can [create a backup](#creating-and-restoring-backups) and then refer to the [command reference](#command-reference) to build and deploy. - -Create or edit `~/.config/dfx/networks.json`, and add the following, note that `dfx install` requires port `8080` to work: - -```json -{ - "local": { - "bind": "127.0.0.1:8080", - "type": "ephemeral", - "replica": { - "subnet_type": "system" - } - } -} -``` +The remaining steps are only necessary for deploying the local ICP ledger canister. This makes it easier to test new account creation with Internet Identity, to make ICP transfers to those accounts or to run Taggr e2e tests without a Docker container. Alternatively, you can [create a backup](#creating-and-restoring-backups) and then refer to the [command reference](#command-reference) to build and deploy. Stop DFX if it's running: @@ -84,11 +70,13 @@ make local_reinstall Use `make cycles` to fabricate cycles for the canister. -Install NNS canisters (see the [DFX docs](https://github.com/dfinity/sdk/blob/master/docs/cli-reference/dfx-nns.md)): +Install the ICP ledger canister at its mainnet ID locally (the backend +hard-codes `MAINNET_LEDGER_CANISTER_ID`, so the ledger has to answer at +`ryjl3-tyaaa-aaaaa-aaaba-cai`): ```shell -dfx extension install nns -dfx nns install +./e2e/import_local_minter.sh +./e2e/install_icp_ledger.sh ``` Now you are ready to create a new Taggr account with Internet Identity locally. If you also want to make ICP transfers to this account then continue with the remaining steps, the remaining steps are not necessary for running e2e tests. diff --git a/e2e/install_icp_ledger.sh b/e2e/install_icp_ledger.sh new file mode 100755 index 00000000..d2c92005 --- /dev/null +++ b/e2e/install_icp_ledger.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Install the ICP ledger canister at its mainnet ID (ryjl3-tyaaa-aaaaa-aaaba-cai) +# on the local replica. The backend hard-codes that principal via +# ic-ledger-types::MAINNET_LEDGER_CANISTER_ID, so e2e tests need a real ledger +# answering at that exact ID. The dfx.json `specified_id` pin makes that work +# without `dfx nns install` and without a `system` subnet. +set -euo pipefail + +VERSION="$(cat .icp-ledger-version | xargs)" +DIR="e2e/icp_ledger" +WASM="${DIR}/ledger.wasm.gz" +DID="${DIR}/ledger.did" + +mkdir -p "${DIR}" + +if [ ! -s "${WASM}" ]; then + curl -fsSL \ + "https://download.dfinity.systems/ic/${VERSION}/canisters/ledger-canister.wasm.gz" \ + -o "${WASM}" +fi + +if [ ! -s "${DID}" ]; then + # Repo path moved mid-2024; try the new layout first, fall back to the old one. + curl -fsSL \ + "https://raw.githubusercontent.com/dfinity/ic/${VERSION}/rs/ledger_suite/icp/ledger.did" \ + -o "${DID}" \ + || curl -fsSL \ + "https://raw.githubusercontent.com/dfinity/ic/${VERSION}/rs/rosetta-api/icp_ledger/ledger.did" \ + -o "${DID}" +fi + +# The ledger forbids fee>0 on transfers from the minting_account, so the +# test identity (local-minter) cannot be the minting_account — `transferICP` +# in e2e tests sends regular fee-paying transfers via `dfx ledger transfer`, +# which uses the default 10_000 e8s fee. Use a separate `minter` identity as +# the minting_account and pre-fund local-minter via initial_values instead. +if ! dfx identity get-principal --identity minter >/dev/null 2>&1; then + dfx identity new minter --storage-mode=plaintext +fi + +MINTER_ACCOUNT=$(dfx ledger account-id --identity minter) +LOCAL_MINTER_ACCOUNT=$(dfx ledger account-id --identity local-minter) + +# --mode reinstall so re-running this script (e.g. between e2e iterations) +# wipes ledger state and re-applies the Init args instead of attempting an +# upgrade with an Init-shaped payload. +dfx deploy icp_ledger --mode reinstall -y --argument "(variant { Init = record { + minting_account = \"${MINTER_ACCOUNT}\"; + initial_values = vec { + record { \"${LOCAL_MINTER_ACCOUNT}\"; record { e8s = 100_000_000_000_000 : nat64 } }; + }; + send_whitelist = vec {}; + transfer_fee = opt record { e8s = 10_000 : nat64 }; + token_symbol = opt \"ICP\"; + token_name = opt \"Internet Computer\"; +} })" diff --git a/release.sh b/release.sh index 8bf69a76..d28b3feb 100755 --- a/release.sh +++ b/release.sh @@ -1,15 +1,90 @@ -#!/bin/sh +#!/bin/bash +set -eo pipefail export PATH=${HOME}/.local/share/dfx/bin:${PATH} -make build -dfx start --background -dfx deploy -dfx canister info taggr -OUTPUT=$(dfx canister call taggr prod_release) -if [ "$OUTPUT" != "(true)" ]; then - echo "Error: dev feature is enabled!" - exit 1 -fi -dfx stop -cp .dfx/local/canisters/taggr/taggr.wasm.gz target/wasm32-unknown-unknown/release/taggr.wasm.gz +run_release() { + make build + dfx start --background + dfx deploy taggr + dfx canister info taggr + OUTPUT=$(dfx canister call taggr prod_release) + if [ "$OUTPUT" != "(true)" ]; then + echo "Error: dev feature is enabled!" + exit 1 + fi + dfx stop + cp .dfx/local/canisters/taggr/taggr.wasm.gz target/wasm32-unknown-unknown/release/taggr.wasm.gz +} + +prepare_artifacts() { + # Backend src/backend/assets.rs and storage.rs use include_bytes! on + # dist/frontend/* and target/wasm32-unknown-unknown/release/bucket.wasm.gz, + # so cargo cannot compile the backend (host-side, for tests/clippy) until + # the frontend and bucket canister have been built. + echo "==> [1/7] Building frontend + canisters (prerequisite for cargo lint/test)" + NODE_ENV=production npm run build --quiet >/dev/null 2>&1 + ./build.sh bucket >/dev/null 2>&1 + ./build.sh taggr >/dev/null 2>&1 +} + +run_lints() { + echo "==> [2/7] Lints" + cargo clippy -q --tests --benches -- -D clippy::all + cargo fmt --all -- --check + npm run format:check --silent +} + +run_cargo_tests() { + echo "==> [3/7] Cargo tests" + cargo test -q -- --test-threads 1 +} + +run_e2e() { + # Silence the dfx/build chatter to keep CI output (and Claude transcripts) + # readable — only stage markers and the playwright run itself print. Re-run + # with `bash -x` or remove the `>/dev/null 2>&1` redirects to debug. + echo "==> [4/7] e2e: dfx start + ICP ledger + canister create" + dfx start --background -qqqq >/dev/null 2>&1 + ./e2e/import_local_minter.sh >/dev/null 2>&1 + dfx canister create --all >/dev/null 2>&1 + ./e2e/install_icp_ledger.sh >/dev/null 2>&1 + + echo "==> [5/7] e2e: dev build (needs .dfx/local/canister_ids.json from create)" + NODE_ENV=production DFX_NETWORK=local npm run build --quiet >/dev/null 2>&1 + ./build.sh bucket >/dev/null 2>&1 + FEATURES=dev ./build.sh taggr >/dev/null 2>&1 + + echo "==> [6/7] e2e: deploy + cycles" + FEATURES=dev dfx deploy taggr >/dev/null 2>&1 + dfx --identity local-minter ledger fabricate-cycles --all --cycles 1000000000000000 >/dev/null 2>&1 + + echo "==> [7/7] e2e: playwright" + npm run test:e2e + + dfx stop >/dev/null 2>&1 +} + +run_tests() { + prepare_artifacts + run_lints + run_cargo_tests + run_e2e +} + +case "${1:-release}" in + tests) + run_tests + ;; + release) + # Tests gate the release: a failure here aborts before run_release thanks + # to set -e, so a hash is only ever produced for a fully-tested build. + run_tests + rm -rf .dfx + run_release + ;; + *) + echo "unknown mode: $1 (expected: tests | release)" + exit 1 + ;; +esac diff --git a/src/backend/assets.rs b/src/backend/assets.rs index c8ce986f..29c28669 100644 --- a/src/backend/assets.rs +++ b/src/backend/assets.rs @@ -62,42 +62,6 @@ pub fn load(domains: &HashMap) { include_bytes!("../../dist/frontend/index.js.gz").to_vec(), ); - add_asset( - &["/dfinity.js"], - vec![ - ("Content-Type".into(), "text/javascript".into()), - ("Content-Encoding".into(), "gzip".into()), - ], - include_bytes!("../../dist/frontend/dfinity.js.gz").to_vec(), - ); - - add_asset( - &["/react.js"], - vec![ - ("Content-Type".into(), "text/javascript".into()), - ("Content-Encoding".into(), "gzip".into()), - ], - include_bytes!("../../dist/frontend/react.js.gz").to_vec(), - ); - - add_asset( - &["/vendors.js"], - vec![ - ("Content-Type".into(), "text/javascript".into()), - ("Content-Encoding".into(), "gzip".into()), - ], - include_bytes!("../../dist/frontend/vendors.js.gz").to_vec(), - ); - - add_asset( - &["/app-components.js"], - vec![ - ("Content-Type".into(), "text/javascript".into()), - ("Content-Encoding".into(), "gzip".into()), - ], - include_bytes!("../../dist/frontend/app-components.js.gz").to_vec(), - ); - add_asset( &["/favicon.ico"], vec![("Content-Type".into(), "image/vnd.microsoft.icon".into())], diff --git a/src/backend/env/features.rs b/src/backend/env/features.rs deleted file mode 100644 index f4082bd1..00000000 --- a/src/backend/env/features.rs +++ /dev/null @@ -1,16 +0,0 @@ -use std::collections::HashSet; - -use serde::{Deserialize, Serialize}; - -use super::{user::UserId, Time}; - -// Retained only so the `Memory::features` index can still be deserialized. -// The next release drops the field; serde silently ignores it once the migration -// has drained the index. -#[derive(Default, Serialize, Deserialize)] -pub struct Feature { - pub supporters: HashSet, - pub status: u8, - #[serde(default)] - pub last_activity: Time, -} diff --git a/src/backend/env/memory.rs b/src/backend/env/memory.rs index 5c1dbd83..745263fd 100644 --- a/src/backend/env/memory.rs +++ b/src/backend/env/memory.rs @@ -1,5 +1,4 @@ use super::{ - features::Feature, post::{Post, PostId}, token::Transaction, }; @@ -22,8 +21,6 @@ pub struct Api { pub struct Memory { api: Api, pub posts: ObjectManager, - // TODO: remove - pub features: ObjectManager, #[serde(default)] pub ledger: ObjectManager, #[serde(skip)] @@ -84,7 +81,6 @@ impl Memory { self.api.init(); self.api_ref = Rc::new(RefCell::new(std::mem::take(&mut self.api))); self.posts.init(Rc::clone(&self.api_ref)); - self.features.init(Rc::clone(&self.api_ref)); self.ledger.init(Rc::clone(&self.api_ref)); } diff --git a/src/backend/env/mod.rs b/src/backend/env/mod.rs index f60c4122..6ea72cb1 100644 --- a/src/backend/env/mod.rs +++ b/src/backend/env/mod.rs @@ -38,7 +38,6 @@ pub mod canisters; pub mod config; pub mod delegations; pub mod domains; -pub mod features; pub mod invite; pub mod invoices; pub mod memory; diff --git a/src/backend/updates.rs b/src/backend/updates.rs index bf630383..92985d47 100644 --- a/src/backend/updates.rs +++ b/src/backend/updates.rs @@ -97,26 +97,10 @@ fn post_upgrade() { } #[allow(clippy::all)] -fn sync_post_upgrade_fixtures() { - mutate(|state| { - // One-time cleanup: drain the features index. Each remove() frees the - // stable-memory blocks back to the allocator. The next release can then - // drop Memory::features entirely. - let ids: Vec = state.memory.features.iter().map(|(id, _)| *id).collect(); - for id in ids { - if let Err(err) = state.memory.features.remove(&id) { - state - .logger - .error(format!("couldn't drain feature {}: {}", id, err)); - } - } - }); -} +fn sync_post_upgrade_fixtures() {} #[allow(clippy::all)] -async fn async_post_upgrade_fixtures() { - env::storage::upgrade_buckets().await; -} +async fn async_post_upgrade_fixtures() {} /* * UPDATES diff --git a/src/frontend/src/common.tsx b/src/frontend/src/common.tsx index 7051d590..c3dddf28 100755 --- a/src/frontend/src/common.tsx +++ b/src/frontend/src/common.tsx @@ -1183,7 +1183,7 @@ export const showPopUp = ( export const signOut = async () => { localStorage.clear(); sessionStorage.clear(); - window.authClient.logout(); + await window.authClient.logout(); restartApp(); return true; }; diff --git a/src/frontend/src/header.tsx b/src/frontend/src/header.tsx index 7066ba64..92699cc2 100644 --- a/src/frontend/src/header.tsx +++ b/src/frontend/src/header.tsx @@ -212,7 +212,7 @@ const UserSection = ({ user }: { user: UserType }) => { className="icon_link" href={`/#/user/${user.name}`} > - {user.name.toUpperCase()} + {user.name.toUpperCase()} - - - - diff --git a/src/frontend/src/index.tsx b/src/frontend/src/index.tsx index 5bd1ae50..81193844 100644 --- a/src/frontend/src/index.tsx +++ b/src/frontend/src/index.tsx @@ -74,6 +74,46 @@ const parseHash = (): string[] => { const headerRoot = createRoot(document.getElementById("header") as Element); const footerRoot = createRoot(document.getElementById("footer") as Element); const stack = document.getElementById("stack") as HTMLElement; +const stackRoot = createRoot(stack); + +type Frame = { hash: string; key: number; node: React.ReactNode }; +let frames: Frame[] = []; +let frameSeq = 0; +const frameListeners = new Set<() => void>(); +const notifyFrames = () => frameListeners.forEach((l) => l()); + +const FrameStack = () => { + const fs = React.useSyncExternalStore( + (cb) => { + frameListeners.add(cb); + return () => { + frameListeners.delete(cb); + }; + }, + () => frames, + ); + return ( + <> + {fs.map((f, i) => ( +
+ {f.node} +
+ ))} + + ); +}; + +stackRoot.render( + + + , +); const renderFrame = (content: React.ReactNode) => { document.getElementById("logo_container")?.remove(); @@ -85,21 +125,16 @@ const renderFrame = (content: React.ReactNode) => { return; } - const frames = Array.from(stack.children as HTMLCollectionOf); - frames.forEach((e) => (e.style.display = "none")); - const currentFrame = frames[frames.length - 1]; - const lastFrame = frames[frames.length - 2]; - - if (lastFrame && lastFrame.dataset.hash == location.hash) { - currentFrame.remove(); - lastFrame.style.display = "block"; - return; + const last = frames[frames.length - 2]; + if (last && last.hash == location.hash) { + frames = frames.slice(0, -1); + } else { + frames = [ + ...frames, + { hash: location.hash, key: frameSeq++, node: content }, + ]; } - - let frame = document.createElement("div"); - frame.dataset.hash = location.hash; - stack.appendChild(frame); - createRoot(frame).render(content); + notifyFrames(); }; const App = () => { @@ -274,7 +309,7 @@ const App = () => { /> , ); - renderFrame({content}); + renderFrame(content); }; const reloadCache = async () => { @@ -421,8 +456,8 @@ const bootstrap = async () => { window.setUI = setUI; window.resetUI = () => { window.uiInitialized = false; - const frames = Array.from(stack.children); - frames.forEach((frame) => frame.remove()); + frames = []; + notifyFrames(); }; const futures = [reloadCache()]; diff --git a/webpack.config.js b/webpack.config.js index 709dd691..ce19cb56 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -73,46 +73,6 @@ module.exports = { extractComments: false, }), ], - splitChunks: { - chunks: "all", - cacheGroups: { - // Vendor chunk for node_modules - vendor: { - test: /[\\/]node_modules[\\/]/, - name: "vendors", - chunks: "all", - priority: 20, - minSize: 20000, - }, - // React-specific chunk - react: { - test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/, - name: "react", - chunks: "all", - priority: 30, - }, - // DFINITY/IC libraries - dfinity: { - test: /[\\/]node_modules[\\/]@dfinity[\\/]/, - name: "dfinity", - chunks: "all", - priority: 25, - }, - // App components in single chunk - appComponents: { - test: /[\\/]src[\\/]frontend[\\/]src[\\/](?!index\.tsx$).*\.tsx$/, - name: "app-components", - chunks: "all", - priority: 15, - }, - // Default chunk for remaining code - default: { - minChunks: 2, - priority: 1, - reuseExistingChunk: true, - }, - }, - }, }, resolve: { extensions: [".js", ".ts", ".jsx", ".tsx"], @@ -122,10 +82,7 @@ module.exports = { }, output: { filename: "[name].js", - chunkFilename: "[name].chunk.js", path: path.join(__dirname, "dist", frontendDirectory), - chunkFormat: "array-push", - crossOriginLoading: "anonymous", clean: true, }, module: {