From ae237ca0aa4bed21c943f92ea328fedcc25ab737 Mon Sep 17 00:00:00 2001 From: Jake Wang Date: Thu, 24 Sep 2026 03:26:23 -0400 Subject: [PATCH 1/7] fix core branching, imports, and v2 release readiness --- .github/workflows/ci.yml | 4 + .github/workflows/release.yml | 18 ++- CHANGELOG.md | 26 ++++ README.md | 4 +- VERSION | 2 +- api/go.mod | 6 +- api/main.go | 4 +- api/server/api_test.go | 2 +- api/server/branch_capture_test.go | 81 ++++++++++++ api/server/console.go | 4 +- api/server/demo_test.go | 2 +- api/server/router.go | 26 ++-- api/server/security_test.go | 2 +- cli/cmd/checkout.go | 2 +- cli/cmd/cli_test.go | 12 ++ cli/cmd/console.go | 2 +- cli/cmd/import.go | 55 +++++++- cli/cmd/mcp.go | 2 +- cli/cmd/pin.go | 2 +- cli/cmd/restore.go | 2 +- cli/cmd/root.go | 2 +- cli/cmd/services.go | 2 +- cli/go.mod | 8 +- cmd/compat-verify/main.go | 6 +- compat/run.sh | 2 +- docs/ARCHITECTURE.md | 8 +- docs/CLI.md | 10 +- docs/OPERATIONS.md | 38 ++++++ docs/PERFORMANCE.md | 16 ++- go.mod | 2 +- internal/branch/wal/service.go | 2 +- internal/checkout/service.go | 4 +- internal/gc/service.go | 6 +- internal/importer/service.go | 126 +++++++++++++----- internal/ingest/errors_test.go | 2 +- internal/ingest/service.go | 6 +- internal/materializer/keys_test.go | 2 +- internal/materializer/service.go | 2 +- internal/merge/keys_test.go | 2 +- internal/merge/service.go | 8 +- internal/migrate/service.go | 8 +- internal/pin/service.go | 2 +- internal/project/wal/import.go | 90 +++++++++++++ internal/project/wal/service.go | 10 +- internal/restore/service.go | 8 +- internal/sandbox/service.go | 6 +- internal/snapshot/auto.go | 2 +- internal/snapshot/codec.go | 4 +- internal/snapshot/service.go | 6 +- internal/timetravel/service.go | 4 +- internal/undo/service.go | 8 +- internal/wal/models.go | 1 + internal/wal/service.go | 29 +++-- internal/walwriter/writer.go | 6 +- internal/wireproxy/proxy.go | 2 +- npm/package.json | 2 +- pkg/mcpserver/server.go | 2 +- pkg/version/version.go | 4 +- pkg/walcli/services.go | 64 ++++------ publish-guides/README.md | 95 +++++++------- publish-guides/homebrew.md | 64 +++------- publish-guides/npm.md | 91 ++++--------- scripts/check-go-module.sh | 38 ++++++ scripts/release-version.js | 14 +- scripts/release-version.test.js | 8 ++ server.json | 4 +- tests/wal/capture_hardening_test.go | 4 +- tests/wal/checkout_test.go | 4 +- tests/wal/chunkstore_test.go | 10 +- tests/wal/compression_test.go | 2 +- tests/wal/determinism_property_test.go | 12 +- tests/wal/gc_test.go | 4 +- tests/wal/hardening_core_test.go | 12 +- tests/wal/import_recovery_test.go | 156 +++++++++++++++++++++++ tests/wal/import_test.go | 71 ++++++----- tests/wal/index_startup_test.go | 80 ++++++++++++ tests/wal/ingest_test.go | 8 +- tests/wal/integration_test.go | 14 +- tests/wal/materializer_test.go | 8 +- tests/wal/mcpserver_test.go | 20 +-- tests/wal/merge_test.go | 6 +- tests/wal/migrate_test.go | 8 +- tests/wal/performance_checks_test.go | 4 +- tests/wal/pin_test.go | 8 +- tests/wal/restore_test.go | 14 +- tests/wal/sandbox_test.go | 8 +- tests/wal/sequencer_test.go | 2 +- tests/wal/snapshot_auto_test.go | 4 +- tests/wal/snapshot_shutdown_test.go | 6 +- tests/wal/snapshot_test.go | 14 +- tests/wal/timetravel_performance_test.go | 12 +- tests/wal/timetravel_test.go | 12 +- tests/wal/undo_hardening_test.go | 6 +- tests/wal/undo_test.go | 4 +- tests/wal/wal_comprehensive_test.go | 6 +- tests/wal/wal_core_test.go | 6 +- tests/wal/wal_stress_test.go | 4 +- tests/wal/wireproxy_test.go | 4 +- tests/wal/write_performance_test.go | 8 +- 99 files changed, 1125 insertions(+), 500 deletions(-) create mode 100644 api/server/branch_capture_test.go create mode 100644 internal/project/wal/import.go create mode 100644 scripts/check-go-module.sh create mode 100644 tests/wal/import_recovery_test.go create mode 100644 tests/wal/index_startup_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00224b7..064db0d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,7 @@ name: CI on: + workflow_call: push: branches: [ master, main ] pull_request: @@ -47,6 +48,9 @@ jobs: - name: Download dependencies run: go mod download + - name: Compile an external Go v2 consumer + run: bash scripts/check-go-module.sh + - uses: actions/setup-node@v4 with: node-version: '22' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 242c795..1ee63eb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,6 +12,10 @@ concurrency: cancel-in-progress: false jobs: + validate: + name: Validate the tagged source + uses: ./.github/workflows/ci.yml + prepare: name: Validate release version runs-on: ubuntu-latest @@ -19,6 +23,8 @@ jobs: version: ${{ steps.version.outputs.version }} steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - uses: actions/setup-node@v4 with: node-version: '22' @@ -27,15 +33,17 @@ jobs: env: RELEASE_TAG: ${{ github.ref_name }} run: | - version=$(node scripts/release-version.js "$RELEASE_TAG") + version=$(node scripts/release-version.js "$RELEASE_TAG" --check) echo "version=$version" >> "$GITHUB_OUTPUT" - node scripts/release-version.js --check + # Nested Go modules need their own semantic-version tag. Both tags + # must identify the reviewed source that the reusable CI validates. + test "$(git rev-parse "refs/tags/api/v${version}^{commit}")" = "$(git rev-parse HEAD)" - name: Check release tooling run: node --test scripts/release-version.test.js npm/scripts/install.test.js build: name: Build binaries - needs: prepare + needs: [prepare, validate] runs-on: ubuntu-latest strategy: matrix: @@ -65,7 +73,7 @@ jobs: mkdir -p ../dist asset="argon-${GOOS}-${GOARCH}" if [ "$GOOS" = windows ]; then asset="$asset.exe"; fi - go build -trimpath -ldflags="-s -w -X github.com/argon-lab/argon/pkg/version.Build=$RELEASE_VERSION" -o "../dist/$asset" . + go build -trimpath -ldflags="-s -w -X github.com/argon-lab/argon/v2/pkg/version.Build=$RELEASE_VERSION" -o "../dist/$asset" . - uses: actions/upload-artifact@v4 with: name: binaries-${{ matrix.goos }}-${{ matrix.goarch }} @@ -74,7 +82,7 @@ jobs: release: name: Create release - needs: [prepare, build] + needs: [prepare, validate, build] runs-on: ubuntu-latest permissions: contents: write diff --git a/CHANGELOG.md b/CHANGELOG.md index 94cdf0b..4d728d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,32 @@ Planned: GCS chunk-store backend; an opt-in read-your-writes barrier in the wire proxy (hold a write's ack until the ingester confirms the WAL entry — a synchronization barrier, not in-proxy capture). +## [2.1.2] - 2026-09-24 + +### Fixed + +- REST branch creation drains the live parent's capture before fixing its fork + point and refuses degraded history; omitted `from` defaults to `main`. +- Concurrent engine/CLI startup preserves the current unique WAL index instead + of dropping and rebuilding it on every connection. Legacy definitions still + migrate when needed. +- Go consumers use valid `github.com/argon-lab/argon/v2` and + `github.com/argon-lab/argon/api/v2` modules, with separate matching release tags. + +### Import and release hardening + +- Real imports require `--source-quiesced`: stop source writes and DDL for the + entire copy. Preflight rejects unreadable/unsupported collections before + creating target data. Imports remain hidden until successful completion. +- Failed or canceled imports remove their unpublished project/history so the + name can be retried. `argon import cleanup --project P --yes` recovers an + unfinished import after a killed process; completed projects are protected. +- Release publication depends on the complete CI suite for the exact tagged + source, consistent committed version metadata and matching root/API tags. + CI also compiles an external Go consumer of both v2 modules. +- Publishing and performance documentation now describes the actual release + flow, source-quiescence contract and completed small-scale benchmark evidence. + ## [2.1.1] - 2026-09-07 ### Fixed diff --git a/README.md b/README.md index 4bdc4a9..1e106e6 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,8 @@ main ──branch──▶ experiment ──checkout──▶ mongodb://… ← ``` ```bash -# 0 · Bring your data in ("git clone") — or: argon projects create myapp -argon import database --uri mongodb://localhost:27017 --database myapp --project myapp +# 0 · Stop source writers and DDL for the import — or: argon projects create myapp +argon import database --uri mongodb://localhost:27017 --database myapp --project myapp --source-quiesced # 1 · Branch — instant, no copy argon branches create experiment -p myapp diff --git a/VERSION b/VERSION index 3e3c2f1..eca07e4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.1.1 +2.1.2 diff --git a/api/go.mod b/api/go.mod index 0e89aa3..e2fc24d 100644 --- a/api/go.mod +++ b/api/go.mod @@ -1,15 +1,15 @@ -module github.com/argon-lab/argon/api +module github.com/argon-lab/argon/api/v2 go 1.26.6 require ( - github.com/argon-lab/argon v0.0.0 + github.com/argon-lab/argon/v2 v2.1.2 github.com/gin-gonic/gin v1.9.1 github.com/stretchr/testify v1.9.0 go.mongodb.org/mongo-driver v1.17.7 ) -replace github.com/argon-lab/argon => ../ +replace github.com/argon-lab/argon/v2 => ../ require ( github.com/aws/aws-sdk-go-v2 v1.42.1 // indirect diff --git a/api/main.go b/api/main.go index 6356bea..fc6e500 100644 --- a/api/main.go +++ b/api/main.go @@ -19,8 +19,8 @@ import ( "syscall" "time" - "github.com/argon-lab/argon/api/server" - "github.com/argon-lab/argon/pkg/walcli" + "github.com/argon-lab/argon/api/v2/server" + "github.com/argon-lab/argon/v2/pkg/walcli" ) func main() { diff --git a/api/server/api_test.go b/api/server/api_test.go index 2457640..0b4ef7c 100644 --- a/api/server/api_test.go +++ b/api/server/api_test.go @@ -10,7 +10,7 @@ import ( "testing" "time" - "github.com/argon-lab/argon/pkg/walcli" + "github.com/argon-lab/argon/v2/pkg/walcli" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" diff --git a/api/server/branch_capture_test.go b/api/server/branch_capture_test.go new file mode 100644 index 0000000..4959a8b --- /dev/null +++ b/api/server/branch_capture_test.go @@ -0,0 +1,81 @@ +package server + +import ( + "context" + "fmt" + "net/http" + "testing" + "time" + + "github.com/argon-lab/argon/v2/pkg/walcli" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson" +) + +func TestAPI_CreateBranchDrainsNativeParent(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 40*time.Second) + defer cancel() + dbName := fmt.Sprintf("argon_api_fork_%d", time.Now().UnixNano()) + services, err := walcli.NewServicesAt("mongodb://localhost:27017", dbName) + require.NoError(t, err) + router := NewRouterWith(services, Options{}) + t.Cleanup(func() { + router.Shutdown() + projects, _ := services.Projects.ListProjects() + for _, project := range projects { + branches, _ := services.Branches.ListBranches(project.ID) + for _, branch := range branches { + if branch.PhysicalDB != "" { + _ = services.Client.Database(branch.PhysicalDB).Drop(context.Background()) + } + } + } + _ = services.Client.Database(dbName).Drop(context.Background()) + _ = services.Client.Disconnect(context.Background()) + }) + code, _ := do(t, router, "POST", "/api/v1/projects", map[string]string{"name": "fork"}) + require.Equal(t, http.StatusCreated, code) + code, _ = do(t, router, "POST", "/api/v1/projects/fork/branches/main/checkout", nil) + require.Equal(t, http.StatusOK, code) + projectID := mustProjectID(t, services, "fork") + parent, err := services.Branches.GetBranch(projectID, "main") + require.NoError(t, err) + + // Stop capture to make the otherwise timing-dependent gap deterministic. + // The REST fork must restart/drain it, including a completed native write. + require.NoError(t, services.Ingest.Stop(ctx, parent.ID)) + physical := services.Client.Database(parent.PhysicalDB) + _, err = physical.Collection("notes").InsertOne(ctx, bson.M{"_id": "pending", "value": "must inherit"}) + require.NoError(t, err) + before, err := services.Branches.GetBranchByID(parent.ID) + require.NoError(t, err) + code, response := do(t, router, "POST", "/api/v1/projects/fork/branches", map[string]string{"name": "child"}) + require.Equal(t, http.StatusCreated, code, "%v", response) + child, err := services.Branches.GetBranch(projectID, "child") + require.NoError(t, err) + require.Equal(t, parent.ID, child.ParentID, "omitted from defaults to main") + require.Greater(t, child.BaseLSN, before.HeadLSN) + code, response = do(t, router, "POST", "/api/v1/projects/fork/branches/child/checkout", nil) + require.Equal(t, http.StatusOK, code, "%v", response) + child, err = services.Branches.GetBranchByID(child.ID) + require.NoError(t, err) + var inherited bson.M + require.NoError(t, services.Client.Database(child.PhysicalDB).Collection("notes").FindOne(ctx, bson.M{"_id": "pending"}).Decode(&inherited)) + require.Equal(t, "must inherit", inherited["value"]) + + // A DDL gap cannot be made durable by draining. It must refuse the fork, + // rather than creating an apparently valid branch at an incomplete head. + require.NoError(t, physical.Collection("notes").Drop(ctx)) + require.Eventually(t, func() bool { + for _, status := range services.Ingest.Statuses() { + if status.BranchID == parent.ID && status.State == "degraded" { + return true + } + } + return false + }, 10*time.Second, 20*time.Millisecond) + code, response = do(t, router, "POST", "/api/v1/projects/fork/branches", map[string]string{"name": "unsafe", "from": "main"}) + require.Equal(t, http.StatusServiceUnavailable, code, "%v", response) + _, err = services.Branches.GetBranch(projectID, "unsafe") + require.Error(t, err) +} diff --git a/api/server/console.go b/api/server/console.go index 3e2b13c..74b3dab 100644 --- a/api/server/console.go +++ b/api/server/console.go @@ -8,7 +8,7 @@ package server import ( "crypto/subtle" "fmt" - "github.com/argon-lab/argon/pkg/version" + "github.com/argon-lab/argon/v2/pkg/version" "net" "net/http" "net/url" @@ -25,7 +25,7 @@ import ( ) // Version shares the embedded release metadata with the CLI. Release builds -// override github.com/argon-lab/argon/pkg/version.Build via -ldflags. +// override github.com/argon-lab/argon/v2/pkg/version.Build via -ldflags. var Version = version.String() // Options are the cross-cutting server settings. The zero value is an open diff --git a/api/server/demo_test.go b/api/server/demo_test.go index 86d2808..e8f0dd4 100644 --- a/api/server/demo_test.go +++ b/api/server/demo_test.go @@ -10,7 +10,7 @@ import ( "testing" "time" - "github.com/argon-lab/argon/pkg/walcli" + "github.com/argon-lab/argon/v2/pkg/walcli" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/api/server/router.go b/api/server/router.go index ce56005..6c3e0d1 100644 --- a/api/server/router.go +++ b/api/server/router.go @@ -17,7 +17,7 @@ import ( "sync" "time" - "github.com/argon-lab/argon/pkg/walcli" + "github.com/argon-lab/argon/v2/pkg/walcli" "github.com/gin-gonic/gin" "go.mongodb.org/mongo-driver/bson/primitive" ) @@ -305,16 +305,22 @@ func (r *Router) createBranch(c *gin.Context) { abortErr(c, http.StatusBadRequest, err) return } - parentID := "" - if body.From != "" { - parent, err := r.services.Branches.GetBranch(projectID, body.From) - if err != nil { - abortErr(c, http.StatusNotFound, fmt.Errorf("parent branch %q not found", body.From)) - return - } - parentID = parent.ID + if body.From == "" { + body.From = "main" + } + parent, err := r.services.Branches.GetBranch(projectID, body.From) + if err != nil { + abortErr(c, http.StatusNotFound, fmt.Errorf("parent branch %q not found", body.From)) + return + } + // The fork point is the parent's durable WAL head. A native write may + // already be acknowledged while capture is behind, so drain before + // CreateBranch reloads and fixes that head for the child's lifetime. + if err := r.services.SyncBranch(c.Request.Context(), parent.ID); err != nil { + abortErr(c, http.StatusServiceUnavailable, err) + return } - branch, err := r.services.Branches.CreateBranch(projectID, body.Name, parentID) + branch, err := r.services.Branches.CreateBranch(projectID, body.Name, parent.ID) if err != nil { abortErr(c, http.StatusConflict, err) return diff --git a/api/server/security_test.go b/api/server/security_test.go index ed4c841..d677b7b 100644 --- a/api/server/security_test.go +++ b/api/server/security_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "github.com/argon-lab/argon/pkg/walcli" + "github.com/argon-lab/argon/v2/pkg/walcli" "github.com/gin-gonic/gin" "github.com/stretchr/testify/require" ) diff --git a/cli/cmd/checkout.go b/cli/cmd/checkout.go index 3f0542d..d8c6360 100644 --- a/cli/cmd/checkout.go +++ b/cli/cmd/checkout.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/argon-lab/argon/pkg/walcli" + "github.com/argon-lab/argon/v2/pkg/walcli" "github.com/spf13/cobra" ) diff --git a/cli/cmd/cli_test.go b/cli/cmd/cli_test.go index 2703cf5..66a61c5 100644 --- a/cli/cmd/cli_test.go +++ b/cli/cmd/cli_test.go @@ -83,6 +83,18 @@ func TestCLIStatusConnectionFailureIsJSONAndNonzero(t *testing.T) { } } +func TestCLIImportRequiresStoppedSourceBeforeConnecting(t *testing.T) { + _, stderr, err := invokeCLI(t, "mongodb://127.0.0.1:1", "import", "database", + "--uri", "mongodb://127.0.0.1:1", "--database", "source", "--project", "target", "--yes") + if err == nil || !strings.Contains(stderr, "--source-quiesced") { + t.Fatalf("import must reject a missing source acknowledgment before connecting: %v %s", err, stderr) + } + _, stderr, err = invokeCLI(t, "mongodb://127.0.0.1:1", "import", "cleanup", "--project", "target") + if err == nil || !strings.Contains(stderr, "stop the original importer") { + t.Fatalf("cleanup must require stopped-importer confirmation: %v %s", err, stderr) + } +} + func TestCLIPinnedWorkflowJSON(t *testing.T) { uri := os.Getenv("ARGON_TEST_MONGODB_URI") if uri == "" { diff --git a/cli/cmd/console.go b/cli/cmd/console.go index 0632cd9..427ae52 100644 --- a/cli/cmd/console.go +++ b/cli/cmd/console.go @@ -11,7 +11,7 @@ import ( "syscall" "time" - "github.com/argon-lab/argon/api/server" + "github.com/argon-lab/argon/api/v2/server" "github.com/spf13/cobra" ) diff --git a/cli/cmd/import.go b/cli/cmd/import.go index 4b26497..3dc2274 100644 --- a/cli/cmd/import.go +++ b/cli/cmd/import.go @@ -5,6 +5,8 @@ import ( "encoding/json" "fmt" "os" + "os/signal" + "syscall" "time" "github.com/spf13/cobra" @@ -60,7 +62,8 @@ time travel, branching, and other Argon features on your existing data. Available subcommands: preview - Preview what would be imported database - Import an existing MongoDB database - status - Check status of import operations`, + status - Check status of import operations + cleanup - Recover an unpublished import after its process stopped`, } // importPreviewCmd previews what would be imported @@ -125,16 +128,23 @@ var importDatabaseCmd = &cobra.Command{ This creates a new Argon project and imports all data from the source database, enabling time travel and branching capabilities. +Stop source application writers and DDL first, keep them stopped until this +command finishes, and acknowledge this with --source-quiesced. This copies +ordinary documents; it is not a consistent online clone and does not copy +indexes or collection options. Failed imports are cleaned up automatically. + Example: - argon import database --uri "mongodb://localhost:27017" --database "myapp" --project "imported-myapp"`, + argon import database --uri "mongodb://localhost:27017" --database "myapp" --project "imported-myapp" --source-quiesced`, RunE: func(cmd *cobra.Command, args []string) error { - ctx := context.Background() + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM) + defer stop() // Get flags mongoURI, _ := cmd.Flags().GetString("uri") databaseName, _ := cmd.Flags().GetString("database") projectName, _ := cmd.Flags().GetString("project") dryRun, _ := cmd.Flags().GetBool("dry-run") + sourceQuiesced, _ := cmd.Flags().GetBool("source-quiesced") batchSize, _ := cmd.Flags().GetInt("batch-size") outputFormat, _ := cmd.Flags().GetString("output") @@ -147,6 +157,9 @@ Example: if projectName == "" { return fmt.Errorf("--project flag is required") } + if !dryRun && !sourceQuiesced { + return fmt.Errorf("pause source writes and DDL, then pass --source-quiesced; --yes only confirms creating the target project") + } // Initialize services services, err := newCommandServices(cmd) @@ -188,7 +201,7 @@ Example: fmt.Fprintln(cmd.ErrOrStderr(), " (DRY RUN - no changes will be made)") } - resultData, err := services.ImportDatabase(ctx, opts.MongoURI, opts.DatabaseName, opts.ProjectName, opts.DryRun, opts.BatchSize) + resultData, err := services.ImportDatabase(ctx, opts.MongoURI, opts.DatabaseName, opts.ProjectName, opts.DryRun, opts.BatchSize, sourceQuiesced) if err != nil { return fmt.Errorf("failed to import database: %w", err) } @@ -206,6 +219,33 @@ Example: }, } +var importCleanupCmd = &cobra.Command{ + Use: "cleanup", + Short: "Remove an unfinished import after its process has stopped", + Long: `Recover a project name left reserved by a killed or crashed import. +Stop the original importer before running this command with --yes. +Only unpublished import data is removed; completed projects are refused.`, + RunE: func(cmd *cobra.Command, args []string) error { + confirmed, _ := cmd.Flags().GetBool("yes") + if !confirmed { + return fmt.Errorf("stop the original importer, then pass --yes to remove its unfinished data") + } + name, _ := cmd.Flags().GetString("project") + services, err := newCommandServices(cmd) + if err != nil { + return err + } + if err := services.Projects.CleanupImport(cmd.Context(), name); err != nil { + return err + } + if jsonOutput(cmd) { + return writeJSON(cmd, map[string]any{"project": name, "cleaned": true}) + } + fmt.Fprintf(cmd.OutOrStdout(), "Removed unfinished import %q; the name can be reused.\n", name) + return nil + }, +} + // importStatusCmd shows the status of import operations var importStatusCmd = &cobra.Command{ Use: "status", @@ -269,6 +309,7 @@ func init() { importCmd.AddCommand(importPreviewCmd) importCmd.AddCommand(importDatabaseCmd) importCmd.AddCommand(importStatusCmd) + importCmd.AddCommand(importCleanupCmd) // Preview command flags importPreviewCmd.Flags().StringP("uri", "u", "", "MongoDB connection URI (required)") @@ -282,6 +323,7 @@ func init() { importDatabaseCmd.Flags().StringP("database", "d", "", "Database name to import (required)") importDatabaseCmd.Flags().StringP("project", "p", "", "Argon project name to create (required)") importDatabaseCmd.Flags().Bool("dry-run", false, "Preview import without making changes") + importDatabaseCmd.Flags().Bool("source-quiesced", false, "Confirm source writers and DDL are paused until the import finishes") importDatabaseCmd.Flags().BoolP("yes", "y", false, "Skip the confirmation prompt (required when stdin is not a terminal)") importDatabaseCmd.Flags().Int("batch-size", 1000, "Number of documents to process in each batch") importDatabaseCmd.Flags().StringP("output", "o", "table", "Output format: table, json") @@ -292,6 +334,9 @@ func init() { // Status command flags importStatusCmd.Flags().StringP("project", "p", "", "Project name to check status (required)") _ = importStatusCmd.MarkFlagRequired("project") + importCleanupCmd.Flags().StringP("project", "p", "", "Unfinished import project name") + importCleanupCmd.Flags().Bool("yes", false, "Confirm the original importer is stopped and remove its unfinished data") + _ = importCleanupCmd.MarkFlagRequired("project") } // printImportPreview prints the import preview in a user-friendly format @@ -319,7 +364,7 @@ func printImportPreview(preview *ImportPreview) error { } fmt.Printf("💡 Next steps:\n") - fmt.Printf(" Run import: argon import database --uri --database %s --project \n", preview.DatabaseName) + fmt.Printf(" Stop source writers, then run: argon import database --uri --database %s --project --source-quiesced\n", preview.DatabaseName) return nil } diff --git a/cli/cmd/mcp.go b/cli/cmd/mcp.go index c7d0333..7bd1d87 100644 --- a/cli/cmd/mcp.go +++ b/cli/cmd/mcp.go @@ -5,7 +5,7 @@ import ( "fmt" "os" - "github.com/argon-lab/argon/pkg/mcpserver" + "github.com/argon-lab/argon/v2/pkg/mcpserver" "github.com/spf13/cobra" ) diff --git a/cli/cmd/pin.go b/cli/cmd/pin.go index 6491446..101d2e3 100644 --- a/cli/cmd/pin.go +++ b/cli/cmd/pin.go @@ -7,7 +7,7 @@ import ( "fmt" "time" - "github.com/argon-lab/argon/pkg/walcli" + "github.com/argon-lab/argon/v2/pkg/walcli" "github.com/spf13/cobra" ) diff --git a/cli/cmd/restore.go b/cli/cmd/restore.go index a0b50ba..eecaea2 100644 --- a/cli/cmd/restore.go +++ b/cli/cmd/restore.go @@ -4,7 +4,7 @@ import ( "fmt" "time" - "github.com/argon-lab/argon/pkg/walcli" + "github.com/argon-lab/argon/v2/pkg/walcli" "github.com/spf13/cobra" ) diff --git a/cli/cmd/root.go b/cli/cmd/root.go index efe34c8..487e5e1 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -4,7 +4,7 @@ import ( "errors" "fmt" - "github.com/argon-lab/argon/pkg/version" + "github.com/argon-lab/argon/v2/pkg/version" "github.com/spf13/cobra" ) diff --git a/cli/cmd/services.go b/cli/cmd/services.go index b306f2e..0dc5cad 100644 --- a/cli/cmd/services.go +++ b/cli/cmd/services.go @@ -5,7 +5,7 @@ import ( "errors" "time" - "github.com/argon-lab/argon/pkg/walcli" + "github.com/argon-lab/argon/v2/pkg/walcli" "github.com/spf13/cobra" ) diff --git a/cli/go.mod b/cli/go.mod index 55c0ab1..8595537 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -3,7 +3,7 @@ module argon-cli go 1.26.6 require ( - github.com/argon-lab/argon v0.0.0 + github.com/argon-lab/argon/v2 v2.1.2 github.com/spf13/cobra v1.9.1 ) @@ -32,10 +32,10 @@ require ( google.golang.org/protobuf v1.36.1 // indirect ) -replace github.com/argon-lab/argon => ../ +replace github.com/argon-lab/argon/v2 => ../ require ( - github.com/argon-lab/argon/api v0.0.0 + github.com/argon-lab/argon/api/v2 v2.1.2 github.com/aws/aws-sdk-go-v2 v1.42.1 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14 // indirect github.com/aws/aws-sdk-go-v2/config v1.32.28 // indirect @@ -72,4 +72,4 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect ) -replace github.com/argon-lab/argon/api => ../api +replace github.com/argon-lab/argon/api/v2 => ../api diff --git a/cmd/compat-verify/main.go b/cmd/compat-verify/main.go index 08f0551..89f861b 100644 --- a/cmd/compat-verify/main.go +++ b/cmd/compat-verify/main.go @@ -11,9 +11,9 @@ import ( "os" "time" - "github.com/argon-lab/argon/internal/mongoexpr" - "github.com/argon-lab/argon/internal/wal" - "github.com/argon-lab/argon/pkg/walcli" + "github.com/argon-lab/argon/v2/internal/mongoexpr" + "github.com/argon-lab/argon/v2/internal/wal" + "github.com/argon-lab/argon/v2/pkg/walcli" "go.mongodb.org/mongo-driver/bson" ) diff --git a/compat/run.sh b/compat/run.sh index 1a7d68c..9c287f7 100755 --- a/compat/run.sh +++ b/compat/run.sh @@ -10,7 +10,7 @@ cd "$(dirname "$0")/.." STAMP=$(date +%s) echo "=== building tools ===" -go build -o /tmp/argonctl ./cli 2>/dev/null || (cd cli && go build -o /tmp/argonctl .) +(cd cli && go build -o /tmp/argonctl .) go build -o /tmp/compat-verify ./cmd/compat-verify run_driver() { diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d643c16..afabece 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -359,9 +359,11 @@ Current limitations (deliberate scope): snapshots cover them; snapshot chunks can additionally live in an S3-compatible or filesystem chunk store (see "Chunk store backends" above). GCS is not yet a backend. -- Published benchmarks still need native capture overhead, end-to-end sandbox - readiness and divergence storage growth. The public `WriterFor` write API - exists; the old public-writer issue is no longer a blocker. +- The published small-scale benchmark matrix includes native capture delay, + end-to-end sandbox readiness and divergence storage categories. Expanded + dataset/concurrency/depth matrices and longer recovery runs remain separate + evidence: do not generalize small runs or twenty-sample p99 values into + production latency guarantees. The public `WriterFor` API is available. Performance characteristics are measured by the public benchmark suite at https://github.com/argon-lab/benchmarks — reproducible with diff --git a/docs/CLI.md b/docs/CLI.md index 459117c..0589ae0 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -42,11 +42,19 @@ truth. ``` argon import preview --uri U --database D -argon import database --uri U --database D --project P [--dry-run] [--yes] +argon import database --uri U --database D --project P --source-quiesced [--yes] +argon import database --uri U --database D --project P --dry-run +argon import cleanup --project P --yes # only after stopping a crashed importer argon import status ``` Imports auto-snapshot, so reads never replay the whole import. +Pause source writers and DDL throughout a real import; `--yes` does not replace +`--source-quiesced`. Source preflight checks collection types/readability. The +target stays unpublished until copying succeeds; ordinary failures clean up +the reserved project and history. After a hard kill, stop the old process and +use `import cleanup` before retrying the same name. Completed projects cannot +be removed by cleanup. See [the import contract](OPERATIONS.md#importing-existing-data). ## History: time travel, undo, restore diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 9f8b320..a206a3b 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -45,6 +45,44 @@ migration. Everything here reflects what the code does today. | `argon sandbox sweep -p P` | schedule for CLI-only use | reap expired sandboxes (pinned ones are skipped loudly) | | `argon gc -p P` | cron | reclaim covered, out-of-retention WAL entries | +## Importing existing data + +`argon import database` copies ordinary documents with sequential cursor reads. +Stop all source application writes, jobs and DDL before starting, and keep them +stopped until completion. Pass `--source-quiesced` to acknowledge that contract; +the command cannot prove that an external writer is paused. `--yes` only skips +the target-creation confirmation. If writers resume during copying, consistency +is outside the contract: discard that result and repeat from a quiesced source. +This is not an online transactional clone or continuous source synchronization. +Indexes, validators and collection options are not copied. + +```sh +argon import preview --uri "$SOURCE_MONGODB_URI" --database myapp +# Pause source writers and DDL now. +argon import database --uri "$SOURCE_MONGODB_URI" --database myapp --project myapp --source-quiesced --yes +# Source writers may resume after success. +``` + +Preflight verifies readable ordinary collections before creating the target; +views/time-series collections and an absent/no-collection source are rejected. +The reserved target is hidden from normal project lookup/listing until every +collection succeeds. A handled error or cancellation removes that unpublished +project, its branches, WAL and counter transactionally, so retrying its name +starts cleanly. Cleanup uses a separate context when the copy was canceled. + +If the process is killed, or the metadata database was unavailable during +cleanup, stop the original importer and recover explicitly: + +```sh +argon import cleanup --project myapp --yes +``` + +This command accepts only an unfinished import; it refuses ordinary or completed +projects. Do not run recovery concurrently with the original importer. Imports +restart from the beginning; source snapshot/catch-up and resumable copies remain +future capabilities. Destination metadata needs the normal writable replica set +for transactional recovery, even when the quiesced source is standalone. + ## Snapshot chunk stores Snapshots are content-addressed, zstd-compressed chunks (~4 MB), diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index a3e83ea..90b9317 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -27,10 +27,18 @@ checkout, time to connect, first read/write, or native capture overhead. Snapshot/time-travel samples in that baseline do not establish p95/p99 read latency. The recorded engine revision must accompany reused figures. -Still required: end-to-end sandbox startup, native write/capture latency, -write throughput, divergence storage growth, multiple dataset sizes and -concurrency levels. These workloads can use the public `WriterFor` API and -managed capture; they are no longer blocked on the old public-writer issue. +The later committed-engine matrix also measures end-to-end sandbox readiness, +first query, native acknowledgment-to-capture delay, throughput, storage +categories and small ancestry/concurrency cases. Inspect each run's exact +engine source and raw samples in RESULTS.md; these are measurements for the +recorded environment, not an SLA. A p99 computed from twenty observations +does not establish production tail latency. + +Expanded dataset/concurrency/depth runs and longer recovery/soak runs need +their own published evidence before drawing scale conclusions. Use the +benchmark repository's current results and experiment plan to distinguish +completed runs from runnable workloads. The public `WriterFor` API and managed +capture already support these workloads. If you publish an Argon number anywhere — a README, a blog post, a talk — it must come from a linked RESULTS.md run. Regressions the local diff --git a/go.mod b/go.mod index bfb02d3..2628677 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/argon-lab/argon +module github.com/argon-lab/argon/v2 go 1.26.6 diff --git a/internal/branch/wal/service.go b/internal/branch/wal/service.go index 6d2e502..68b8fca 100644 --- a/internal/branch/wal/service.go +++ b/internal/branch/wal/service.go @@ -6,7 +6,7 @@ import ( "fmt" "time" - "github.com/argon-lab/argon/internal/wal" + "github.com/argon-lab/argon/v2/internal/wal" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" diff --git a/internal/checkout/service.go b/internal/checkout/service.go index fad88e9..c9889d7 100644 --- a/internal/checkout/service.go +++ b/internal/checkout/service.go @@ -15,8 +15,8 @@ import ( "strings" "sync" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - "github.com/argon-lab/argon/internal/materializer" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + "github.com/argon-lab/argon/v2/internal/materializer" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" diff --git a/internal/gc/service.go b/internal/gc/service.go index c601c08..8a4cd67 100644 --- a/internal/gc/service.go +++ b/internal/gc/service.go @@ -36,9 +36,9 @@ import ( "math" "time" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - "github.com/argon-lab/argon/internal/snapshot" - "github.com/argon-lab/argon/internal/wal" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + "github.com/argon-lab/argon/v2/internal/snapshot" + "github.com/argon-lab/argon/v2/internal/wal" ) // Config tunes garbage collection. diff --git a/internal/importer/service.go b/internal/importer/service.go index 3edd7ee..77e6b92 100644 --- a/internal/importer/service.go +++ b/internal/importer/service.go @@ -2,16 +2,18 @@ package importer import ( "context" + "errors" "fmt" + "sort" "time" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - projectwal "github.com/argon-lab/argon/internal/project/wal" - "github.com/argon-lab/argon/internal/wal" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + projectwal "github.com/argon-lab/argon/v2/internal/project/wal" + "github.com/argon-lab/argon/v2/internal/wal" ) // ImportService handles importing existing MongoDB databases into Argon WAL system @@ -35,11 +37,11 @@ func (s *ImportService) SetImportedHook(hook func(branch *wal.Branch)) { // ImportPreview contains information about what would be imported type ImportPreview struct { - DatabaseName string `json:"database_name"` - Collections []CollectionInfo `json:"collections"` - TotalDocuments int64 `json:"total_documents"` - EstimatedSize int64 `json:"estimated_size_bytes"` - EstimatedWALEntries int64 `json:"estimated_wal_entries"` + DatabaseName string `json:"database_name"` + Collections []CollectionInfo `json:"collections"` + TotalDocuments int64 `json:"total_documents"` + EstimatedSize int64 `json:"estimated_size_bytes"` + EstimatedWALEntries int64 `json:"estimated_wal_entries"` } // CollectionInfo contains details about a collection to be imported @@ -57,18 +59,21 @@ type ImportOptions struct { ProjectName string `json:"project_name"` DryRun bool `json:"dry_run"` BatchSize int `json:"batch_size"` + // SourceQuiesced confirms application writers and DDL are stopped for + // the entire import. Ordinary cursor reads are not an online snapshot. + SourceQuiesced bool `json:"source_quiesced"` } // ImportResult contains the result of an import operation type ImportResult struct { - ProjectID string `json:"project_id"` - BranchID string `json:"branch_id"` - ImportedDocs int64 `json:"imported_documents"` - WALEntries int64 `json:"wal_entries_created"` - Collections []string `json:"imported_collections"` - Duration time.Duration `json:"duration"` - StartLSN int64 `json:"start_lsn"` - EndLSN int64 `json:"end_lsn"` + ProjectID string `json:"project_id"` + BranchID string `json:"branch_id"` + ImportedDocs int64 `json:"imported_documents"` + WALEntries int64 `json:"wal_entries_created"` + Collections []string `json:"imported_collections"` + Duration time.Duration `json:"duration"` + StartLSN int64 `json:"start_lsn"` + EndLSN int64 `json:"end_lsn"` } // NewImportService creates a new import service @@ -95,7 +100,7 @@ func (s *ImportService) PreviewImport(ctx context.Context, mongoURI, databaseNam } db := client.Database(databaseName) - + // List all collections collectionNames, err := db.ListCollectionNames(ctx, bson.D{}) if err != nil { @@ -115,7 +120,7 @@ func (s *ImportService) PreviewImport(ctx context.Context, mongoURI, databaseNam } collection := db.Collection(collName) - + // Get document count docCount, err := collection.EstimatedDocumentCount(ctx) if err != nil { @@ -127,7 +132,7 @@ func (s *ImportService) PreviewImport(ctx context.Context, mongoURI, databaseNam err = db.RunCommand(ctx, bson.D{ {Key: "collStats", Value: collName}, }).Decode(&stats) - + sizeBytes := int64(0) if err == nil { if size, ok := stats["size"].(int32); ok { @@ -167,7 +172,7 @@ func (s *ImportService) PreviewImport(ctx context.Context, mongoURI, databaseNam } // ImportDatabase imports an existing MongoDB database into Argon WAL system -func (s *ImportService) ImportDatabase(ctx context.Context, opts ImportOptions) (*ImportResult, error) { +func (s *ImportService) ImportDatabase(ctx context.Context, opts ImportOptions) (_ *ImportResult, err error) { startTime := time.Now() // Validate options @@ -192,21 +197,37 @@ func (s *ImportService) ImportDatabase(ctx context.Context, opts ImportOptions) } sourceDB := sourceClient.Database(opts.DatabaseName) - - // Check if project already exists + // Preserve the actionable target-name error before scanning source + // collections or creating target metadata. existingProject, err := s.projectService.GetProjectByName(opts.ProjectName) if err == nil && existingProject != nil { return nil, fmt.Errorf("project '%s' already exists", opts.ProjectName) } + // Check collection types and read permissions before reserving a target + // project. Views/time-series options cannot be reproduced by this importer. + collectionNames, err := importCollections(ctx, sourceDB) + if err != nil { + return nil, err + } // Create new project if not in dry run mode var project *wal.Project var branch *wal.Branch if !opts.DryRun { - project, err = s.projectService.CreateProject(opts.ProjectName) + project, err = s.projectService.BeginImport(ctx, opts.ProjectName) if err != nil { return nil, fmt.Errorf("failed to create project: %w", err) } + defer func() { + if err == nil { + return + } + cleanup, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + if cleanupErr := s.projectService.AbortImport(cleanup, project.ID); cleanupErr != nil { + err = errors.Join(err, fmt.Errorf("cleanup incomplete for %q; stop this importer, then run argon import cleanup --project %q --yes: %w", opts.ProjectName, opts.ProjectName, cleanupErr)) + } + }() // Get the default main branch branch, err = s.branchService.GetBranch(project.ID, "main") @@ -215,12 +236,6 @@ func (s *ImportService) ImportDatabase(ctx context.Context, opts ImportOptions) } } - // Get list of collections to import - collectionNames, err := sourceDB.ListCollectionNames(ctx, bson.D{}) - if err != nil { - return nil, fmt.Errorf("failed to list collections: %w", err) - } - result := &ImportResult{ Collections: make([]string, 0), Duration: 0, @@ -262,6 +277,12 @@ func (s *ImportService) ImportDatabase(ctx context.Context, opts ImportOptions) if !opts.DryRun { result.EndLSN = s.walService.GetCurrentLSN(project.ID) + if err := ctx.Err(); err != nil { + return nil, err + } + if err := s.projectService.CompleteImport(ctx, project.ID); err != nil { + return nil, fmt.Errorf("failed to publish imported project: %w", err) + } if s.onImported != nil { branch, err := s.branchService.GetBranchByID(branch.ID) if err == nil { @@ -274,6 +295,43 @@ func (s *ImportService) ImportDatabase(ctx context.Context, opts ImportOptions) return result, nil } +func importCollections(ctx context.Context, db *mongo.Database) ([]string, error) { + cursor, err := db.ListCollections(ctx, bson.D{}) + if err != nil { + return nil, fmt.Errorf("source collection preflight: %w", err) + } + defer cursor.Close(ctx) + var names []string + for cursor.Next(ctx) { + var spec struct { + Name string `bson:"name"` + Type string `bson:"type"` + } + if err := cursor.Decode(&spec); err != nil { + return nil, err + } + if isSystemCollection(spec.Name) { + continue + } + if spec.Type != "collection" { + return nil, fmt.Errorf("source collection %q has unsupported type %q; import ordinary collections only", spec.Name, spec.Type) + } + // Limit-one verifies source read permission without copying data yet. + if err := db.Collection(spec.Name).FindOne(ctx, bson.D{}).Err(); err != nil && err != mongo.ErrNoDocuments { + return nil, fmt.Errorf("source collection %q is not readable: %w", spec.Name, err) + } + names = append(names, spec.Name) + } + if err := cursor.Err(); err != nil { + return nil, err + } + if len(names) == 0 { + return nil, fmt.Errorf("source database %q has no importable collections; check its name and permissions", db.Name()) + } + sort.Strings(names) + return names, nil +} + // importCollection imports a single collection into the WAL system. // Imports write put entries directly (one batched append per batch of // documents) instead of going through the interceptor: the target project @@ -295,6 +353,9 @@ func (s *ImportService) importCollection(ctx context.Context, sourceDB *mongo.Da // Process documents in batches for cursor.Next(ctx) { + if err := ctx.Err(); err != nil { + return importedCount, walEntriesCount, err + } var doc bson.M if err := cursor.Decode(&doc); err != nil { return importedCount, walEntriesCount, fmt.Errorf("failed to decode document: %w", err) @@ -381,6 +442,9 @@ func (s *ImportService) validateImportOptions(opts ImportOptions) error { if opts.ProjectName == "" { return fmt.Errorf("project_name is required") } + if !opts.DryRun && !opts.SourceQuiesced { + return fmt.Errorf("source writes and DDL must be paused for the entire import; set source_quiesced (CLI: --source-quiesced) after stopping writers") + } return nil } @@ -393,11 +457,11 @@ func isSystemCollection(name string) bool { "config.", "argon_wal.", // Don't import our own WAL collections } - + for _, prefix := range systemPrefixes { if len(name) >= len(prefix) && name[:len(prefix)] == prefix { return true } } return false -} \ No newline at end of file +} diff --git a/internal/ingest/errors_test.go b/internal/ingest/errors_test.go index 861e4f9..b03eb0e 100644 --- a/internal/ingest/errors_test.go +++ b/internal/ingest/errors_test.go @@ -4,7 +4,7 @@ import ( "context" "errors" "fmt" - "github.com/argon-lab/argon/internal/wal" + "github.com/argon-lab/argon/v2/internal/wal" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "testing" diff --git a/internal/ingest/service.go b/internal/ingest/service.go index 27e478d..d6c02e8 100644 --- a/internal/ingest/service.go +++ b/internal/ingest/service.go @@ -18,9 +18,9 @@ import ( "sync" "time" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - "github.com/argon-lab/argon/internal/checkout" - "github.com/argon-lab/argon/internal/wal" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + "github.com/argon-lab/argon/v2/internal/checkout" + "github.com/argon-lab/argon/v2/internal/wal" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" diff --git a/internal/materializer/keys_test.go b/internal/materializer/keys_test.go index 62a9cd9..5bac61e 100644 --- a/internal/materializer/keys_test.go +++ b/internal/materializer/keys_test.go @@ -3,7 +3,7 @@ package materializer import ( "testing" - "github.com/argon-lab/argon/internal/wal" + "github.com/argon-lab/argon/v2/internal/wal" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" ) diff --git a/internal/materializer/service.go b/internal/materializer/service.go index a6d9656..d184aa1 100644 --- a/internal/materializer/service.go +++ b/internal/materializer/service.go @@ -10,7 +10,7 @@ package materializer import ( "fmt" - "github.com/argon-lab/argon/internal/wal" + "github.com/argon-lab/argon/v2/internal/wal" "go.mongodb.org/mongo-driver/bson" ) diff --git a/internal/merge/keys_test.go b/internal/merge/keys_test.go index 1a21aee..661dc4e 100644 --- a/internal/merge/keys_test.go +++ b/internal/merge/keys_test.go @@ -4,7 +4,7 @@ import ( "reflect" "testing" - "github.com/argon-lab/argon/internal/wal" + "github.com/argon-lab/argon/v2/internal/wal" ) func TestDeleteKeysRoundTrip(t *testing.T) { diff --git a/internal/merge/service.go b/internal/merge/service.go index b956574..db42f66 100644 --- a/internal/merge/service.go +++ b/internal/merge/service.go @@ -27,10 +27,10 @@ import ( "sort" "time" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - "github.com/argon-lab/argon/internal/materializer" - "github.com/argon-lab/argon/internal/mongoexpr" - "github.com/argon-lab/argon/internal/wal" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + "github.com/argon-lab/argon/v2/internal/materializer" + "github.com/argon-lab/argon/v2/internal/mongoexpr" + "github.com/argon-lab/argon/v2/internal/wal" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" diff --git a/internal/migrate/service.go b/internal/migrate/service.go index d76f55f..47b1c26 100644 --- a/internal/migrate/service.go +++ b/internal/migrate/service.go @@ -22,10 +22,10 @@ import ( "fmt" "sort" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - "github.com/argon-lab/argon/internal/mongoexpr" - "github.com/argon-lab/argon/internal/materializer" - "github.com/argon-lab/argon/internal/wal" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + "github.com/argon-lab/argon/v2/internal/mongoexpr" + "github.com/argon-lab/argon/v2/internal/materializer" + "github.com/argon-lab/argon/v2/internal/wal" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" diff --git a/internal/pin/service.go b/internal/pin/service.go index 198f7bb..b9f4e34 100644 --- a/internal/pin/service.go +++ b/internal/pin/service.go @@ -27,7 +27,7 @@ import ( "fmt" "time" - branchwal "github.com/argon-lab/argon/internal/branch/wal" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" diff --git a/internal/project/wal/import.go b/internal/project/wal/import.go new file mode 100644 index 0000000..d11c682 --- /dev/null +++ b/internal/project/wal/import.go @@ -0,0 +1,90 @@ +package wal + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/argon-lab/argon/v2/internal/wal" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +// BeginImport reserves a project name before any copying. Staged imports are +// hidden from normal project lookup/listing until CompleteImport publishes them. +func (s *ProjectService) BeginImport(ctx context.Context, name string) (_ *wal.Project, err error) { + project := &wal.Project{ID: primitive.NewObjectID().Hex(), Name: name, CreatedAt: time.Now(), UseWAL: true, Importing: true} + if _, err = s.collection.InsertOne(ctx, project); err != nil { + return nil, fmt.Errorf("reserve import project: %w", err) + } + defer func() { + if err != nil { + cleanup, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + if cleanupErr := s.AbortImport(cleanup, project.ID); cleanupErr != nil { + err = errors.Join(err, fmt.Errorf("cleanup incomplete for import project %s: %w", name, cleanupErr)) + } + } + }() + if _, err = s.wal.Append(&wal.Entry{ProjectID: project.ID, Operation: wal.OpCreateProject, + Metadata: map[string]interface{}{"project_name": name, "import": true}}); err != nil { + return nil, err + } + branch, err := s.branches.CreateBranch(project.ID, "main", "") + if err != nil { + return nil, err + } + project.MainBranchID = branch.ID + _, err = s.collection.UpdateOne(ctx, bson.M{"_id": project.ID, "importing": true}, bson.M{"$set": bson.M{"main_branch_id": branch.ID}}) + return project, err +} + +func (s *ProjectService) CompleteImport(ctx context.Context, projectID string) error { + result, err := s.collection.UpdateOne(ctx, bson.M{"_id": projectID, "importing": true}, bson.M{"$unset": bson.M{"importing": ""}}) + if err != nil { + return err + } + if result.MatchedCount != 1 { + return fmt.Errorf("staged import %s no longer exists", projectID) + } + return nil +} + +// AbortImport removes only an unpublished import and its private history in one +// transaction. It cannot delete an ordinary or successfully published project. +func (s *ProjectService) AbortImport(ctx context.Context, projectID string) error { + session, err := s.db.Client().StartSession() + if err != nil { + return err + } + defer session.EndSession(ctx) + _, err = session.WithTransaction(ctx, func(sc mongo.SessionContext) (interface{}, error) { + result, err := s.collection.DeleteOne(sc, bson.M{"_id": projectID, "importing": true}) + if err != nil { + return nil, err + } + if result.DeletedCount != 1 { + return nil, fmt.Errorf("refusing cleanup: project is not an unpublished import") + } + for _, name := range []string{"wal_log", "wal_branches"} { + if _, err := s.db.Collection(name).DeleteMany(sc, bson.M{"project_id": projectID}); err != nil { + return nil, err + } + } + _, err = s.db.Collection("wal_counters").DeleteOne(sc, bson.M{"_id": projectID}) + return nil, err + }) + return err +} + +// CleanupImport is an explicit recovery operation after a killed/crashed import. +// The operator must stop the original importer before calling it. +func (s *ProjectService) CleanupImport(ctx context.Context, name string) error { + var project wal.Project + if err := s.collection.FindOne(ctx, bson.M{"name": name, "importing": true}).Decode(&project); err != nil { + return fmt.Errorf("no unfinished import named %q: %w", name, err) + } + return s.AbortImport(ctx, project.ID) +} diff --git a/internal/project/wal/service.go b/internal/project/wal/service.go index 4366963..1d9b9db 100644 --- a/internal/project/wal/service.go +++ b/internal/project/wal/service.go @@ -6,8 +6,8 @@ import ( "fmt" "time" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - "github.com/argon-lab/argon/internal/wal" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + "github.com/argon-lab/argon/v2/internal/wal" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" @@ -103,7 +103,7 @@ func (s *ProjectService) GetProject(projectID string) (*wal.Project, error) { ctx := context.Background() var project wal.Project - err := s.collection.FindOne(ctx, bson.M{"_id": projectID}).Decode(&project) + err := s.collection.FindOne(ctx, bson.M{"_id": projectID, "importing": bson.M{"$ne": true}}).Decode(&project) if err != nil { if err == mongo.ErrNoDocuments { return nil, errors.New("project not found") @@ -119,7 +119,7 @@ func (s *ProjectService) GetProjectByName(name string) (*wal.Project, error) { ctx := context.Background() var project wal.Project - err := s.collection.FindOne(ctx, bson.M{"name": name}).Decode(&project) + err := s.collection.FindOne(ctx, bson.M{"name": name, "importing": bson.M{"$ne": true}}).Decode(&project) if err != nil { if err == mongo.ErrNoDocuments { return nil, errors.New("project not found") @@ -143,7 +143,7 @@ func (s *ProjectService) GetProjectIDByName(name string) (string, error) { // ListProjects lists all WAL-enabled projects func (s *ProjectService) ListProjects() ([]*wal.Project, error) { ctx := context.Background() - cursor, err := s.collection.Find(ctx, bson.M{"use_wal": true}) + cursor, err := s.collection.Find(ctx, bson.M{"use_wal": true, "importing": bson.M{"$ne": true}}) if err != nil { return nil, err } diff --git a/internal/restore/service.go b/internal/restore/service.go index ba836f7..c45fb8d 100644 --- a/internal/restore/service.go +++ b/internal/restore/service.go @@ -4,10 +4,10 @@ import ( "fmt" "time" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - "github.com/argon-lab/argon/internal/materializer" - "github.com/argon-lab/argon/internal/timetravel" - "github.com/argon-lab/argon/internal/wal" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + "github.com/argon-lab/argon/v2/internal/materializer" + "github.com/argon-lab/argon/v2/internal/timetravel" + "github.com/argon-lab/argon/v2/internal/wal" "go.mongodb.org/mongo-driver/bson/primitive" ) diff --git a/internal/sandbox/service.go b/internal/sandbox/service.go index 0053f08..1359562 100644 --- a/internal/sandbox/service.go +++ b/internal/sandbox/service.go @@ -11,9 +11,9 @@ import ( "fmt" "time" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - "github.com/argon-lab/argon/internal/checkout" - "github.com/argon-lab/argon/internal/wal" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + "github.com/argon-lab/argon/v2/internal/checkout" + "github.com/argon-lab/argon/v2/internal/wal" ) // DefaultTTL bounds sandboxes that don't specify one. diff --git a/internal/snapshot/auto.go b/internal/snapshot/auto.go index fcec7d1..8bc87ed 100644 --- a/internal/snapshot/auto.go +++ b/internal/snapshot/auto.go @@ -5,7 +5,7 @@ import ( "fmt" "log" - "github.com/argon-lab/argon/internal/wal" + "github.com/argon-lab/argon/v2/internal/wal" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" diff --git a/internal/snapshot/codec.go b/internal/snapshot/codec.go index 05a4180..983fff1 100644 --- a/internal/snapshot/codec.go +++ b/internal/snapshot/codec.go @@ -5,8 +5,8 @@ import ( "fmt" "sort" - "github.com/argon-lab/argon/internal/mongoexpr" - "github.com/argon-lab/argon/internal/wal" + "github.com/argon-lab/argon/v2/internal/mongoexpr" + "github.com/argon-lab/argon/v2/internal/wal" "go.mongodb.org/mongo-driver/bson" ) diff --git a/internal/snapshot/service.go b/internal/snapshot/service.go index 80bb411..6a22d1c 100644 --- a/internal/snapshot/service.go +++ b/internal/snapshot/service.go @@ -6,9 +6,9 @@ import ( "sync" "time" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - "github.com/argon-lab/argon/internal/materializer" - "github.com/argon-lab/argon/internal/wal" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + "github.com/argon-lab/argon/v2/internal/materializer" + "github.com/argon-lab/argon/v2/internal/wal" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" diff --git a/internal/timetravel/service.go b/internal/timetravel/service.go index a0a0fd0..3055bbf 100644 --- a/internal/timetravel/service.go +++ b/internal/timetravel/service.go @@ -4,8 +4,8 @@ import ( "fmt" "time" - "github.com/argon-lab/argon/internal/materializer" - "github.com/argon-lab/argon/internal/wal" + "github.com/argon-lab/argon/v2/internal/materializer" + "github.com/argon-lab/argon/v2/internal/wal" "go.mongodb.org/mongo-driver/bson" ) diff --git a/internal/undo/service.go b/internal/undo/service.go index 58631ed..b09ee91 100644 --- a/internal/undo/service.go +++ b/internal/undo/service.go @@ -15,10 +15,10 @@ import ( "fmt" "sort" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - "github.com/argon-lab/argon/internal/materializer" - "github.com/argon-lab/argon/internal/mongoexpr" - "github.com/argon-lab/argon/internal/wal" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + "github.com/argon-lab/argon/v2/internal/materializer" + "github.com/argon-lab/argon/v2/internal/mongoexpr" + "github.com/argon-lab/argon/v2/internal/wal" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" diff --git a/internal/wal/models.go b/internal/wal/models.go index 3b00818..e69cf87 100644 --- a/internal/wal/models.go +++ b/internal/wal/models.go @@ -227,4 +227,5 @@ type Project struct { MainBranchID string `bson:"main_branch_id" json:"main_branch_id"` CreatedAt time.Time `bson:"created_at" json:"created_at"` UseWAL bool `bson:"use_wal" json:"use_wal"` + Importing bool `bson:"importing,omitempty" json:"importing,omitempty"` } diff --git a/internal/wal/service.go b/internal/wal/service.go index 10d2a87..6bde978 100644 --- a/internal/wal/service.go +++ b/internal/wal/service.go @@ -21,12 +21,6 @@ type Service struct { compressor *Compressor } -// legacyIndexNames are indexes from earlier releases whose keys or options -// conflict with the current definitions and must be dropped before creating -// the new ones. Creating an index whose name matches an existing one with -// different options fails, so these are removed up front. -var legacyIndexNames = []string{"lsn_1", "project_id_1_lsn_1"} - // NewService creates a new WAL service func NewService(db *mongo.Database) (*Service, error) { // Initialize compressor with default config @@ -45,10 +39,25 @@ func NewService(db *mongo.Database) (*Service, error) { ctx := context.Background() - // Earlier releases enforced a globally unique lsn; LSNs are now scoped - // per project, so the old definitions conflict and must be dropped. - for _, name := range legacyIndexNames { - _, _ = s.collection.Indexes().DropOne(ctx, name) + // Only migrate incompatible legacy definitions. Dropping the current + // project/LSN index on every connection races another process's index + // creation (e.g. a CLI command starting alongside `argon watch`). + specs, err := s.collection.Indexes().ListSpecifications(ctx) + if err != nil { + _ = compressor.Close() + return nil, fmt.Errorf("inspect WAL indexes: %w", err) + } + for _, spec := range specs { + legacy := spec.Name == "lsn_1" + if spec.Name == "project_id_1_lsn_1" { + legacy = spec.Unique == nil || !*spec.Unique + } + if legacy { + if _, err := s.collection.Indexes().DropOne(ctx, spec.Name); err != nil { + _ = compressor.Close() + return nil, fmt.Errorf("migrate WAL index %s: %w", spec.Name, err) + } + } } indexes := []mongo.IndexModel{ diff --git a/internal/walwriter/writer.go b/internal/walwriter/writer.go index f0cca45..a7bdfd6 100644 --- a/internal/walwriter/writer.go +++ b/internal/walwriter/writer.go @@ -13,9 +13,9 @@ import ( "sync" "time" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - "github.com/argon-lab/argon/internal/materializer" - "github.com/argon-lab/argon/internal/wal" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + "github.com/argon-lab/argon/v2/internal/materializer" + "github.com/argon-lab/argon/v2/internal/wal" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" diff --git a/internal/wireproxy/proxy.go b/internal/wireproxy/proxy.go index 8f6e7b6..554e320 100644 --- a/internal/wireproxy/proxy.go +++ b/internal/wireproxy/proxy.go @@ -41,7 +41,7 @@ import ( "sync" "time" - branchwal "github.com/argon-lab/argon/internal/branch/wal" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" "go.mongodb.org/mongo-driver/bson" ) diff --git a/npm/package.json b/npm/package.json index 06b6a7b..877719a 100644 --- a/npm/package.json +++ b/npm/package.json @@ -1,6 +1,6 @@ { "name": "argonctl", - "version": "2.1.1", + "version": "2.1.2", "mcpName": "io.github.argon-lab/argon", "description": "Git for MongoDB: branch, time-travel, merge and undo - CLI", "keywords": [ diff --git a/pkg/mcpserver/server.go b/pkg/mcpserver/server.go index e5989bb..059f533 100644 --- a/pkg/mcpserver/server.go +++ b/pkg/mcpserver/server.go @@ -20,7 +20,7 @@ import ( "sync" "time" - "github.com/argon-lab/argon/pkg/walcli" + "github.com/argon-lab/argon/v2/pkg/walcli" ) const protocolVersion = "2024-11-05" diff --git a/pkg/version/version.go b/pkg/version/version.go index f51c1d0..1380a68 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -4,11 +4,11 @@ package version import ( "strings" - "github.com/argon-lab/argon" + "github.com/argon-lab/argon/v2" ) // Build overrides VERSION in release builds. Set it with: -// -ldflags "-X github.com/argon-lab/argon/pkg/version.Build=2.0.1". +// -ldflags "-X github.com/argon-lab/argon/v2/pkg/version.Build=2.0.1". var Build string // String returns the release tag version, or VERSION for a source build. diff --git a/pkg/walcli/services.go b/pkg/walcli/services.go index 9bd9700..50effea 100644 --- a/pkg/walcli/services.go +++ b/pkg/walcli/services.go @@ -7,24 +7,24 @@ import ( "strings" "time" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - "github.com/argon-lab/argon/internal/checkout" - "github.com/argon-lab/argon/internal/gc" - "github.com/argon-lab/argon/internal/importer" - "github.com/argon-lab/argon/internal/ingest" - "github.com/argon-lab/argon/internal/materializer" - "github.com/argon-lab/argon/internal/merge" - "github.com/argon-lab/argon/internal/migrate" - "github.com/argon-lab/argon/internal/pin" - projectwal "github.com/argon-lab/argon/internal/project/wal" - "github.com/argon-lab/argon/internal/restore" - "github.com/argon-lab/argon/internal/sandbox" - "github.com/argon-lab/argon/internal/snapshot" - "github.com/argon-lab/argon/internal/timetravel" - "github.com/argon-lab/argon/internal/undo" - "github.com/argon-lab/argon/internal/wal" - "github.com/argon-lab/argon/internal/walwriter" - "github.com/argon-lab/argon/internal/wireproxy" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + "github.com/argon-lab/argon/v2/internal/checkout" + "github.com/argon-lab/argon/v2/internal/gc" + "github.com/argon-lab/argon/v2/internal/importer" + "github.com/argon-lab/argon/v2/internal/ingest" + "github.com/argon-lab/argon/v2/internal/materializer" + "github.com/argon-lab/argon/v2/internal/merge" + "github.com/argon-lab/argon/v2/internal/migrate" + "github.com/argon-lab/argon/v2/internal/pin" + projectwal "github.com/argon-lab/argon/v2/internal/project/wal" + "github.com/argon-lab/argon/v2/internal/restore" + "github.com/argon-lab/argon/v2/internal/sandbox" + "github.com/argon-lab/argon/v2/internal/snapshot" + "github.com/argon-lab/argon/v2/internal/timetravel" + "github.com/argon-lab/argon/v2/internal/undo" + "github.com/argon-lab/argon/v2/internal/wal" + "github.com/argon-lab/argon/v2/internal/walwriter" + "github.com/argon-lab/argon/v2/internal/wireproxy" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) @@ -365,29 +365,13 @@ func (s *Services) ImportPreview(ctx context.Context, mongoURI, databaseName str } // ImportDatabase wraps the importer database functionality for CLI use -func (s *Services) ImportDatabase(ctx context.Context, mongoURI, databaseName, projectName string, dryRun bool, batchSize int) (interface{}, error) { - // Use a map to avoid importing the internal types - opts := map[string]interface{}{ - "mongo_uri": mongoURI, - "database_name": databaseName, - "project_name": projectName, - "dry_run": dryRun, - "batch_size": batchSize, - } - - // Create a struct that matches the internal ImportOptions - return s.callImportDatabase(ctx, opts) -} - -// callImportDatabase creates the proper options struct and calls the service -func (s *Services) callImportDatabase(ctx context.Context, opts map[string]interface{}) (interface{}, error) { - // Import the internal package here where it's allowed +func (s *Services) ImportDatabase(ctx context.Context, mongoURI, databaseName, projectName string, dryRun bool, batchSize int, sourceQuiesced ...bool) (interface{}, error) { + // The optional acknowledgment preserves source compatibility while failing + // closed for writes until a caller confirms the source is quiesced. importOpts := importer.ImportOptions{ - MongoURI: opts["mongo_uri"].(string), - DatabaseName: opts["database_name"].(string), - ProjectName: opts["project_name"].(string), - DryRun: opts["dry_run"].(bool), - BatchSize: opts["batch_size"].(int), + MongoURI: mongoURI, DatabaseName: databaseName, ProjectName: projectName, + DryRun: dryRun, BatchSize: batchSize, + SourceQuiesced: len(sourceQuiesced) > 0 && sourceQuiesced[0], } return s.Importer.ImportDatabase(ctx, importOpts) diff --git a/publish-guides/README.md b/publish-guides/README.md index 6b8fc69..ef7819f 100644 --- a/publish-guides/README.md +++ b/publish-guides/README.md @@ -1,60 +1,67 @@ -# Publishing Argon Packages +# Releasing Argon -This directory contains guides for publishing Argon to various package managers. +`VERSION` is the source version. `scripts/release-version.js` synchronizes npm, +MCP and the nested Go modules' engine/API requirements. Run all release steps +from a clean reviewed checkout; do not change the source behind an existing tag. -## 📦 Package Publishing Status +## Prepare and validate -| Package | Platform | Status | Install Command | -|---------|----------|---------|----------------| -| CLI | Homebrew | ✅ Ready | `brew install argon-lab/tap/argonctl` | -| CLI | NPM | ✅ Ready | `npm install -g argonctl` | -| Python (agents) | PyPI | ✅ Ready | `pip install argon-agents` | -| Go engine | Go Modules | ✅ Ready | `go get github.com/argon-lab/argon` | +For example, to prepare 2.1.2: -The legacy `argon-mongodb` PyPI package targeted the v1 engine and is -frozen; its in-repo source was removed with v2. The v2 Python surface is -the REST-based `argon-agents` package. - -## 🚀 Quick Publishing Checklist +```sh +node scripts/release-version.js 2.1.2 +node scripts/release-version.js --check +node --test scripts/release-version.test.js npm/scripts/install.test.js +bash scripts/check-go-module.sh +``` -### 1. Create GitHub Release -```bash -# Tag the version -git tag v2.0.0 -git push origin v2.0.0 +Update the changelog, build the public console as described in the repository's +console instructions, and review the complete commit. CI must pass root, API and +CLI tests against a MongoDB replica set, the MinIO contract suite, native driver +compatibility, all three Go vet checks and reachable-vulnerability scans. -# Build binaries for all platforms -make build-all-platforms +Create **both** Go module tags on that same reviewed commit and push them together: -# Create release on GitHub with binaries +```sh +git tag v2.1.2 +git tag api/v2.1.2 +git push --atomic origin v2.1.2 api/v2.1.2 ``` -### 2. Update Package Versions -- Homebrew: `homebrew-tap/argonctl.rb` - update version and SHA256 -- NPM: `npm/package.json` - update version -- Python: `pyproject.toml` - update version +The root tag publishes `github.com/argon-lab/argon/v2`; the prefixed tag publishes +the nested `github.com/argon-lab/argon/api/v2` module. The CLI is built from this +checkout, not distributed as a separately tagged Go module. Versions before +2.1.2 used invalid v2 module paths; consumers must update imports to `/v2`. -### 3. Publish Packages -```bash -# Homebrew (push to homebrew-tap repo) -cd homebrew-tap && git push +The tag workflow calls the complete CI workflow **on the tagged source**, checks +both tags and committed version metadata, then builds five platform binaries. +Only successful validation permits GitHub release → npm → MCP publication. +The npm step needs the configured `NPM_TOKEN`; MCP uses GitHub OIDC. -# NPM -cd npm && npm publish --access public +## Verify every channel -# PyPI -python -m build && python -m twine upload dist/* +```sh +bash scripts/check-go-module.sh v2.1.2 # external consumer; no local replaces +npm view argonctl@2.1.2 version +npm install --prefix /tmp/argon-release-check argonctl@2.1.2 +/tmp/argon-release-check/node_modules/.bin/argon --version ``` -## 📚 Detailed Guides -- [Homebrew Publishing](./homebrew.md) -- [NPM Publishing](./npm.md) -- [PyPI Publishing](./pypi.md) +Verify the corresponding active version in the +[MCP Registry](https://registry.modelcontextprotocol.io/v0.1/servers/io.github.argon-lab%2Fargon/versions/latest), +then [update and test Homebrew](homebrew.md). Record the source commit, tags, +workflow URL, console source provenance and tested package versions in the release. +Hosted demo promotion has its own candidate validation and must not be inferred +from package publication. -## 🔑 Required Accounts -- **NPM**: Create account at https://npmjs.com -- **PyPI**: Create account at https://pypi.org -- **Homebrew**: Just need GitHub repo access +## Recovery and other packages -## 📧 Support -For publishing issues: support@argonlabs.tech \ No newline at end of file +- [npm recovery](npm.md): use the clean tagged checkout and existing release + assets; do not regenerate or replace an already published version. +- MCP only: manually dispatch `publish-mcp.yml` with the already published npm + version. It verifies npm visibility before publishing. +- Python: `argon-agents` is released from its + [own repository](https://github.com/argon-lab/argon-agents). Verify the actual + [PyPI version](https://pypi.org/project/argon-agents/) before recommending an + install command; a GitHub release or green skipped-upload job is insufficient. +- The legacy `argon-mongodb` v1 package is frozen and is not the v2 SDK. diff --git a/publish-guides/homebrew.md b/publish-guides/homebrew.md index a128896..0273ffd 100644 --- a/publish-guides/homebrew.md +++ b/publish-guides/homebrew.md @@ -1,51 +1,29 @@ -# Publishing to Homebrew +# Homebrew release update -## Prerequisites -- GitHub releases with tagged versions -- Built binaries for macOS (Intel and Apple Silicon) +The public [argon-lab/homebrew-tap](https://github.com/argon-lab/homebrew-tap) +formula builds `cli/` from the reviewed source archive. It requires Go at build +time and exposes both `argon` and `argonctl`. There are currently no bottles. -## Steps to Publish +After the engine's tag validation/release succeeds, update `argonctl.rb` in the +tap repository using the **same archive URL as the formula**: -### 1. Create a New Release -```bash -# Tag the release -git tag v1.0.0 -git push origin v1.0.0 - -# Create GitHub release with binaries -# Upload argon-darwin-amd64 and argon-darwin-arm64 -``` - -### 2. Update Formula -```bash -# Get the SHA256 of the release tarball -curl -L https://api.github.com/repos/argon-lab/argon/tarball/v1.0.0 | shasum -a 256 - -# Update homebrew-tap/argonctl.rb with new version and SHA256 +```sh +curl --fail --location https://github.com/argon-lab/argon/archive/refs/tags/v2.1.2.tar.gz | shasum -a 256 ``` -### 3. Test Locally -```bash -# Test the formula -brew install --build-from-source ./homebrew-tap/argonctl.rb -brew test argonctl -brew audit --new --formula ./homebrew-tap/argonctl.rb -``` - -### 4. Publish -```bash -cd homebrew-tap -git add argonctl.rb -git commit -m "Update argonctl to v1.0.0" -git push -``` +Set its `url` and `sha256`; the linker version symbol for v2 is +`github.com/argon-lab/argon/v2/pkg/version.Build`. Keep this synchronized with +the source module path. Review the formula diff and test in a disposable tap or +clean runner before publishing the tap change: -### 5. Users Install -```bash -brew install argon-lab/tap/argonctl +```sh +brew install --build-from-source argon-lab/tap/argonctl +brew test argon-lab/tap/argonctl +argon --version +argonctl --version ``` -## Maintenance -- Update formula for each new release -- Consider using GitHub Actions for automated updates -- Add bottle (pre-compiled binary) support for faster installs \ No newline at end of file +Both binaries must report the exact new version. A matching archive checksum +alone does not replace the build and formula tests. Commit and push the reviewed +tap update only after those checks pass. Package publication does not deploy the +hosted demo. diff --git a/publish-guides/npm.md b/publish-guides/npm.md index 911f3c9..ac8cdcb 100644 --- a/publish-guides/npm.md +++ b/publish-guides/npm.md @@ -1,67 +1,24 @@ -# Publishing to NPM - -## Prerequisites -- NPM account with access to publish -- GitHub releases with platform binaries - -## Initial Setup -```bash -# Login to npm -npm login - -# Add npm organization (if needed) -npm org create argon-lab -``` - -## Steps to Publish - -### 1. Build Binaries for All Platforms -```bash -# Build for all platforms -GOOS=darwin GOARCH=amd64 go build -o dist/argon-darwin-amd64 ./cli -GOOS=darwin GOARCH=arm64 go build -o dist/argon-darwin-arm64 ./cli -GOOS=linux GOARCH=amd64 go build -o dist/argon-linux-amd64 ./cli -GOOS=windows GOARCH=amd64 go build -o dist/argon-windows-amd64.exe ./cli - -# Upload to GitHub release -``` - -### 2. Update Package Version -```bash -cd npm -npm version 1.0.0 -``` - -### 3. Test Locally -```bash -# Pack the package -npm pack - -# Test installation -npm install -g argonctl-1.0.0.tgz -argon --version -argonctl --version - -# Uninstall test -npm uninstall -g argonctl -``` - -### 4. Publish to NPM -```bash -# Dry run first -npm publish --dry-run - -# Publish (public package) -npm publish --access public -``` - -### 5. Users Install -```bash -npm install -g argonctl -``` - -## Important Notes -- The install script downloads platform-specific binary from GitHub releases -- Ensure GitHub release is created before npm publish -- Update download URLs in scripts/install.js if needed -- Consider using npm organization scope: @argon-lab/argon \ No newline at end of file +# npm publication and recovery + +Normal publication is the tested tag workflow described in [Releasing Argon](README.md). +It builds binaries in the `cli/` Go module, publishes GitHub assets and SHA256SUMS, +then publishes the stamped `argonctl` package. Do not run `npm version` +independently: `VERSION`, npm, MCP and Go companion requirements must agree. + +If the npm job fails after the release assets were published: + +1. Resolve the credential or registry failure and rerun the failed job. Verify + whether the version already exists before retrying; npm versions are immutable. +2. If manual recovery is necessary, check out the exact clean release tag, + authenticate the authorized npm publisher and run `bash scripts/publish-npm.sh`. + This script requires an exact tag, checks/stamps shared metadata, previews the + tarball and asks for final confirmation. +3. Check `npm view argonctl@VERSION version mcpName` and install into a fresh + temporary prefix. Run both `argon --version` and `argonctl --version`. +4. If npm was recovered manually, dispatch `publish-mcp.yml` for that same version. + +The installer supports macOS and Linux on amd64/arm64 and Windows amd64. Windows +arm64 is not a published binary target. The npm launchers download the matching +GitHub release asset at install time, so publishing npm before those assets exist +produces a broken installation. Prereleases use npm's `next` tag; stable releases +use `latest`. diff --git a/scripts/check-go-module.sh b/scripts/check-go-module.sh new file mode 100644 index 0000000..cfc59ce --- /dev/null +++ b/scripts/check-go-module.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Compile a consumer outside the source tree. With a version argument this +# verifies a published module without a replace directive; CI uses its checkout. +set -euo pipefail +source_root="$(cd "$(dirname "$0")/.." && pwd)" +consumer_dir="$(mktemp -d)" +trap 'rm -rf "$consumer_dir"' EXIT +cd "$consumer_dir" +go mod init example.com/argon-consumer +if [ "$#" -eq 0 ]; then + source_version="$(tr -d '\n\r' < "$source_root/VERSION")" + go mod edit "-require=github.com/argon-lab/argon/v2@v$source_version" + go mod edit "-replace=github.com/argon-lab/argon/v2=$source_root" + go mod edit "-require=github.com/argon-lab/argon/api/v2@v$source_version" + go mod edit "-replace=github.com/argon-lab/argon/api/v2=$source_root/api" +else + go get "github.com/argon-lab/argon/v2@$1" "github.com/argon-lab/argon/api/v2@$1" +fi +cat > main.go <<'GO' +package main + +import ( + "fmt" + "github.com/argon-lab/argon/v2/pkg/version" + "github.com/argon-lab/argon/v2/pkg/walcli" + "github.com/argon-lab/argon/api/v2/server" +) + +func main() { + var services *walcli.Services + _ = services + _ = server.NewRouterWith + fmt.Println(version.String()) +} +GO +go mod tidy +go build . +./argon-consumer diff --git a/scripts/release-version.js b/scripts/release-version.js index b87f548..34452be 100644 --- a/scripts/release-version.js +++ b/scripts/release-version.js @@ -27,15 +27,25 @@ function syncVersion(root, input, check = false) { if (!npmPackage || server.name !== pkg.mcpName) { throw new Error('server.json must identify the npm package and match its mcpName'); } + const modules = ['api/go.mod', 'cli/go.mod'].map(relative => { + const filename = path.join(root, relative); + const contents = fs.readFileSync(filename, 'utf8'); + const required = [...contents.matchAll(/github\.com\/argon-lab\/argon\/(?:api\/)?v2 v([^\s]+)/g)]; + if (!required.length) throw new Error(`${relative} must require the Go v2 module`); + return { filename, contents, required }; + }); if (check) { - if ([source, pkg.version, server.version, npmPackage.version].some(value => value !== version)) { - throw new Error('VERSION, npm/package.json, and server.json disagree; run node scripts/release-version.js'); + if ([source, pkg.version, server.version, npmPackage.version, ...modules.flatMap(module => module.required.map(match => match[1]))].some(value => value !== version)) { + throw new Error('VERSION, npm/package.json, server.json, and companion Go requirements disagree; run node scripts/release-version.js'); } } else { pkg.version = server.version = npmPackage.version = version; fs.writeFileSync(path.join(root, 'VERSION'), `${version}\n`); fs.writeFileSync(packagePath, `${JSON.stringify(pkg, null, 2)}\n`); fs.writeFileSync(serverPath, `${JSON.stringify(server, null, 2)}\n`); + for (const module of modules) { + fs.writeFileSync(module.filename, module.contents.replace(/(github\.com\/argon-lab\/argon\/(?:api\/)?v2) v[^\s]+/g, `$1 v${version}`)); + } } return version; } diff --git a/scripts/release-version.test.js b/scripts/release-version.test.js index 397623b..9dcaae7 100644 --- a/scripts/release-version.test.js +++ b/scripts/release-version.test.js @@ -16,11 +16,19 @@ test('tag overrides all artifacts and check rejects version drift', t => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'argon-version-')); t.after(() => fs.rmSync(root, { recursive: true, force: true })); fs.mkdirSync(path.join(root, 'npm')); + for (const module of ['api', 'cli']) { + fs.mkdirSync(path.join(root, module)); + fs.writeFileSync(path.join(root, module, 'go.mod'), 'require github.com/argon-lab/argon/v2 v2.0.0\n'); + } fs.writeFileSync(path.join(root, 'VERSION'), '1.0.0\n'); fs.writeFileSync(path.join(root, 'npm/package.json'), JSON.stringify({ name: 'argonctl', version: '1.1.0', mcpName: 'io.github.argon-lab/argon' })); fs.writeFileSync(path.join(root, 'server.json'), JSON.stringify({ name: 'io.github.argon-lab/argon', version: '1.0.0', packages: [{ registryType: 'npm', identifier: 'argonctl', version: '1.0.0' }] })); assert.throws(() => syncVersion(root, undefined, true), /disagree/); assert.equal(syncVersion(root, 'v2.2.0'), '2.2.0'); assert.equal(syncVersion(root, undefined, true), '2.2.0'); + assert.match(fs.readFileSync(path.join(root, 'api/go.mod'), 'utf8'), /argon\/v2 v2\.2\.0/); + fs.writeFileSync(path.join(root, 'cli/go.mod'), 'require github.com/argon-lab/argon/v2 v2.1.0\n'); + assert.throws(() => syncVersion(root, undefined, true), /disagree/); + syncVersion(root, '2.2.0'); assert.throws(() => syncVersion(root, '2.3.0', true), /disagree/); }); diff --git a/server.json b/server.json index b4d32f4..75cbbd9 100644 --- a/server.json +++ b/server.json @@ -6,12 +6,12 @@ "url": "https://github.com/argon-lab/argon", "source": "github" }, - "version": "2.1.1", + "version": "2.1.2", "packages": [ { "registryType": "npm", "identifier": "argonctl", - "version": "2.1.1", + "version": "2.1.2", "transport": { "type": "stdio" }, diff --git a/tests/wal/capture_hardening_test.go b/tests/wal/capture_hardening_test.go index f791a00..c15e382 100644 --- a/tests/wal/capture_hardening_test.go +++ b/tests/wal/capture_hardening_test.go @@ -7,8 +7,8 @@ import ( "testing" "time" - "github.com/argon-lab/argon/internal/ingest" - "github.com/argon-lab/argon/internal/snapshot" + "github.com/argon-lab/argon/v2/internal/ingest" + "github.com/argon-lab/argon/v2/internal/snapshot" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/event" diff --git a/tests/wal/checkout_test.go b/tests/wal/checkout_test.go index 868224f..959c733 100644 --- a/tests/wal/checkout_test.go +++ b/tests/wal/checkout_test.go @@ -5,8 +5,8 @@ import ( "fmt" "testing" - "github.com/argon-lab/argon/internal/checkout" - "github.com/argon-lab/argon/internal/walwriter" + "github.com/argon-lab/argon/v2/internal/checkout" + "github.com/argon-lab/argon/v2/internal/walwriter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" diff --git a/tests/wal/chunkstore_test.go b/tests/wal/chunkstore_test.go index 1631de9..2fa32e3 100644 --- a/tests/wal/chunkstore_test.go +++ b/tests/wal/chunkstore_test.go @@ -7,11 +7,11 @@ import ( "os" "testing" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - "github.com/argon-lab/argon/internal/walwriter" - "github.com/argon-lab/argon/internal/materializer" - "github.com/argon-lab/argon/internal/snapshot" - "github.com/argon-lab/argon/internal/wal" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + "github.com/argon-lab/argon/v2/internal/walwriter" + "github.com/argon-lab/argon/v2/internal/materializer" + "github.com/argon-lab/argon/v2/internal/snapshot" + "github.com/argon-lab/argon/v2/internal/wal" "github.com/aws/aws-sdk-go-v2/aws" awsconfig "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/s3" diff --git a/tests/wal/compression_test.go b/tests/wal/compression_test.go index f247037..394a9f9 100644 --- a/tests/wal/compression_test.go +++ b/tests/wal/compression_test.go @@ -4,7 +4,7 @@ import ( "bytes" "testing" - "github.com/argon-lab/argon/internal/wal" + "github.com/argon-lab/argon/v2/internal/wal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" diff --git a/tests/wal/determinism_property_test.go b/tests/wal/determinism_property_test.go index deb0f83..62bc92d 100644 --- a/tests/wal/determinism_property_test.go +++ b/tests/wal/determinism_property_test.go @@ -6,12 +6,12 @@ import ( "math/rand" "testing" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - "github.com/argon-lab/argon/internal/materializer" - "github.com/argon-lab/argon/internal/restore" - "github.com/argon-lab/argon/internal/timetravel" - "github.com/argon-lab/argon/internal/wal" - "github.com/argon-lab/argon/internal/walwriter" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + "github.com/argon-lab/argon/v2/internal/materializer" + "github.com/argon-lab/argon/v2/internal/restore" + "github.com/argon-lab/argon/v2/internal/timetravel" + "github.com/argon-lab/argon/v2/internal/wal" + "github.com/argon-lab/argon/v2/internal/walwriter" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" ) diff --git a/tests/wal/gc_test.go b/tests/wal/gc_test.go index 49d5130..5f39712 100644 --- a/tests/wal/gc_test.go +++ b/tests/wal/gc_test.go @@ -6,8 +6,8 @@ import ( "testing" "time" - "github.com/argon-lab/argon/internal/walwriter" - "github.com/argon-lab/argon/internal/gc" + "github.com/argon-lab/argon/v2/internal/walwriter" + "github.com/argon-lab/argon/v2/internal/gc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" diff --git a/tests/wal/hardening_core_test.go b/tests/wal/hardening_core_test.go index c012aea..27b0aeb 100644 --- a/tests/wal/hardening_core_test.go +++ b/tests/wal/hardening_core_test.go @@ -6,12 +6,12 @@ import ( "testing" "time" - "github.com/argon-lab/argon/internal/checkout" - "github.com/argon-lab/argon/internal/materializer" - "github.com/argon-lab/argon/internal/merge" - "github.com/argon-lab/argon/internal/snapshot" - "github.com/argon-lab/argon/internal/wal" - "github.com/argon-lab/argon/internal/walwriter" + "github.com/argon-lab/argon/v2/internal/checkout" + "github.com/argon-lab/argon/v2/internal/materializer" + "github.com/argon-lab/argon/v2/internal/merge" + "github.com/argon-lab/argon/v2/internal/snapshot" + "github.com/argon-lab/argon/v2/internal/wal" + "github.com/argon-lab/argon/v2/internal/walwriter" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" diff --git a/tests/wal/import_recovery_test.go b/tests/wal/import_recovery_test.go new file mode 100644 index 0000000..789fc45 --- /dev/null +++ b/tests/wal/import_recovery_test.go @@ -0,0 +1,156 @@ +package wal_test + +import ( + "context" + "fmt" + "testing" + "time" + + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + "github.com/argon-lab/argon/v2/internal/importer" + projectwal "github.com/argon-lab/argon/v2/internal/project/wal" + "github.com/argon-lab/argon/v2/internal/wal" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +func importRecoveryFixture(t *testing.T) (*importer.ImportService, *projectwal.ProjectService, *mongo.Database, *mongo.Database) { + t.Helper() + meta := setupTestDB(t) + source := setupTestSourceDB(t, fmt.Sprintf("argon_import_recovery_%d", time.Now().UnixNano())) + log, err := wal.NewService(meta) + require.NoError(t, err) + branches, err := branchwal.NewBranchService(meta, log) + require.NoError(t, err) + projects, err := projectwal.NewProjectService(meta, log, branches) + require.NoError(t, err) + return importer.NewImportService(log, projects, branches), projects, meta, source +} + +func TestImport_PreflightAndQuiescenceFailBeforeTargetCreation(t *testing.T) { + service, projects, meta, source := importRecoveryFixture(t) + ctx := context.Background() + opts := importer.ImportOptions{MongoURI: getTestMongoURI(), DatabaseName: source.Name(), ProjectName: "preflight"} + // While multiple collections are being written, a caller must not begin + // copying without the explicit stopped-writers acknowledgment. + for _, collection := range []string{"orders", "payments"} { + _, err := source.Collection(collection).InsertOne(ctx, bson.M{"_id": 1}) + require.NoError(t, err) + } + writerCtx, stopWriter := context.WithCancel(ctx) + defer stopWriter() + writeDone := make(chan error, 1) + firstWrite := make(chan struct{}) + go func() { + first := true + for writerCtx.Err() == nil { + for _, name := range []string{"orders", "payments"} { + if _, err := source.Collection(name).UpdateOne(writerCtx, bson.M{"_id": 1}, bson.M{"$inc": bson.M{"revision": 1}}); err != nil { + writeDone <- err + return + } + } + if first { + close(firstWrite) + first = false + } + } + writeDone <- writerCtx.Err() + }() + select { + case <-firstWrite: + case err := <-writeDone: + t.Fatalf("source writer: %v", err) + case <-time.After(5 * time.Second): + t.Fatal("source writer did not start") + } + _, err := service.ImportDatabase(ctx, opts) + require.ErrorContains(t, err, "source_quiesced") + stopWriter() + <-writeDone + n, err := meta.Collection("wal_projects").CountDocuments(ctx, bson.M{}) + require.NoError(t, err) + require.Zero(t, n) + + opts.SourceQuiesced = true + opts.DatabaseName = "argon_missing_import_source" + _, err = service.ImportDatabase(ctx, opts) + require.ErrorContains(t, err, "no importable collections") + opts.DatabaseName = source.Name() + require.NoError(t, source.CreateView(ctx, "orders_view", "orders", mongo.Pipeline{})) + _, err = service.ImportDatabase(ctx, opts) + require.ErrorContains(t, err, "unsupported type") + require.NoError(t, source.Collection("orders_view").Drop(ctx)) + result, err := service.ImportDatabase(ctx, opts) + require.NoError(t, err) + require.EqualValues(t, 2, result.ImportedDocs) + _, err = projects.GetProjectByName(opts.ProjectName) + require.NoError(t, err) + require.Error(t, projects.CleanupImport(ctx, opts.ProjectName), "cleanup must never delete a completed project") +} + +func TestImport_CanceledCopyIsHiddenCleanedAndRetryable(t *testing.T) { + service, projects, meta, source := importRecoveryFixture(t) + ctx := context.Background() + docs := make([]interface{}, 1500) + for i := range docs { + docs[i] = bson.M{"_id": i, "value": "source"} + } + _, err := source.Collection("records").InsertMany(ctx, docs) + require.NoError(t, err) + opts := importer.ImportOptions{MongoURI: getTestMongoURI(), DatabaseName: source.Name(), ProjectName: "retry", BatchSize: 1, SourceQuiesced: true} + copyCtx, cancelCopy := context.WithCancel(ctx) + defer cancelCopy() + done := make(chan error, 1) + go func() { _, err := service.ImportDatabase(copyCtx, opts); done <- err }() + var staged wal.Project + require.Eventually(t, func() bool { + if err := meta.Collection("wal_projects").FindOne(ctx, bson.M{"name": "retry", "importing": true}).Decode(&staged); err != nil { + return false + } + n, _ := meta.Collection("wal_log").CountDocuments(ctx, bson.M{"project_id": staged.ID, "actor": "importer"}) + return n > 0 + }, 15*time.Second, 5*time.Millisecond) + _, err = projects.GetProjectByName("retry") + require.Error(t, err, "in-progress data must not be published") + visible, err := projects.ListProjects() + require.NoError(t, err) + require.Empty(t, visible) + cancelCopy() + select { + case err := <-done: + require.ErrorIs(t, err, context.Canceled) + case <-time.After(30 * time.Second): + t.Fatal("cancelled import did not finish cleanup") + } + for _, name := range []string{"wal_projects", "wal_log", "wal_branches", "wal_counters"} { + n, err := meta.Collection(name).CountDocuments(ctx, bson.M{}) + require.NoError(t, err) + require.Zero(t, n, "%s retains failed import data", name) + } + opts.BatchSize = 500 + result, err := service.ImportDatabase(ctx, opts) + require.NoError(t, err) + require.EqualValues(t, len(docs), result.ImportedDocs) +} + +func TestImport_CrashRecoveryOnlyRemovesStagedProject(t *testing.T) { + _, projects, meta, _ := importRecoveryFixture(t) + ctx := context.Background() + ordinary, err := projects.CreateProject("keep") + require.NoError(t, err) + staged, err := projects.BeginImport(ctx, "crashed") + require.NoError(t, err) + // Simulate a process disappearing: no importer defer runs. A separate + // recovery call may delete only this explicitly unfinished reservation. + require.NoError(t, projects.CleanupImport(ctx, "crashed")) + n, err := meta.Collection("wal_log").CountDocuments(ctx, bson.M{"project_id": staged.ID}) + require.NoError(t, err) + require.Zero(t, n) + require.Error(t, projects.AbortImport(ctx, ordinary.ID)) + _, err = projects.GetProjectByName("keep") + require.NoError(t, err) + _, err = projects.BeginImport(ctx, "crashed") + require.NoError(t, err, "crash recovery must free the original name") +} diff --git a/tests/wal/import_test.go b/tests/wal/import_test.go index c179fa7..c9f14cc 100644 --- a/tests/wal/import_test.go +++ b/tests/wal/import_test.go @@ -12,16 +12,16 @@ import ( "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" - "github.com/argon-lab/argon/internal/importer" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - projectwal "github.com/argon-lab/argon/internal/project/wal" - "github.com/argon-lab/argon/internal/wal" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + "github.com/argon-lab/argon/v2/internal/importer" + projectwal "github.com/argon-lab/argon/v2/internal/project/wal" + "github.com/argon-lab/argon/v2/internal/wal" ) // TestImportPreview tests the import preview functionality func TestImportPreview(t *testing.T) { ctx := context.Background() - + // Setup test databases walDB := setupTestDB(t) sourceDB := setupTestSourceDB(t, "test_source_import") @@ -75,7 +75,7 @@ func TestImportPreview(t *testing.T) { // TestImportDatabaseDryRun tests the dry run import functionality func TestImportDatabaseDryRun(t *testing.T) { ctx := context.Background() - + // Setup test databases walDB := setupTestDB(t) sourceDB := setupTestSourceDB(t, "test_source_import_dry") @@ -98,11 +98,12 @@ func TestImportDatabaseDryRun(t *testing.T) { // Test dry run import sourceMongoURI := getTestMongoURI() opts := importer.ImportOptions{ - MongoURI: sourceMongoURI, - DatabaseName: "test_source_import_dry", - ProjectName: "test-import-project", - DryRun: true, - BatchSize: 100, + SourceQuiesced: true, + MongoURI: sourceMongoURI, + DatabaseName: "test_source_import_dry", + ProjectName: "test-import-project", + DryRun: true, + BatchSize: 100, } result, err := importService.ImportDatabase(ctx, opts) @@ -124,7 +125,7 @@ func TestImportDatabaseDryRun(t *testing.T) { // TestImportDatabaseActual tests the actual import functionality func TestImportDatabaseActual(t *testing.T) { ctx := context.Background() - + // Setup test databases walDB := setupTestDB(t) sourceDB := setupTestSourceDB(t, "test_source_import_actual") @@ -147,11 +148,12 @@ func TestImportDatabaseActual(t *testing.T) { // Test actual import sourceMongoURI := getTestMongoURI() opts := importer.ImportOptions{ - MongoURI: sourceMongoURI, - DatabaseName: "test_source_import_actual", - ProjectName: "test-import-project", - DryRun: false, - BatchSize: 2, // Small batch for testing + SourceQuiesced: true, + MongoURI: sourceMongoURI, + DatabaseName: "test_source_import_actual", + ProjectName: "test-import-project", + DryRun: false, + BatchSize: 2, // Small batch for testing } result, err := importService.ImportDatabase(ctx, opts) @@ -207,7 +209,7 @@ func TestImportDatabaseActual(t *testing.T) { // TestImportValidation tests input validation func TestImportValidation(t *testing.T) { ctx := context.Background() - + // Setup test database walDB := setupTestDB(t) @@ -225,8 +227,9 @@ func TestImportValidation(t *testing.T) { // Test missing URI opts := importer.ImportOptions{ - DatabaseName: "test", - ProjectName: "test-project", + SourceQuiesced: true, + DatabaseName: "test", + ProjectName: "test-project", } _, err = importService.ImportDatabase(ctx, opts) assert.Error(t, err) @@ -234,8 +237,9 @@ func TestImportValidation(t *testing.T) { // Test missing database name opts = importer.ImportOptions{ - MongoURI: "mongodb://localhost:27017", - ProjectName: "test-project", + SourceQuiesced: true, + MongoURI: "mongodb://localhost:27017", + ProjectName: "test-project", } _, err = importService.ImportDatabase(ctx, opts) assert.Error(t, err) @@ -243,8 +247,9 @@ func TestImportValidation(t *testing.T) { // Test missing project name opts = importer.ImportOptions{ - MongoURI: "mongodb://localhost:27017", - DatabaseName: "test", + SourceQuiesced: true, + MongoURI: "mongodb://localhost:27017", + DatabaseName: "test", } _, err = importService.ImportDatabase(ctx, opts) assert.Error(t, err) @@ -254,7 +259,7 @@ func TestImportValidation(t *testing.T) { // TestImportDuplicateProject tests handling of duplicate project names func TestImportDuplicateProject(t *testing.T) { ctx := context.Background() - + // Setup test database walDB := setupTestDB(t) @@ -276,10 +281,11 @@ func TestImportDuplicateProject(t *testing.T) { // Try to import with same project name opts := importer.ImportOptions{ - MongoURI: getTestMongoURI(), - DatabaseName: "test_db", - ProjectName: "existing-project", - DryRun: false, + SourceQuiesced: true, + MongoURI: getTestMongoURI(), + DatabaseName: "test_db", + ProjectName: "existing-project", + DryRun: false, } _, err = importService.ImportDatabase(ctx, opts) @@ -290,7 +296,7 @@ func TestImportDuplicateProject(t *testing.T) { // TestImportInvalidDatabase tests handling of invalid database connections func TestImportInvalidDatabase(t *testing.T) { ctx := context.Background() - + // Setup test database walDB := setupTestDB(t) @@ -323,7 +329,7 @@ func TestImportInvalidDatabase(t *testing.T) { // TestImportSystemCollections tests that system collections are properly filtered func TestImportSystemCollections(t *testing.T) { ctx := context.Background() - + // Setup test databases walDB := setupTestDB(t) sourceDB := setupTestSourceDB(t, "test_source_import_system") @@ -380,7 +386,6 @@ func createTestImportData(t *testing.T, db *mongo.Database) { require.NoError(t, err) } - // setupTestSourceDB creates a separate source database for import testing func setupTestSourceDB(t *testing.T, dbName string) *mongo.Database { ctx := context.Background() @@ -418,4 +423,4 @@ func getTestMongoURI() string { return uri } return "mongodb://localhost:27017" -} \ No newline at end of file +} diff --git a/tests/wal/index_startup_test.go b/tests/wal/index_startup_test.go new file mode 100644 index 0000000..9fe1cf8 --- /dev/null +++ b/tests/wal/index_startup_test.go @@ -0,0 +1,80 @@ +package wal_test + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/argon-lab/argon/v2/internal/wal" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/event" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func TestWAL_ConcurrentStartupKeepsCurrentIndexes(t *testing.T) { + ctx := context.Background() + var drops atomic.Int32 + client, err := mongo.Connect(ctx, options.Client().ApplyURI(getTestMongoURI()).SetMonitor(&event.CommandMonitor{ + Started: func(_ context.Context, event *event.CommandStartedEvent) { + if event.CommandName == "dropIndexes" { + drops.Add(1) + } + }, + })) + require.NoError(t, err) + db := client.Database(fmt.Sprintf("argon_index_start_%d", time.Now().UnixNano())) + t.Cleanup(func() { _ = db.Drop(ctx); _ = client.Disconnect(ctx) }) + initial, err := wal.NewService(db) + require.NoError(t, err) + defer initial.Close() + var workers sync.WaitGroup + results := make(chan error, 8) + for i := 0; i < 8; i++ { + workers.Add(1) + go func() { + defer workers.Done() + service, err := wal.NewService(db) + if service != nil { + defer service.Close() + } + results <- err + }() + } + workers.Wait() + close(results) + for err := range results { + require.NoError(t, err) + } + require.Zero(t, drops.Load(), "normal startup must not drop/rebuild the current unique index") +} + +func TestWAL_StartupMigratesOnlyLegacyIndexes(t *testing.T) { + ctx := context.Background() + db := setupTestDB(t) + indexes := db.Collection("wal_log").Indexes() + _, err := indexes.CreateMany(ctx, []mongo.IndexModel{ + {Keys: bson.D{{Key: "lsn", Value: 1}}, Options: options.Index().SetUnique(true)}, + {Keys: bson.D{{Key: "project_id", Value: 1}, {Key: "lsn", Value: 1}}}, + }) + require.NoError(t, err) + service, err := wal.NewService(db) + require.NoError(t, err) + defer service.Close() + specs, err := indexes.ListSpecifications(ctx) + require.NoError(t, err) + var found bool + for _, spec := range specs { + require.NotEqual(t, "lsn_1", spec.Name) + if spec.Name == "project_id_1_lsn_1" { + found = true + require.NotNil(t, spec.Unique) + require.True(t, *spec.Unique) + } + } + require.True(t, found) +} diff --git a/tests/wal/ingest_test.go b/tests/wal/ingest_test.go index 6e5ecd0..8ace045 100644 --- a/tests/wal/ingest_test.go +++ b/tests/wal/ingest_test.go @@ -6,10 +6,10 @@ import ( "testing" "time" - "github.com/argon-lab/argon/internal/checkout" - "github.com/argon-lab/argon/internal/ingest" - "github.com/argon-lab/argon/internal/wal" - "github.com/argon-lab/argon/internal/walwriter" + "github.com/argon-lab/argon/v2/internal/checkout" + "github.com/argon-lab/argon/v2/internal/ingest" + "github.com/argon-lab/argon/v2/internal/wal" + "github.com/argon-lab/argon/v2/internal/walwriter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" diff --git a/tests/wal/integration_test.go b/tests/wal/integration_test.go index 271c98a..8f677b2 100644 --- a/tests/wal/integration_test.go +++ b/tests/wal/integration_test.go @@ -5,13 +5,13 @@ import ( "fmt" "testing" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - "github.com/argon-lab/argon/internal/materializer" - projectwal "github.com/argon-lab/argon/internal/project/wal" - "github.com/argon-lab/argon/internal/restore" - "github.com/argon-lab/argon/internal/timetravel" - "github.com/argon-lab/argon/internal/wal" - "github.com/argon-lab/argon/internal/walwriter" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + "github.com/argon-lab/argon/v2/internal/materializer" + projectwal "github.com/argon-lab/argon/v2/internal/project/wal" + "github.com/argon-lab/argon/v2/internal/restore" + "github.com/argon-lab/argon/v2/internal/timetravel" + "github.com/argon-lab/argon/v2/internal/wal" + "github.com/argon-lab/argon/v2/internal/walwriter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" diff --git a/tests/wal/materializer_test.go b/tests/wal/materializer_test.go index f69a849..3c28ca0 100644 --- a/tests/wal/materializer_test.go +++ b/tests/wal/materializer_test.go @@ -4,10 +4,10 @@ import ( "context" "testing" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - "github.com/argon-lab/argon/internal/materializer" - "github.com/argon-lab/argon/internal/wal" - "github.com/argon-lab/argon/internal/walwriter" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + "github.com/argon-lab/argon/v2/internal/materializer" + "github.com/argon-lab/argon/v2/internal/wal" + "github.com/argon-lab/argon/v2/internal/walwriter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" diff --git a/tests/wal/mcpserver_test.go b/tests/wal/mcpserver_test.go index eaa7407..77d0950 100644 --- a/tests/wal/mcpserver_test.go +++ b/tests/wal/mcpserver_test.go @@ -10,16 +10,16 @@ import ( "testing" "time" - "github.com/argon-lab/argon/internal/checkout" - "github.com/argon-lab/argon/internal/gc" - "github.com/argon-lab/argon/internal/ingest" - "github.com/argon-lab/argon/internal/merge" - projectwal "github.com/argon-lab/argon/internal/project/wal" - "github.com/argon-lab/argon/internal/sandbox" - "github.com/argon-lab/argon/internal/timetravel" - "github.com/argon-lab/argon/internal/undo" - "github.com/argon-lab/argon/pkg/mcpserver" - "github.com/argon-lab/argon/pkg/walcli" + "github.com/argon-lab/argon/v2/internal/checkout" + "github.com/argon-lab/argon/v2/internal/gc" + "github.com/argon-lab/argon/v2/internal/ingest" + "github.com/argon-lab/argon/v2/internal/merge" + projectwal "github.com/argon-lab/argon/v2/internal/project/wal" + "github.com/argon-lab/argon/v2/internal/sandbox" + "github.com/argon-lab/argon/v2/internal/timetravel" + "github.com/argon-lab/argon/v2/internal/undo" + "github.com/argon-lab/argon/v2/pkg/mcpserver" + "github.com/argon-lab/argon/v2/pkg/walcli" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" diff --git a/tests/wal/merge_test.go b/tests/wal/merge_test.go index 662dd11..638401e 100644 --- a/tests/wal/merge_test.go +++ b/tests/wal/merge_test.go @@ -4,9 +4,9 @@ import ( "context" "testing" - "github.com/argon-lab/argon/internal/merge" - "github.com/argon-lab/argon/internal/wal" - "github.com/argon-lab/argon/internal/walwriter" + "github.com/argon-lab/argon/v2/internal/merge" + "github.com/argon-lab/argon/v2/internal/wal" + "github.com/argon-lab/argon/v2/internal/walwriter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" diff --git a/tests/wal/migrate_test.go b/tests/wal/migrate_test.go index 455ed29..7a17d91 100644 --- a/tests/wal/migrate_test.go +++ b/tests/wal/migrate_test.go @@ -4,10 +4,10 @@ import ( "context" "testing" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - "github.com/argon-lab/argon/internal/materializer" - "github.com/argon-lab/argon/internal/migrate" - "github.com/argon-lab/argon/internal/wal" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + "github.com/argon-lab/argon/v2/internal/materializer" + "github.com/argon-lab/argon/v2/internal/migrate" + "github.com/argon-lab/argon/v2/internal/wal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" diff --git a/tests/wal/performance_checks_test.go b/tests/wal/performance_checks_test.go index 053bf12..baf1b4c 100644 --- a/tests/wal/performance_checks_test.go +++ b/tests/wal/performance_checks_test.go @@ -4,8 +4,8 @@ import ( "os" "testing" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - "github.com/argon-lab/argon/internal/wal" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + "github.com/argon-lab/argon/v2/internal/wal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/tests/wal/pin_test.go b/tests/wal/pin_test.go index 893debf..197d998 100644 --- a/tests/wal/pin_test.go +++ b/tests/wal/pin_test.go @@ -6,10 +6,10 @@ import ( "testing" "time" - "github.com/argon-lab/argon/internal/gc" - "github.com/argon-lab/argon/internal/pin" - "github.com/argon-lab/argon/internal/sandbox" - "github.com/argon-lab/argon/internal/walwriter" + "github.com/argon-lab/argon/v2/internal/gc" + "github.com/argon-lab/argon/v2/internal/pin" + "github.com/argon-lab/argon/v2/internal/sandbox" + "github.com/argon-lab/argon/v2/internal/walwriter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" diff --git a/tests/wal/restore_test.go b/tests/wal/restore_test.go index e19fc17..1799c7a 100644 --- a/tests/wal/restore_test.go +++ b/tests/wal/restore_test.go @@ -5,13 +5,13 @@ import ( "testing" "time" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - "github.com/argon-lab/argon/internal/walwriter" - "github.com/argon-lab/argon/internal/materializer" - projectwal "github.com/argon-lab/argon/internal/project/wal" - "github.com/argon-lab/argon/internal/restore" - "github.com/argon-lab/argon/internal/timetravel" - "github.com/argon-lab/argon/internal/wal" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + "github.com/argon-lab/argon/v2/internal/walwriter" + "github.com/argon-lab/argon/v2/internal/materializer" + projectwal "github.com/argon-lab/argon/v2/internal/project/wal" + "github.com/argon-lab/argon/v2/internal/restore" + "github.com/argon-lab/argon/v2/internal/timetravel" + "github.com/argon-lab/argon/v2/internal/wal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" diff --git a/tests/wal/sandbox_test.go b/tests/wal/sandbox_test.go index 3b2e1a5..351630e 100644 --- a/tests/wal/sandbox_test.go +++ b/tests/wal/sandbox_test.go @@ -5,10 +5,10 @@ import ( "testing" "time" - "github.com/argon-lab/argon/internal/checkout" - "github.com/argon-lab/argon/internal/gc" - "github.com/argon-lab/argon/internal/sandbox" - "github.com/argon-lab/argon/internal/walwriter" + "github.com/argon-lab/argon/v2/internal/checkout" + "github.com/argon-lab/argon/v2/internal/gc" + "github.com/argon-lab/argon/v2/internal/sandbox" + "github.com/argon-lab/argon/v2/internal/walwriter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" diff --git a/tests/wal/sequencer_test.go b/tests/wal/sequencer_test.go index d738c87..7551bc8 100644 --- a/tests/wal/sequencer_test.go +++ b/tests/wal/sequencer_test.go @@ -5,7 +5,7 @@ import ( "sync" "testing" - "github.com/argon-lab/argon/internal/wal" + "github.com/argon-lab/argon/v2/internal/wal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/tests/wal/snapshot_auto_test.go b/tests/wal/snapshot_auto_test.go index 6642eb9..7d41bd0 100644 --- a/tests/wal/snapshot_auto_test.go +++ b/tests/wal/snapshot_auto_test.go @@ -5,8 +5,8 @@ import ( "fmt" "testing" - "github.com/argon-lab/argon/internal/walwriter" - "github.com/argon-lab/argon/internal/snapshot" + "github.com/argon-lab/argon/v2/internal/walwriter" + "github.com/argon-lab/argon/v2/internal/snapshot" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" diff --git a/tests/wal/snapshot_shutdown_test.go b/tests/wal/snapshot_shutdown_test.go index b4aecad..6959e86 100644 --- a/tests/wal/snapshot_shutdown_test.go +++ b/tests/wal/snapshot_shutdown_test.go @@ -6,9 +6,9 @@ import ( "testing" "time" - "github.com/argon-lab/argon/internal/materializer" - "github.com/argon-lab/argon/internal/snapshot" - "github.com/argon-lab/argon/internal/walwriter" + "github.com/argon-lab/argon/v2/internal/materializer" + "github.com/argon-lab/argon/v2/internal/snapshot" + "github.com/argon-lab/argon/v2/internal/walwriter" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" ) diff --git a/tests/wal/snapshot_test.go b/tests/wal/snapshot_test.go index ae9a0c4..ef0c60e 100644 --- a/tests/wal/snapshot_test.go +++ b/tests/wal/snapshot_test.go @@ -8,13 +8,13 @@ import ( "testing" "time" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - "github.com/argon-lab/argon/internal/walwriter" - "github.com/argon-lab/argon/internal/materializer" - "github.com/argon-lab/argon/internal/restore" - "github.com/argon-lab/argon/internal/snapshot" - "github.com/argon-lab/argon/internal/timetravel" - "github.com/argon-lab/argon/internal/wal" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + "github.com/argon-lab/argon/v2/internal/walwriter" + "github.com/argon-lab/argon/v2/internal/materializer" + "github.com/argon-lab/argon/v2/internal/restore" + "github.com/argon-lab/argon/v2/internal/snapshot" + "github.com/argon-lab/argon/v2/internal/timetravel" + "github.com/argon-lab/argon/v2/internal/wal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" diff --git a/tests/wal/timetravel_performance_test.go b/tests/wal/timetravel_performance_test.go index 0d4842c..85802c1 100644 --- a/tests/wal/timetravel_performance_test.go +++ b/tests/wal/timetravel_performance_test.go @@ -6,12 +6,12 @@ import ( "testing" "time" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - "github.com/argon-lab/argon/internal/materializer" - projectwal "github.com/argon-lab/argon/internal/project/wal" - "github.com/argon-lab/argon/internal/timetravel" - "github.com/argon-lab/argon/internal/wal" - "github.com/argon-lab/argon/internal/walwriter" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + "github.com/argon-lab/argon/v2/internal/materializer" + projectwal "github.com/argon-lab/argon/v2/internal/project/wal" + "github.com/argon-lab/argon/v2/internal/timetravel" + "github.com/argon-lab/argon/v2/internal/wal" + "github.com/argon-lab/argon/v2/internal/walwriter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" diff --git a/tests/wal/timetravel_test.go b/tests/wal/timetravel_test.go index d237807..f60b331 100644 --- a/tests/wal/timetravel_test.go +++ b/tests/wal/timetravel_test.go @@ -5,12 +5,12 @@ import ( "testing" "time" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - "github.com/argon-lab/argon/internal/materializer" - projectwal "github.com/argon-lab/argon/internal/project/wal" - "github.com/argon-lab/argon/internal/timetravel" - "github.com/argon-lab/argon/internal/wal" - "github.com/argon-lab/argon/internal/walwriter" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + "github.com/argon-lab/argon/v2/internal/materializer" + projectwal "github.com/argon-lab/argon/v2/internal/project/wal" + "github.com/argon-lab/argon/v2/internal/timetravel" + "github.com/argon-lab/argon/v2/internal/wal" + "github.com/argon-lab/argon/v2/internal/walwriter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" diff --git a/tests/wal/undo_hardening_test.go b/tests/wal/undo_hardening_test.go index 9ae00b0..ff9069f 100644 --- a/tests/wal/undo_hardening_test.go +++ b/tests/wal/undo_hardening_test.go @@ -4,9 +4,9 @@ import ( "context" "testing" - "github.com/argon-lab/argon/internal/undo" - "github.com/argon-lab/argon/internal/wal" - "github.com/argon-lab/argon/internal/walwriter" + "github.com/argon-lab/argon/v2/internal/undo" + "github.com/argon-lab/argon/v2/internal/wal" + "github.com/argon-lab/argon/v2/internal/walwriter" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" ) diff --git a/tests/wal/undo_test.go b/tests/wal/undo_test.go index 4ae4f44..359ab6e 100644 --- a/tests/wal/undo_test.go +++ b/tests/wal/undo_test.go @@ -5,8 +5,8 @@ import ( "fmt" "testing" - "github.com/argon-lab/argon/internal/walwriter" - "github.com/argon-lab/argon/internal/undo" + "github.com/argon-lab/argon/v2/internal/walwriter" + "github.com/argon-lab/argon/v2/internal/undo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" diff --git a/tests/wal/wal_comprehensive_test.go b/tests/wal/wal_comprehensive_test.go index a307efb..7d948d0 100644 --- a/tests/wal/wal_comprehensive_test.go +++ b/tests/wal/wal_comprehensive_test.go @@ -5,9 +5,9 @@ import ( "testing" "time" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - projectwal "github.com/argon-lab/argon/internal/project/wal" - "github.com/argon-lab/argon/internal/wal" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + projectwal "github.com/argon-lab/argon/v2/internal/project/wal" + "github.com/argon-lab/argon/v2/internal/wal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/tests/wal/wal_core_test.go b/tests/wal/wal_core_test.go index 2f7b1c9..70ef12e 100644 --- a/tests/wal/wal_core_test.go +++ b/tests/wal/wal_core_test.go @@ -6,9 +6,9 @@ import ( "testing" "time" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - projectwal "github.com/argon-lab/argon/internal/project/wal" - "github.com/argon-lab/argon/internal/wal" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + projectwal "github.com/argon-lab/argon/v2/internal/project/wal" + "github.com/argon-lab/argon/v2/internal/wal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" diff --git a/tests/wal/wal_stress_test.go b/tests/wal/wal_stress_test.go index 4858c70..df89f17 100644 --- a/tests/wal/wal_stress_test.go +++ b/tests/wal/wal_stress_test.go @@ -6,8 +6,8 @@ import ( "testing" "time" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - "github.com/argon-lab/argon/internal/wal" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + "github.com/argon-lab/argon/v2/internal/wal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/tests/wal/wireproxy_test.go b/tests/wal/wireproxy_test.go index 22e5ddb..2d46008 100644 --- a/tests/wal/wireproxy_test.go +++ b/tests/wal/wireproxy_test.go @@ -6,8 +6,8 @@ import ( "net" "testing" - projectwal "github.com/argon-lab/argon/internal/project/wal" - "github.com/argon-lab/argon/internal/wireproxy" + projectwal "github.com/argon-lab/argon/v2/internal/project/wal" + "github.com/argon-lab/argon/v2/internal/wireproxy" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" diff --git a/tests/wal/write_performance_test.go b/tests/wal/write_performance_test.go index 0ebe6e7..b38f9ed 100644 --- a/tests/wal/write_performance_test.go +++ b/tests/wal/write_performance_test.go @@ -7,10 +7,10 @@ import ( "testing" "time" - branchwal "github.com/argon-lab/argon/internal/branch/wal" - "github.com/argon-lab/argon/internal/materializer" - "github.com/argon-lab/argon/internal/wal" - "github.com/argon-lab/argon/internal/walwriter" + branchwal "github.com/argon-lab/argon/v2/internal/branch/wal" + "github.com/argon-lab/argon/v2/internal/materializer" + "github.com/argon-lab/argon/v2/internal/wal" + "github.com/argon-lab/argon/v2/internal/walwriter" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" From c2bd177c3beb69d300efc1a7853ba5605bc3affa Mon Sep 17 00:00:00 2001 From: Jake Wang Date: Thu, 24 Sep 2026 03:28:19 -0400 Subject: [PATCH 2/7] fix release recovery with nested API tags --- cli/cmd/import.go | 1 + scripts/publish-npm.sh | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/cli/cmd/import.go b/cli/cmd/import.go index 3dc2274..a0f73fd 100644 --- a/cli/cmd/import.go +++ b/cli/cmd/import.go @@ -336,6 +336,7 @@ func init() { _ = importStatusCmd.MarkFlagRequired("project") importCleanupCmd.Flags().StringP("project", "p", "", "Unfinished import project name") importCleanupCmd.Flags().Bool("yes", false, "Confirm the original importer is stopped and remove its unfinished data") + importCleanupCmd.Flags().StringP("output", "o", "table", "Output format: table, json") _ = importCleanupCmd.MarkFlagRequired("project") } diff --git a/scripts/publish-npm.sh b/scripts/publish-npm.sh index c9b62d0..1d97689 100755 --- a/scripts/publish-npm.sh +++ b/scripts/publish-npm.sh @@ -7,12 +7,17 @@ if ! git diff --quiet HEAD --; then echo 'Publish from a clean tagged checkout so the package matches its release binaries.' >&2 exit 1 fi -release_tag=$(git describe --tags --exact-match HEAD) +release_tag=$(git tag --points-at HEAD --list 'v*') case "$release_tag" in - v*) ;; + v*) + if [[ "$release_tag" == *$'\n'* ]]; then + echo 'HEAD has multiple root release tags; use an unambiguous release checkout.' >&2 + exit 1 + fi + ;; *) echo 'HEAD must have an exact v-prefixed release tag.' >&2; exit 1 ;; esac -release_version=$(node scripts/release-version.js "$release_tag") +release_version=$(node scripts/release-version.js "$release_tag" --check) node scripts/release-version.js --check npm whoami >/dev/null ( From ca0ac5914359fe6682e125cb8dc44b0d78c95969 Mon Sep 17 00:00:00 2001 From: Jake Wang Date: Thu, 24 Sep 2026 03:34:18 -0400 Subject: [PATCH 3/7] docs: align agent setup and retained history guarantees --- README.md | 32 +++++++++++++++++++++++++------- docs/AGENTS.md | 37 +++++++++++++++++++++++++++++-------- docs/ARCHITECTURE.md | 11 ++++++----- docs/CLI.md | 6 ++++-- docs/OPERATIONS.md | 5 +++-- 5 files changed, 67 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 1e106e6..4942e87 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,8 @@ argon restore reset -p myapp -b main --time 2026-07-07T09:00:00Z --backup pre-in Prefer clicking? `argon console` serves a local web console (UI + REST API), supervises capture, reaps expired sandboxes every minute, and opens your browser. +The console's MIT-licensed source is in [web/](web/README.md); see the +[rebuild instructions](CONTRIBUTING.md#console-source-and-reproducible-assets). For a complete managed workflow, run [the two-agent pinned dataset example](examples/pinned_agents.py). ## What you get @@ -83,21 +85,21 @@ For a complete managed workflow, run [the two-agent pinned dataset example](exam | **Branching** | `argon branches create` | a metadata write — instant, zero copy | | **Real databases** | `argon checkout` / `argon proxy` | any driver, real mongod; proxy serves stable `mongodb://host/~` URIs | | **Write capture** | `argon watch` | exact change-stream images → history, one actor label per branch | -| **Time travel** | `argon time-travel query` | any historical state, by LSN or timestamp | +| **Time travel** | `argon time-travel query` | query captured document states at retained LSNs; restore/pin also accept timestamps | | **Undo** | `argon undo --actor ` | revert a range or actor label; append-only, conflict-aware | | **Restore** | `argon restore preview/reset/branch` | rewind a released branch or fork retained history | | **Data PRs** | `argon merge preview/apply` | three-way merges as reviewable plans; conflicts never silent | | **Sandboxes** | `argon sandbox create --ttl 1h` | fork + checkout + TTL in one step — disposable agent workspaces | -| **Dataset pins** | `argon pin create` / `pin sandbox` | immutable named states that survive GC and resets — reproducible evals | +| **Dataset pins** | `argon pin create` / `pin sandbox` | named captured states protected from GC and resets while the pin exists | | **Web console** | `argon console` | local UI + REST API in one command | Data history covers document inserts, updates, replacements and deletes. Collection drop/rename produces degraded capture; indexes and collection options are not versioned. Native writes are asynchronous; control operations drain capture. -Stop writers before release. Pins preserve states, while GC can expire audit +Stop writers before release. Existing pins protect their referenced states, while GC can expire audit and undo history. See [operations](docs/OPERATIONS.md) for recovery and credentials. -Storage retention uses snapshots + retention-window GC keep state plus a +Storage retention uses snapshots + retention-window GC to keep state plus a window of history, not every write forever. Details and the consistency model, stated honestly: [ARCHITECTURE.md](docs/ARCHITECTURE.md). @@ -105,17 +107,33 @@ model, stated honestly: [ARCHITECTURE.md](docs/ARCHITECTURE.md). ```bash claude mcp add argon -- argon mcp # 13 tools: sandbox, diff, merge, undo, pins -pip install argon-agents # LangGraph checkpointer + Mem0, over REST +# Install the v0.2.0 release wheel; PyPI currently has the older v0.1.0 API. +python3 -m pip install 'argon-agents[langgraph] @ https://github.com/argon-lab/argon-agents/releases/download/v0.2.0/argon_agents-0.2.0-py3-none-any.whl' ``` +Start `argon console --no-browser` in another terminal, then: + ```python -saver = ArgonCheckpointSaver.from_sandbox(argon, "myapp") # checkpoint on a disposable branch -saver.merge() # adopt the run — or .discard() +from argon_agents import ArgonClient, ArgonCheckpointSaver + +argon = ArgonClient("http://127.0.0.1:1818") +argon.get_or_create_project("myapp") +saver = ArgonCheckpointSaver.from_sandbox(argon, "myapp") +# Compile and run your LangGraph graph with checkpointer=saver, then review: +plan = argon.merge_preview("myapp", saver.sandbox.branch) +print(plan) +# After reviewing this exact plan, apply it explicitly: +# argon.merge_apply(plan["id"]) +# Or reject the run: saver.discard() argon.create_pin("myapp", "eval-v1") # pin the dataset once run = argon.sandbox_from_pin("myapp", "eval-v1") # identical state, every eval run ``` +`saver.merge()` is a convenience call that previews and applies immediately; +use the separate calls above when approval is required. Pins protect captured +document state while they exist; keep independent backups. + The full agent workflow: [docs/AGENTS.md](docs/AGENTS.md). ## Documentation diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 0c77d9e..a6fd9f7 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -2,7 +2,7 @@ Give the agent a real MongoDB database that is secretly a disposable branch. Review what it did as a diff. Merge the good, undo the bad, -reproduce any run from a pinned dataset. +repeat runs from the same pinned document state. ``` pin ──▶ sandbox (TTL) ──▶ agent writes via any driver ──▶ diff @@ -43,8 +43,8 @@ argon sandbox discard … # or let the managed server swee ## Pins — reproducible evals -A pin is a named, immutable branch state that survives GC and resets -forever: +A pin is a named, immutable reference to a captured branch state. Its +referenced history is protected from GC and resets while the pin exists: ```bash argon pin create -p myapp --name eval-v1 --note "golden dataset" @@ -53,6 +53,8 @@ argon pin sandbox -p myapp --name eval-v1 # fresh sandbox per run Pin once, fork per run: identical input state every time, no matter what happened to the branch since. Delete the pin to release its history. +Pins are not independent backups, and matching input data does not make an +agent's output deterministic. ## MCP server @@ -116,18 +118,31 @@ everything reclaimed after `ARGON_DEMO_TTL_MINUTES` (default 60). ## Python — argon-agents -`pip install argon-agents` (add `[langgraph]` for the checkpointer): +Install the v0.2.0 release wheel, with `[langgraph]` for the checkpointer. +PyPI currently serves the older v0.1.0 API: + +```bash +python3 -m pip install 'argon-agents[langgraph] @ https://github.com/argon-lab/argon-agents/releases/download/v0.2.0/argon_agents-0.2.0-py3-none-any.whl' +``` + +Start `argon console --no-browser` in another terminal. It listens on port +1818; use port 8080 instead if you started the standalone API with `go run .`. ```python from argon_agents import ArgonClient, ArgonCheckpointSaver -argon = ArgonClient("http://localhost:8080") +argon = ArgonClient("http://127.0.0.1:1818") +argon.get_or_create_project("myapp") # LangGraph: the official MongoDB checkpointer on a sandboxed branch saver = ArgonCheckpointSaver.from_sandbox(argon, "myapp", ttl_minutes=60) -graph = builder.compile(checkpointer=saver) -saver.fork(argon) # branch the whole checkpoint history -saver.merge() # adopt the run — or saver.discard() +# Compile and run your graph with checkpointer=saver. +# Optional: forked_saver = saver.fork(argon) branches its checkpoint history. +plan = argon.merge_preview("myapp", saver.sandbox.branch) +print(plan) # inspect document changes and conflicts +# After reviewing this exact plan: +# argon.merge_apply(plan["id"]) +# Or reject the run: saver.discard() # Mem0: sandboxed agent memory from argon_agents import sandboxed_mem0_config @@ -140,6 +155,12 @@ run = argon.sandbox_from_pin("myapp", "eval-v1") LangGraph's checkpoint ids rewind steps *within* a thread; Argon adds fork/merge/undo/audit *across* the whole store. +The `saver.merge()`, `sandbox.merge()` and `argon.merge()` convenience methods +preview and apply immediately, without an approval pause. Use separate preview +and apply calls for reviewed changes. The running API manages capture and TTL +sweeps; materializing a sandbox copies data into a physical MongoDB database. +Mem0 semantic vector retrieval also requires Atlas Search or a compatible +MongoDB Search deployment and separately provisioned search indexes. ## Wire proxy diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index afabece..b0165cb 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -185,8 +185,8 @@ running `argon watch` by hand. Register with an MCP client, e.g. ### Dataset pins (reproducible evals) A pin is a named, immutable reference to a branch state — Argon's tag, -with one addition Git doesn't need: pinned history survives garbage -collection and resets forever. `argon pin create -p proj --name eval-v1` +whose referenced captured history is protected from garbage collection and +resets while the pin exists. `argon pin create -p proj --name eval-v1` pins the current head (or `--lsn` / `--time`); `argon pin sandbox` forks a TTL sandbox that starts at exactly the pinned state; `argon pin branch` makes a durable branch instead. Pin an eval dataset once, fork a fresh @@ -195,7 +195,8 @@ matter what happened to the branch since — resets included, because discarded ranges only apply to readers whose bound lies beyond them, the same rule that keeps pre-reset backup branches intact. Deleting a branch with pins is refused (like deleting a branch with live children); deleting -the pin releases its history to the next GC run. The MCP server exposes +the pin releases its history to the next GC run. Pins do not replace independent +backups. The MCP server exposes `argon_pin_create` / `argon_pin_list` / `argon_pin_sandbox`, the REST API `/projects/:p/pins`. @@ -214,7 +215,7 @@ the retention window (default 7 days), and `S_i` is the newest snapshot at or below each live child's fork point — children and all their descendants read the parent's segment with an upper bound pinned to the fork, so they can only be served by snapshots at or below it. Dataset pins enter the -same minimum as permanent readers at their LSN: a pin at `P` clamps the +same minimum as readers at their LSN while they exist: a pin at `P` clamps the cutoff to the newest snapshot usable at bound `P`, and to zero — nothing reclaimed — while no such snapshot exists. Entries at or below the cutoff are deleted; control entries stay. @@ -225,7 +226,7 @@ time-travel, audit and undo below the cutoff, which is exactly what a retention window means. Snapshots and GC bound replay and reclaim eligible history. Total storage -still grows with active branch states, permanent pins and write volume inside +still grows with active branch states, existing pins and write volume inside the retention window; retention is not a fixed disk-space quota. ## Write paths diff --git a/docs/CLI.md b/docs/CLI.md index 0589ae0..78934ad 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -97,8 +97,10 @@ argon pin branch -p P --name N --as NEW durable branch from the pin argon pin sandbox -p P --name N [--ttl 1h] TTL sandbox from the pin ``` -Pinned states survive GC and resets forever — pin an eval dataset once, -fork a sandbox per run, get identical input every time. +Pins protect their referenced captured states from GC and resets while they +exist. Pin an eval dataset once and fork a sandbox per run for identical +document input. Deleting the pin releases that protection; keep independent +backups. ## Sandboxes — disposable agent branches diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index a206a3b..b9858e0 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -109,8 +109,9 @@ every pin's coverage. Consequences, stated plainly: - No snapshot → nothing is ever deleted, no matter how old. - Reclaiming entries ends time-travel/audit/undo below the cutoff — that is what a retention window means; pick it accordingly (default 7 days). -- Pins punch permanent holes: a pinned state stays materializable forever - until the pin is deleted. +- Existing pins protect the history needed to materialize their referenced + captured states. Deleting a pin releases that protection; pins do not replace + independent backups. - Deleting a branch reclaims its entries, snapshots and unshared chunks immediately (deletion is refused while the branch has live children or pins). From b78ceae3f20701055774cc064f5114d9b0a73c65 Mon Sep 17 00:00:00 2001 From: Jake Wang Date: Thu, 24 Sep 2026 03:34:52 -0400 Subject: [PATCH 4/7] fix capture cancellation during collection setup --- CHANGELOG.md | 2 ++ internal/ingest/errors_test.go | 10 ++++++ internal/ingest/lifecycle.go | 3 ++ internal/ingest/service.go | 8 +++++ tests/wal/capture_hardening_test.go | 50 +++++++++++++++++++++++++++++ 5 files changed, 73 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d728d4..a53037c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ entry — a synchronization barrier, not in-proxy capture). - Concurrent engine/CLI startup preserves the current unique WAL index instead of dropping and rebuilding it on every connection. Legacy definitions still migrate when needed. +- Canceling capture while enabling images for a new collection preserves the + resumable boundary and stops cleanly instead of marking a permanent history gap. - Go consumers use valid `github.com/argon-lab/argon/v2` and `github.com/argon-lab/argon/api/v2` modules, with separate matching release tags. diff --git a/internal/ingest/errors_test.go b/internal/ingest/errors_test.go index b03eb0e..70c34f7 100644 --- a/internal/ingest/errors_test.go +++ b/internal/ingest/errors_test.go @@ -28,6 +28,16 @@ func TestImageSetupIndexBuildIsRetryable(t *testing.T) { } } +func TestImageSetupCancellationIsNotACaptureGap(t *testing.T) { + for _, cancellation := range []error{context.Canceled, context.DeadlineExceeded} { + err := imageSetupError(fmt.Errorf("collection setup interrupted: %w", cancellation)) + var gap *CaptureError + if !errors.Is(err, cancellation) || errors.As(err, &gap) { + t.Fatalf("cancellation lost its identity or became a permanent gap: %v", err) + } + } +} + func TestCancellationKeepsOnlyCompleteTransactionPrefix(t *testing.T) { last := bson.Raw{2} before := bson.Raw{1} diff --git a/internal/ingest/lifecycle.go b/internal/ingest/lifecycle.go index 44dd8b8..df9fa36 100644 --- a/internal/ingest/lifecycle.go +++ b/internal/ingest/lifecycle.go @@ -159,6 +159,9 @@ func (s *Service) execute(ctx context.Context, branchID string, cfg runConfig, r err = s.runStream(ctx, branchID, cfg, run) } if ctx.Err() != nil { + if errors.Is(err, ctx.Err()) { + return nil + } return err } var gap *CaptureError diff --git a/internal/ingest/service.go b/internal/ingest/service.go index d6c02e8..521fef0 100644 --- a/internal/ingest/service.go +++ b/internal/ingest/service.go @@ -207,6 +207,11 @@ func (s *Service) runStream(ctx context.Context, branchID string, cfg runConfig, } entry, err := s.convertEvent(ctx, physical, branch, stream.Current) if err != nil { + if ctx.Err() != nil && errors.Is(err, ctx.Err()) { + // The current event is not converted or checkpointed. Keep + // its safe predecessor so a restart can replay it intact. + return flushCanceled() + } complete, token := batch, lastSafeToken if len(batch) > 0 && marker.txnID() != "" && marker.txnID() == batch[len(batch)-1].TxnID { complete, token = completePrefix(batch, lastSafeToken, transactionStartToken) @@ -393,6 +398,9 @@ func (s *Service) ensurePrePostImages(ctx context.Context, physical *mongo.Datab } func imageSetupError(err error) error { + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return err + } if mongo.IsNetworkError(err) || mongo.IsTimeout(err) { return err } diff --git a/tests/wal/capture_hardening_test.go b/tests/wal/capture_hardening_test.go index c15e382..120518b 100644 --- a/tests/wal/capture_hardening_test.go +++ b/tests/wal/capture_hardening_test.go @@ -23,6 +23,56 @@ func captureContext(t *testing.T) context.Context { return ctx } +func TestCapture_CancelDuringNewCollectionSetupResumes(t *testing.T) { + ctx := captureContext(t) + watchCtx, cancelWatch := context.WithCancel(ctx) + defer cancelWatch() + var interrupted atomic.Bool + monitor := &event.CommandMonitor{Started: func(_ context.Context, e *event.CommandStartedEvent) { + if e.CommandName == "collMod" && e.Command.Lookup("collMod").StringValue() == "cancel_new" && interrupted.CompareAndSwap(false, true) { + cancelWatch() + } + }} + client, err := mongo.Connect(ctx, options.Client().ApplyURI(getTestMongoURI()).SetMonitor(monitor)) + require.NoError(t, err) + db := client.Database(fmt.Sprintf("argon_capture_cancel_setup_%d", time.Now().UnixNano())) + t.Cleanup(func() { _ = db.Drop(context.Background()); _ = client.Disconnect(context.Background()) }) + f := newIngestFixtureAt(t, "capture-cancel-setup", db) + ready := make(chan struct{}) + done := make(chan error, 1) + go func() { done <- f.ingest.Run(watchCtx, f.branchID, ingest.WithReady(ready)) }() + select { + case <-ready: + case err := <-done: + t.Fatalf("capture startup: %v", err) + case <-ctx.Done(): + t.Fatal(ctx.Err()) + } + _, err = f.physical.Collection("cancel_new").InsertOne(ctx, bson.M{"_id": "pending", "n": 1}) + require.NoError(t, err) + select { + case err := <-done: + require.NoError(t, err, "operator cancellation is not a permanent capture gap") + case <-ctx.Done(): + t.Fatal(ctx.Err()) + } + require.True(t, interrupted.Load(), "test must interrupt actual image setup") + for _, status := range f.ingest.Statuses() { + require.Equal(t, "stopped", status.State) + require.Empty(t, status.Error) + } + require.NoError(t, f.ingest.Start(ctx, f.branchID)) + require.NoError(t, f.ingest.Stop(ctx, f.branchID)) + branch, err := f.branches.GetBranchByID(f.branchID) + require.NoError(t, err) + entries, err := f.wal.GetBranchEntries(f.branchID, "cancel_new", 0, branch.HeadLSN) + require.NoError(t, err) + require.Len(t, entries, 1, "interrupted event must be captured exactly once after resuming") + state, err := f.matFull.MaterializeCollection(branch, "cancel_new") + require.NoError(t, err) + require.EqualValues(t, 1, state["pending"]["n"]) +} + func TestCapture_CanceledLargeTransactionResumesAsOneGroup(t *testing.T) { ctx := captureContext(t) watchCtx, cancelWatch := context.WithCancel(ctx) From 33805fedb9a46d7441503c7cd05bbe123a75ca9d Mon Sep 17 00:00:00 2001 From: Jake Wang <2645794+jakezwang@users.noreply.github.com> Date: Thu, 24 Sep 2026 03:34:54 -0400 Subject: [PATCH 5/7] publish reproducible console source and gate releases on browser verification --- .github/workflows/ci.yml | 53 + .github/workflows/release.yml | 60 +- CONTRIBUTING.md | 28 +- api/server/ui.go | 2 +- api/server/ui/dist/.source | 2 - api/server/ui/dist/assets/index-3FWB-cCO.js | 51 - api/server/ui/dist/assets/index-BhokV2n7.css | 1 + api/server/ui/dist/assets/index-Cpj3B2v5.css | 1 - api/server/ui/dist/assets/index-s0_H-giz.js | 10 + api/server/ui/dist/index.html | 4 +- api/server/ui/dist/provenance.json | 13 + scripts/release-provenance.mjs | 20 + scripts/sync-ui.sh | 48 +- scripts/ui-provenance.mjs | 34 + web/.gitignore | 2 + web/LICENSE | 21 + web/README.md | 38 + web/index.html | 13 + web/package-lock.json | 2168 ++++++++++++++++++ web/package.json | 34 + web/postcss.config.js | 6 + web/scripts/verify-browser.mjs | 434 ++++ web/src/App.tsx | 200 ++ web/src/Tour.tsx | 240 ++ web/src/api.ts | 341 +++ web/src/funnel.tsx | 85 + web/src/hooks.ts | 39 + web/src/index.css | 60 + web/src/main.tsx | 20 + web/src/pages/Branch.tsx | 952 ++++++++ web/src/pages/MergePlan.tsx | 202 ++ web/src/pages/MergePlans.tsx | 68 + web/src/pages/Project.tsx | 565 +++++ web/src/pages/Projects.tsx | 75 + web/src/ui.tsx | 241 ++ web/tailwind.config.js | 24 + web/tsconfig.json | 19 + web/vite.config.ts | 17 + 38 files changed, 6062 insertions(+), 129 deletions(-) delete mode 100644 api/server/ui/dist/.source delete mode 100644 api/server/ui/dist/assets/index-3FWB-cCO.js create mode 100644 api/server/ui/dist/assets/index-BhokV2n7.css delete mode 100644 api/server/ui/dist/assets/index-Cpj3B2v5.css create mode 100644 api/server/ui/dist/assets/index-s0_H-giz.js create mode 100644 api/server/ui/dist/provenance.json create mode 100644 scripts/release-provenance.mjs create mode 100644 scripts/ui-provenance.mjs create mode 100644 web/.gitignore create mode 100644 web/LICENSE create mode 100644 web/README.md create mode 100644 web/index.html create mode 100644 web/package-lock.json create mode 100644 web/package.json create mode 100644 web/postcss.config.js create mode 100644 web/scripts/verify-browser.mjs create mode 100644 web/src/App.tsx create mode 100644 web/src/Tour.tsx create mode 100644 web/src/api.ts create mode 100644 web/src/funnel.tsx create mode 100644 web/src/hooks.ts create mode 100644 web/src/index.css create mode 100644 web/src/main.tsx create mode 100644 web/src/pages/Branch.tsx create mode 100644 web/src/pages/MergePlan.tsx create mode 100644 web/src/pages/MergePlans.tsx create mode 100644 web/src/pages/Project.tsx create mode 100644 web/src/pages/Projects.tsx create mode 100644 web/src/ui.tsx create mode 100644 web/tailwind.config.js create mode 100644 web/tsconfig.json create mode 100644 web/vite.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 064db0d..4c80b24 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,59 @@ on: branches: [ master, main ] jobs: + console: + name: Public console build and browser regression + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '24' + cache: npm + cache-dependency-path: web/package-lock.json + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache-dependency-path: | + go.sum + api/go.sum + - name: Verify public source reproduces embedded assets + run: bash scripts/sync-ui.sh --check + - name: Audit complete console dependency tree + run: npm audit --prefix web + - name: Start disposable MongoDB replica set + run: | + docker run -d --name console-mongo -p 127.0.0.1:27017:27017 mongo:7.0.43 --replSet rs0 --bind_ip_all + for i in $(seq 1 30); do docker exec console-mongo mongosh --quiet --eval 'db.adminCommand({ping:1})' && break; sleep 1; done + docker exec console-mongo mongosh --quiet --eval 'rs.initiate({_id:"rs0",members:[{_id:0,host:"localhost:27017"}]})' + for i in $(seq 1 30); do docker exec console-mongo mongosh --quiet --eval 'if(!db.hello().isWritablePrimary)quit(1)' && break; sleep 1; done + docker exec console-mongo mongosh --quiet --eval 'if(!db.hello().isWritablePrimary)quit(1)' + - name: Build the same engine commit with embedded UI + working-directory: api + run: go build -o /tmp/argon-console-api . + - name: Start production console in isolated demo mode + run: | + PORT=18081 ARGON_DEMO_MODE=1 ARGON_METADATA_DB=argon_console_ci /tmp/argon-console-api >/tmp/argon-console-engine.log 2>&1 & + for i in $(seq 1 30); do curl --fail --silent http://127.0.0.1:18081/api/v1/meta >/dev/null && break; sleep 1; done + curl --fail --silent http://127.0.0.1:18081/api/v1/meta + - name: Install browser + working-directory: web + run: npx playwright install --with-deps chromium + - name: Verify Undo, recovery, conflict review and responsive UI + working-directory: web + env: + CONSOLE_URL: http://127.0.0.1:18081 + run: npm run test:browser + - uses: actions/upload-artifact@v4 + if: always() + continue-on-error: true # Optional evidence; the browser/build steps remain required. + with: + name: public-console-evidence + path: | + /tmp/argon-console-checks + /tmp/argon-console-engine.log + test: name: Test runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1ee63eb..dc3d3ce 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,61 +41,47 @@ jobs: - name: Check release tooling run: node --test scripts/release-version.test.js npm/scripts/install.test.js - build: - name: Build binaries + release: + name: Build and publish verified binaries needs: [prepare, validate] runs-on: ubuntu-latest - strategy: - matrix: - goos: [linux, darwin, windows] - goarch: [amd64, arm64] - exclude: - - goos: windows - goarch: arm64 + permissions: + contents: write steps: - uses: actions/checkout@v4 - - name: Set up Go - uses: actions/setup-go@v5 + - uses: actions/setup-node@v4 + with: + node-version: '24' + - uses: actions/setup-go@v5 with: go-version-file: go.mod cache-dependency-path: | go.sum cli/go.sum api/go.sum - - name: Build binary + # Cross-compile in one job. Final release assets are uploaded directly; + # publication does not depend on temporary Actions artifact storage. + - name: Build every supported platform working-directory: cli env: - GOOS: ${{ matrix.goos }} - GOARCH: ${{ matrix.goarch }} CGO_ENABLED: '0' RELEASE_VERSION: ${{ needs.prepare.outputs.version }} run: | mkdir -p ../dist - asset="argon-${GOOS}-${GOARCH}" - if [ "$GOOS" = windows ]; then asset="$asset.exe"; fi - go build -trimpath -ldflags="-s -w -X github.com/argon-lab/argon/v2/pkg/version.Build=$RELEASE_VERSION" -o "../dist/$asset" . - - uses: actions/upload-artifact@v4 - with: - name: binaries-${{ matrix.goos }}-${{ matrix.goarch }} - path: dist/ - if-no-files-found: error - - release: - name: Create release - needs: [prepare, validate, build] - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/download-artifact@v4 - with: - pattern: binaries-* - path: dist/ - merge-multiple: true + for target in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64; do + export GOOS="${target%/*}" GOARCH="${target#*/}" + asset="argon-${GOOS}-${GOARCH}" + if [ "$GOOS" = windows ]; then asset="$asset.exe"; fi + go build -trimpath -ldflags="-s -w -X github.com/argon-lab/argon/v2/pkg/version.Build=$RELEASE_VERSION" -o "../dist/$asset" . + done + - name: Record engine, public console and binary provenance + env: + RELEASE_VERSION: ${{ needs.prepare.outputs.version }} + run: node scripts/release-provenance.mjs dist - name: Generate checksums working-directory: dist - run: sha256sum argon-* > SHA256SUMS - - name: Create release + run: sha256sum argon-* release-provenance.json > SHA256SUMS + - name: Create release with all required assets uses: softprops/action-gh-release@v2 with: files: dist/* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 09cfab3..9e382c3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,9 +39,9 @@ Please read and follow our [Code of Conduct](CODE_OF_CONDUCT.md) to ensure a wel ### Prerequisites -- Go 1.21+ +- Go 1.26.6+ (see `go.mod`) - Docker (for MongoDB and MinIO) -- Node 20+ and Python 3.10+ only if you touch the driver-compatibility +- Node.js 24 for the console (`web/`); Python 3.10+ for the driver-compatibility harness (`compat/`) ### MongoDB (replica set required) @@ -84,6 +84,15 @@ The driver-compatibility harness (`bash compat/run.sh`) runs real pymongo and mongoose workloads against a checked-out branch and verifies WAL convergence; CI runs it on every push. +### Console source and reproducible assets + +The current console is public in [`web/`](web/README.md), under MIT. Run +`bash scripts/sync-ui.sh` from this repository to rebuild the assets embedded +in the Go binary. `bash scripts/sync-ui.sh --check` verifies the committed +assets and their SHA-256 provenance. Commit source and built assets together. +No private companion checkout is needed. CI tests the production console +against the same engine commit, including Undo scope and expired sessions. + ### Two rules the tests enforce - **Determinism**: replaying the same WAL prefix must always produce the @@ -139,9 +148,10 @@ convergence; CI runs it on every push. ### Before Submitting -- [ ] Run `make lint` to check code style -- [ ] Run `make test` to ensure all tests pass -- [ ] Run `make bench` if you've made performance-related changes +- [ ] Run `go vet ./...` in each changed Go module +- [ ] Run the engine, API and CLI tests described above +- [ ] Run `bash scripts/sync-ui.sh --check` for console changes +- [ ] Use the [benchmark suite](https://github.com/argon-lab/benchmarks) for performance claims - [ ] Update documentation for API changes - [ ] Add tests for new functionality - [ ] Rebase on latest main branch @@ -305,10 +315,10 @@ We value all contributions! Contributors will be: export ARGON_LOG_LEVEL=debug # Run with race detector -go run -race ./cmd/argon +(cd cli && go run -race . console --no-browser) -# Profile CPU usage -go run ./cmd/argon --cpuprofile=cpu.prof +# Profile engine tests +go test ./tests/wal -run Test -cpuprofile=cpu.prof ``` ### Common Issues @@ -321,7 +331,7 @@ go run ./cmd/argon --cpuprofile=cpu.prof ```bash # Run specific tests -go test -run TestBranchCreation ./engine +go test ./tests/wal -run TestBranch -count=1 # Update all dependencies go get -u ./... diff --git a/api/server/ui.go b/api/server/ui.go index aae7f0d..65eac42 100644 --- a/api/server/ui.go +++ b/api/server/ui.go @@ -1,5 +1,5 @@ // Serving the embedded web console. The SPA build is vendored into -// ui/dist by scripts/sync-ui.sh (a placeholder page ships by default), so +// ui/dist from public web/ source by scripts/sync-ui.sh, so // one binary carries both the control plane and its UI. package server diff --git a/api/server/ui/dist/.source b/api/server/ui/dist/.source deleted file mode 100644 index a823c63..0000000 --- a/api/server/ui/dist/.source +++ /dev/null @@ -1,2 +0,0 @@ -0304e68 -source-diff-sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 diff --git a/api/server/ui/dist/assets/index-3FWB-cCO.js b/api/server/ui/dist/assets/index-3FWB-cCO.js deleted file mode 100644 index 6311d5f..0000000 --- a/api/server/ui/dist/assets/index-3FWB-cCO.js +++ /dev/null @@ -1,51 +0,0 @@ -var Iu=e=>{throw TypeError(e)};var Il=(e,t,n)=>t.has(e)||Iu("Cannot "+n);var v=(e,t,n)=>(Il(e,t,"read from private field"),n?n.call(e):t.get(e)),F=(e,t,n)=>t.has(e)?Iu("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,n),_=(e,t,n,r)=>(Il(e,t,"write to private field"),r?r.call(e,n):t.set(e,n),n),I=(e,t,n)=>(Il(e,t,"access private method"),n);var si=(e,t,n,r)=>({set _(s){_(e,t,s,n)},get _(){return v(e,t,r)}});(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))r(s);new MutationObserver(s=>{for(const i of s)if(i.type==="childList")for(const l of i.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&r(l)}).observe(document,{childList:!0,subtree:!0});function n(s){const i={};return s.integrity&&(i.integrity=s.integrity),s.referrerPolicy&&(i.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?i.credentials="include":s.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function r(s){if(s.ep)return;s.ep=!0;const i=n(s);fetch(s.href,i)}})();function Ep(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Cd={exports:{}},fl={},jd={exports:{}},U={};/** - * @license React - * react.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Ks=Symbol.for("react.element"),Cp=Symbol.for("react.portal"),jp=Symbol.for("react.fragment"),Np=Symbol.for("react.strict_mode"),Pp=Symbol.for("react.profiler"),Rp=Symbol.for("react.provider"),_p=Symbol.for("react.context"),Lp=Symbol.for("react.forward_ref"),Tp=Symbol.for("react.suspense"),Fp=Symbol.for("react.memo"),Op=Symbol.for("react.lazy"),$u=Symbol.iterator;function bp(e){return e===null||typeof e!="object"?null:(e=$u&&e[$u]||e["@@iterator"],typeof e=="function"?e:null)}var Nd={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},Pd=Object.assign,Rd={};function Qr(e,t,n){this.props=e,this.context=t,this.refs=Rd,this.updater=n||Nd}Qr.prototype.isReactComponent={};Qr.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};Qr.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function _d(){}_d.prototype=Qr.prototype;function _o(e,t,n){this.props=e,this.context=t,this.refs=Rd,this.updater=n||Nd}var Lo=_o.prototype=new _d;Lo.constructor=_o;Pd(Lo,Qr.prototype);Lo.isPureReactComponent=!0;var zu=Array.isArray,Ld=Object.prototype.hasOwnProperty,To={current:null},Td={key:!0,ref:!0,__self:!0,__source:!0};function Fd(e,t,n){var r,s={},i=null,l=null;if(t!=null)for(r in t.ref!==void 0&&(l=t.ref),t.key!==void 0&&(i=""+t.key),t)Ld.call(t,r)&&!Td.hasOwnProperty(r)&&(s[r]=t[r]);var a=arguments.length-2;if(a===1)s.children=n;else if(1>>1,de=T[se];if(0>>1;ses(Dl,$))Pns(ri,Dl)?(T[se]=ri,T[Pn]=$,se=Pn):(T[se]=Dl,T[Nn]=$,se=Nn);else if(Pns(ri,$))T[se]=ri,T[Pn]=$,se=Pn;else break e}}return D}function s(T,D){var $=T.sortIndex-D.sortIndex;return $!==0?$:T.id-D.id}if(typeof performance=="object"&&typeof performance.now=="function"){var i=performance;e.unstable_now=function(){return i.now()}}else{var l=Date,a=l.now();e.unstable_now=function(){return l.now()-a}}var u=[],c=[],p=1,d=null,m=3,x=!1,g=!1,S=!1,E=typeof setTimeout=="function"?setTimeout:null,f=typeof clearTimeout=="function"?clearTimeout:null,h=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function y(T){for(var D=n(c);D!==null;){if(D.callback===null)r(c);else if(D.startTime<=T)r(c),D.sortIndex=D.expirationTime,t(u,D);else break;D=n(c)}}function k(T){if(S=!1,y(T),!g)if(n(u)!==null)g=!0,bl(C);else{var D=n(c);D!==null&&Ml(k,D.startTime-T)}}function C(T,D){g=!1,S&&(S=!1,f(N),N=-1),x=!0;var $=m;try{for(y(D),d=n(u);d!==null&&(!(d.expirationTime>D)||T&&!te());){var se=d.callback;if(typeof se=="function"){d.callback=null,m=d.priorityLevel;var de=se(d.expirationTime<=D);D=e.unstable_now(),typeof de=="function"?d.callback=de:d===n(u)&&r(u),y(D)}else r(u);d=n(u)}if(d!==null)var ni=!0;else{var Nn=n(c);Nn!==null&&Ml(k,Nn.startTime-D),ni=!1}return ni}finally{d=null,m=$,x=!1}}var P=!1,R=null,N=-1,b=5,O=-1;function te(){return!(e.unstable_now()-OT||125se?(T.sortIndex=$,t(c,T),n(u)===null&&T===n(c)&&(S?(f(N),N=-1):S=!0,Ml(k,$-se))):(T.sortIndex=de,t(u,T),g||x||(g=!0,bl(C))),T},e.unstable_shouldYield=te,e.unstable_wrapCallback=function(T){var D=m;return function(){var $=m;m=D;try{return T.apply(this,arguments)}finally{m=$}}}})(Id);Dd.exports=Id;var Wp=Dd.exports;/** - * @license React - * react-dom.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Vp=w,Ge=Wp;function j(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),ya=Object.prototype.hasOwnProperty,qp=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,Au={},Qu={};function Gp(e){return ya.call(Qu,e)?!0:ya.call(Au,e)?!1:qp.test(e)?Qu[e]=!0:(Au[e]=!0,!1)}function Yp(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function Xp(e,t,n,r){if(t===null||typeof t>"u"||Yp(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function be(e,t,n,r,s,i,l){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=s,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=i,this.removeEmptyString=l}var xe={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){xe[e]=new be(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];xe[t]=new be(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){xe[e]=new be(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){xe[e]=new be(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){xe[e]=new be(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){xe[e]=new be(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){xe[e]=new be(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){xe[e]=new be(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){xe[e]=new be(e,5,!1,e.toLowerCase(),null,!1,!1)});var Oo=/[\-:]([a-z])/g;function bo(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(Oo,bo);xe[t]=new be(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(Oo,bo);xe[t]=new be(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(Oo,bo);xe[t]=new be(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){xe[e]=new be(e,1,!1,e.toLowerCase(),null,!1,!1)});xe.xlinkHref=new be("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){xe[e]=new be(e,1,!1,e.toLowerCase(),null,!0,!0)});function Mo(e,t,n,r){var s=xe.hasOwnProperty(t)?xe[t]:null;(s!==null?s.type!==0:r||!(2a||s[l]!==i[a]){var u=` -`+s[l].replace(" at new "," at ");return e.displayName&&u.includes("")&&(u=u.replace("",e.displayName)),u}while(1<=l&&0<=a);break}}}finally{Ul=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?ls(e):""}function Jp(e){switch(e.tag){case 5:return ls(e.type);case 16:return ls("Lazy");case 13:return ls("Suspense");case 19:return ls("SuspenseList");case 0:case 2:case 15:return e=Al(e.type,!1),e;case 11:return e=Al(e.type.render,!1),e;case 1:return e=Al(e.type,!0),e;default:return""}}function wa(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case rr:return"Fragment";case nr:return"Portal";case va:return"Profiler";case Do:return"StrictMode";case ga:return"Suspense";case xa:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case Ud:return(e.displayName||"Context")+".Consumer";case zd:return(e._context.displayName||"Context")+".Provider";case Io:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case $o:return t=e.displayName||null,t!==null?t:wa(e.type)||"Memo";case Yt:t=e._payload,e=e._init;try{return wa(e(t))}catch{}}return null}function Zp(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return wa(t);case 8:return t===Do?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function Sn(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Qd(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function em(e){var t=Qd(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var s=n.get,i=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return s.call(this)},set:function(l){r=""+l,i.call(this,l)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(l){r=""+l},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function ai(e){e._valueTracker||(e._valueTracker=em(e))}function Bd(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=Qd(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function zi(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function Sa(e,t){var n=t.checked;return ee({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function Hu(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=Sn(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function Hd(e,t){t=t.checked,t!=null&&Mo(e,"checked",t,!1)}function ka(e,t){Hd(e,t);var n=Sn(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?Ea(e,t.type,n):t.hasOwnProperty("defaultValue")&&Ea(e,t.type,Sn(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function Ku(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function Ea(e,t,n){(t!=="number"||zi(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var as=Array.isArray;function pr(e,t,n,r){if(e=e.options,t){t={};for(var s=0;s"+t.valueOf().toString()+"",t=oi.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function ks(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var fs={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},tm=["Webkit","ms","Moz","O"];Object.keys(fs).forEach(function(e){tm.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),fs[t]=fs[e]})});function qd(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||fs.hasOwnProperty(e)&&fs[e]?(""+t).trim():t+"px"}function Gd(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,s=qd(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,s):e[n]=s}}var nm=ee({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function Na(e,t){if(t){if(nm[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(j(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(j(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(j(61))}if(t.style!=null&&typeof t.style!="object")throw Error(j(62))}}function Pa(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var Ra=null;function zo(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var _a=null,mr=null,yr=null;function qu(e){if(e=qs(e)){if(typeof _a!="function")throw Error(j(280));var t=e.stateNode;t&&(t=vl(t),_a(e.stateNode,e.type,t))}}function Yd(e){mr?yr?yr.push(e):yr=[e]:mr=e}function Xd(){if(mr){var e=mr,t=yr;if(yr=mr=null,qu(e),t)for(e=0;e>>=0,e===0?32:31-(hm(e)/pm|0)|0}var ui=64,ci=4194304;function os(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function Bi(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,s=e.suspendedLanes,i=e.pingedLanes,l=n&268435455;if(l!==0){var a=l&~s;a!==0?r=os(a):(i&=l,i!==0&&(r=os(i)))}else l=n&~s,l!==0?r=os(l):i!==0&&(r=os(i));if(r===0)return 0;if(t!==0&&t!==r&&!(t&s)&&(s=r&-r,i=t&-t,s>=i||s===16&&(i&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function Ws(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-ht(t),e[t]=n}function gm(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=ps),rc=" ",sc=!1;function gf(e,t){switch(e){case"keyup":return Wm.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function xf(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var sr=!1;function qm(e,t){switch(e){case"compositionend":return xf(t);case"keypress":return t.which!==32?null:(sc=!0,rc);case"textInput":return e=t.data,e===rc&&sc?null:e;default:return null}}function Gm(e,t){if(sr)return e==="compositionend"||!Vo&&gf(e,t)?(e=yf(),Pi=Ho=cn=null,sr=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=oc(n)}}function Ef(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Ef(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Cf(){for(var e=window,t=zi();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=zi(e.document)}return t}function qo(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function sy(e){var t=Cf(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Ef(n.ownerDocument.documentElement,n)){if(r!==null&&qo(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var s=n.textContent.length,i=Math.min(r.start,s);r=r.end===void 0?i:Math.min(r.end,s),!e.extend&&i>r&&(s=r,r=i,i=s),s=uc(n,i);var l=uc(n,r);s&&l&&(e.rangeCount!==1||e.anchorNode!==s.node||e.anchorOffset!==s.offset||e.focusNode!==l.node||e.focusOffset!==l.offset)&&(t=t.createRange(),t.setStart(s.node,s.offset),e.removeAllRanges(),i>r?(e.addRange(t),e.extend(l.node,l.offset)):(t.setEnd(l.node,l.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,ir=null,Ma=null,ys=null,Da=!1;function cc(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Da||ir==null||ir!==zi(r)||(r=ir,"selectionStart"in r&&qo(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),ys&&Rs(ys,r)||(ys=r,r=Wi(Ma,"onSelect"),0or||(e.current=Qa[or],Qa[or]=null,or--)}function W(e,t){or++,Qa[or]=e.current,e.current=t}var kn={},Pe=Cn(kn),ze=Cn(!1),Wn=kn;function br(e,t){var n=e.type.contextTypes;if(!n)return kn;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var s={},i;for(i in n)s[i]=t[i];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=s),s}function Ue(e){return e=e.childContextTypes,e!=null}function qi(){G(ze),G(Pe)}function vc(e,t,n){if(Pe.current!==kn)throw Error(j(168));W(Pe,t),W(ze,n)}function Of(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var s in r)if(!(s in t))throw Error(j(108,Zp(e)||"Unknown",s));return ee({},n,r)}function Gi(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||kn,Wn=Pe.current,W(Pe,e),W(ze,ze.current),!0}function gc(e,t,n){var r=e.stateNode;if(!r)throw Error(j(169));n?(e=Of(e,t,Wn),r.__reactInternalMemoizedMergedChildContext=e,G(ze),G(Pe),W(Pe,e)):G(ze),W(ze,n)}var Ft=null,gl=!1,ta=!1;function bf(e){Ft===null?Ft=[e]:Ft.push(e)}function yy(e){gl=!0,bf(e)}function jn(){if(!ta&&Ft!==null){ta=!0;var e=0,t=H;try{var n=Ft;for(H=1;e>=l,s-=l,It=1<<32-ht(t)+s|n<N?(b=R,R=null):b=R.sibling;var O=m(f,R,y[N],k);if(O===null){R===null&&(R=b);break}e&&R&&O.alternate===null&&t(f,R),h=i(O,h,N),P===null?C=O:P.sibling=O,P=O,R=b}if(N===y.length)return n(f,R),Y&&Rn(f,N),C;if(R===null){for(;NN?(b=R,R=null):b=R.sibling;var te=m(f,R,O.value,k);if(te===null){R===null&&(R=b);break}e&&R&&te.alternate===null&&t(f,R),h=i(te,h,N),P===null?C=te:P.sibling=te,P=te,R=b}if(O.done)return n(f,R),Y&&Rn(f,N),C;if(R===null){for(;!O.done;N++,O=y.next())O=d(f,O.value,k),O!==null&&(h=i(O,h,N),P===null?C=O:P.sibling=O,P=O);return Y&&Rn(f,N),C}for(R=r(f,R);!O.done;N++,O=y.next())O=x(R,f,N,O.value,k),O!==null&&(e&&O.alternate!==null&&R.delete(O.key===null?N:O.key),h=i(O,h,N),P===null?C=O:P.sibling=O,P=O);return e&&R.forEach(function(Je){return t(f,Je)}),Y&&Rn(f,N),C}function E(f,h,y,k){if(typeof y=="object"&&y!==null&&y.type===rr&&y.key===null&&(y=y.props.children),typeof y=="object"&&y!==null){switch(y.$$typeof){case li:e:{for(var C=y.key,P=h;P!==null;){if(P.key===C){if(C=y.type,C===rr){if(P.tag===7){n(f,P.sibling),h=s(P,y.props.children),h.return=f,f=h;break e}}else if(P.elementType===C||typeof C=="object"&&C!==null&&C.$$typeof===Yt&&Sc(C)===P.type){n(f,P.sibling),h=s(P,y.props),h.ref=ts(f,P,y),h.return=f,f=h;break e}n(f,P);break}else t(f,P);P=P.sibling}y.type===rr?(h=Kn(y.props.children,f.mode,k,y.key),h.return=f,f=h):(k=Mi(y.type,y.key,y.props,null,f.mode,k),k.ref=ts(f,h,y),k.return=f,f=k)}return l(f);case nr:e:{for(P=y.key;h!==null;){if(h.key===P)if(h.tag===4&&h.stateNode.containerInfo===y.containerInfo&&h.stateNode.implementation===y.implementation){n(f,h.sibling),h=s(h,y.children||[]),h.return=f,f=h;break e}else{n(f,h);break}else t(f,h);h=h.sibling}h=ua(y,f.mode,k),h.return=f,f=h}return l(f);case Yt:return P=y._init,E(f,h,P(y._payload),k)}if(as(y))return g(f,h,y,k);if(Yr(y))return S(f,h,y,k);vi(f,y)}return typeof y=="string"&&y!==""||typeof y=="number"?(y=""+y,h!==null&&h.tag===6?(n(f,h.sibling),h=s(h,y),h.return=f,f=h):(n(f,h),h=oa(y,f.mode,k),h.return=f,f=h),l(f)):n(f,h)}return E}var Dr=$f(!0),zf=$f(!1),Ji=Cn(null),Zi=null,dr=null,Jo=null;function Zo(){Jo=dr=Zi=null}function eu(e){var t=Ji.current;G(Ji),e._currentValue=t}function Ka(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function gr(e,t){Zi=e,Jo=dr=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&($e=!0),e.firstContext=null)}function st(e){var t=e._currentValue;if(Jo!==e)if(e={context:e,memoizedValue:t,next:null},dr===null){if(Zi===null)throw Error(j(308));dr=e,Zi.dependencies={lanes:0,firstContext:e}}else dr=dr.next=e;return t}var Fn=null;function tu(e){Fn===null?Fn=[e]:Fn.push(e)}function Uf(e,t,n,r){var s=t.interleaved;return s===null?(n.next=n,tu(t)):(n.next=s.next,s.next=n),t.interleaved=n,Bt(e,r)}function Bt(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var Xt=!1;function nu(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Af(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function zt(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function yn(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,Q&2){var s=r.pending;return s===null?t.next=t:(t.next=s.next,s.next=t),r.pending=t,Bt(e,n)}return s=r.interleaved,s===null?(t.next=t,tu(r)):(t.next=s.next,s.next=t),r.interleaved=t,Bt(e,n)}function _i(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Ao(e,n)}}function kc(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var s=null,i=null;if(n=n.firstBaseUpdate,n!==null){do{var l={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};i===null?s=i=l:i=i.next=l,n=n.next}while(n!==null);i===null?s=i=t:i=i.next=t}else s=i=t;n={baseState:r.baseState,firstBaseUpdate:s,lastBaseUpdate:i,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function el(e,t,n,r){var s=e.updateQueue;Xt=!1;var i=s.firstBaseUpdate,l=s.lastBaseUpdate,a=s.shared.pending;if(a!==null){s.shared.pending=null;var u=a,c=u.next;u.next=null,l===null?i=c:l.next=c,l=u;var p=e.alternate;p!==null&&(p=p.updateQueue,a=p.lastBaseUpdate,a!==l&&(a===null?p.firstBaseUpdate=c:a.next=c,p.lastBaseUpdate=u))}if(i!==null){var d=s.baseState;l=0,p=c=u=null,a=i;do{var m=a.lane,x=a.eventTime;if((r&m)===m){p!==null&&(p=p.next={eventTime:x,lane:0,tag:a.tag,payload:a.payload,callback:a.callback,next:null});e:{var g=e,S=a;switch(m=t,x=n,S.tag){case 1:if(g=S.payload,typeof g=="function"){d=g.call(x,d,m);break e}d=g;break e;case 3:g.flags=g.flags&-65537|128;case 0:if(g=S.payload,m=typeof g=="function"?g.call(x,d,m):g,m==null)break e;d=ee({},d,m);break e;case 2:Xt=!0}}a.callback!==null&&a.lane!==0&&(e.flags|=64,m=s.effects,m===null?s.effects=[a]:m.push(a))}else x={eventTime:x,lane:m,tag:a.tag,payload:a.payload,callback:a.callback,next:null},p===null?(c=p=x,u=d):p=p.next=x,l|=m;if(a=a.next,a===null){if(a=s.shared.pending,a===null)break;m=a,a=m.next,m.next=null,s.lastBaseUpdate=m,s.shared.pending=null}}while(!0);if(p===null&&(u=d),s.baseState=u,s.firstBaseUpdate=c,s.lastBaseUpdate=p,t=s.shared.interleaved,t!==null){s=t;do l|=s.lane,s=s.next;while(s!==t)}else i===null&&(s.shared.lanes=0);Gn|=l,e.lanes=l,e.memoizedState=d}}function Ec(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=ra.transition;ra.transition={};try{e(!1),t()}finally{H=n,ra.transition=r}}function sh(){return it().memoizedState}function wy(e,t,n){var r=gn(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},ih(e))lh(t,n);else if(n=Uf(e,t,n,r),n!==null){var s=Fe();pt(n,e,r,s),ah(n,t,r)}}function Sy(e,t,n){var r=gn(e),s={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(ih(e))lh(t,s);else{var i=e.alternate;if(e.lanes===0&&(i===null||i.lanes===0)&&(i=t.lastRenderedReducer,i!==null))try{var l=t.lastRenderedState,a=i(l,n);if(s.hasEagerState=!0,s.eagerState=a,yt(a,l)){var u=t.interleaved;u===null?(s.next=s,tu(t)):(s.next=u.next,u.next=s),t.interleaved=s;return}}catch{}finally{}n=Uf(e,t,s,r),n!==null&&(s=Fe(),pt(n,e,r,s),ah(n,t,r))}}function ih(e){var t=e.alternate;return e===J||t!==null&&t===J}function lh(e,t){vs=nl=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function ah(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Ao(e,n)}}var rl={readContext:st,useCallback:we,useContext:we,useEffect:we,useImperativeHandle:we,useInsertionEffect:we,useLayoutEffect:we,useMemo:we,useReducer:we,useRef:we,useState:we,useDebugValue:we,useDeferredValue:we,useTransition:we,useMutableSource:we,useSyncExternalStore:we,useId:we,unstable_isNewReconciler:!1},ky={readContext:st,useCallback:function(e,t){return wt().memoizedState=[e,t===void 0?null:t],e},useContext:st,useEffect:jc,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,Ti(4194308,4,Zf.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Ti(4194308,4,e,t)},useInsertionEffect:function(e,t){return Ti(4,2,e,t)},useMemo:function(e,t){var n=wt();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=wt();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=wy.bind(null,J,e),[r.memoizedState,e]},useRef:function(e){var t=wt();return e={current:e},t.memoizedState=e},useState:Cc,useDebugValue:cu,useDeferredValue:function(e){return wt().memoizedState=e},useTransition:function(){var e=Cc(!1),t=e[0];return e=xy.bind(null,e[1]),wt().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=J,s=wt();if(Y){if(n===void 0)throw Error(j(407));n=n()}else{if(n=t(),pe===null)throw Error(j(349));qn&30||Kf(r,t,n)}s.memoizedState=n;var i={value:n,getSnapshot:t};return s.queue=i,jc(Vf.bind(null,r,i,e),[e]),r.flags|=2048,Ds(9,Wf.bind(null,r,i,n,t),void 0,null),n},useId:function(){var e=wt(),t=pe.identifierPrefix;if(Y){var n=$t,r=It;n=(r&~(1<<32-ht(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=bs++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=l.createElement(n,{is:r.is}):(e=l.createElement(n),n==="select"&&(l=e,r.multiple?l.multiple=!0:r.size&&(l.size=r.size))):e=l.createElementNS(e,n),e[Ct]=t,e[Ts]=r,vh(e,t,!1,!1),t.stateNode=e;e:{switch(l=Pa(n,r),n){case"dialog":q("cancel",e),q("close",e),s=r;break;case"iframe":case"object":case"embed":q("load",e),s=r;break;case"video":case"audio":for(s=0;szr&&(t.flags|=128,r=!0,ns(i,!1),t.lanes=4194304)}else{if(!r)if(e=tl(l),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),ns(i,!0),i.tail===null&&i.tailMode==="hidden"&&!l.alternate&&!Y)return Se(t),null}else 2*ie()-i.renderingStartTime>zr&&n!==1073741824&&(t.flags|=128,r=!0,ns(i,!1),t.lanes=4194304);i.isBackwards?(l.sibling=t.child,t.child=l):(n=i.last,n!==null?n.sibling=l:t.child=l,i.last=l)}return i.tail!==null?(t=i.tail,i.rendering=t,i.tail=t.sibling,i.renderingStartTime=ie(),t.sibling=null,n=X.current,W(X,r?n&1|2:n&1),t):(Se(t),null);case 22:case 23:return yu(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?Be&1073741824&&(Se(t),t.subtreeFlags&6&&(t.flags|=8192)):Se(t),null;case 24:return null;case 25:return null}throw Error(j(156,t.tag))}function Ly(e,t){switch(Yo(t),t.tag){case 1:return Ue(t.type)&&qi(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Ir(),G(ze),G(Pe),iu(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return su(t),null;case 13:if(G(X),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(j(340));Mr()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return G(X),null;case 4:return Ir(),null;case 10:return eu(t.type._context),null;case 22:case 23:return yu(),null;case 24:return null;default:return null}}var xi=!1,je=!1,Ty=typeof WeakSet=="function"?WeakSet:Set,L=null;function fr(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){re(e,t,r)}else n.current=null}function eo(e,t,n){try{n()}catch(r){re(e,t,r)}}var Dc=!1;function Fy(e,t){if(Ia=Hi,e=Cf(),qo(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var s=r.anchorOffset,i=r.focusNode;r=r.focusOffset;try{n.nodeType,i.nodeType}catch{n=null;break e}var l=0,a=-1,u=-1,c=0,p=0,d=e,m=null;t:for(;;){for(var x;d!==n||s!==0&&d.nodeType!==3||(a=l+s),d!==i||r!==0&&d.nodeType!==3||(u=l+r),d.nodeType===3&&(l+=d.nodeValue.length),(x=d.firstChild)!==null;)m=d,d=x;for(;;){if(d===e)break t;if(m===n&&++c===s&&(a=l),m===i&&++p===r&&(u=l),(x=d.nextSibling)!==null)break;d=m,m=d.parentNode}d=x}n=a===-1||u===-1?null:{start:a,end:u}}else n=null}n=n||{start:0,end:0}}else n=null;for($a={focusedElem:e,selectionRange:n},Hi=!1,L=t;L!==null;)if(t=L,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,L=e;else for(;L!==null;){t=L;try{var g=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(g!==null){var S=g.memoizedProps,E=g.memoizedState,f=t.stateNode,h=f.getSnapshotBeforeUpdate(t.elementType===t.type?S:ut(t.type,S),E);f.__reactInternalSnapshotBeforeUpdate=h}break;case 3:var y=t.stateNode.containerInfo;y.nodeType===1?y.textContent="":y.nodeType===9&&y.documentElement&&y.removeChild(y.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(j(163))}}catch(k){re(t,t.return,k)}if(e=t.sibling,e!==null){e.return=t.return,L=e;break}L=t.return}return g=Dc,Dc=!1,g}function gs(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var s=r=r.next;do{if((s.tag&e)===e){var i=s.destroy;s.destroy=void 0,i!==void 0&&eo(t,n,i)}s=s.next}while(s!==r)}}function Sl(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function to(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function wh(e){var t=e.alternate;t!==null&&(e.alternate=null,wh(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Ct],delete t[Ts],delete t[Aa],delete t[py],delete t[my])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function Sh(e){return e.tag===5||e.tag===3||e.tag===4}function Ic(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Sh(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function no(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Vi));else if(r!==4&&(e=e.child,e!==null))for(no(e,t,n),e=e.sibling;e!==null;)no(e,t,n),e=e.sibling}function ro(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(ro(e,t,n),e=e.sibling;e!==null;)ro(e,t,n),e=e.sibling}var ve=null,dt=!1;function qt(e,t,n){for(n=n.child;n!==null;)kh(e,t,n),n=n.sibling}function kh(e,t,n){if(jt&&typeof jt.onCommitFiberUnmount=="function")try{jt.onCommitFiberUnmount(hl,n)}catch{}switch(n.tag){case 5:je||fr(n,t);case 6:var r=ve,s=dt;ve=null,qt(e,t,n),ve=r,dt=s,ve!==null&&(dt?(e=ve,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):ve.removeChild(n.stateNode));break;case 18:ve!==null&&(dt?(e=ve,n=n.stateNode,e.nodeType===8?ea(e.parentNode,n):e.nodeType===1&&ea(e,n),Ns(e)):ea(ve,n.stateNode));break;case 4:r=ve,s=dt,ve=n.stateNode.containerInfo,dt=!0,qt(e,t,n),ve=r,dt=s;break;case 0:case 11:case 14:case 15:if(!je&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){s=r=r.next;do{var i=s,l=i.destroy;i=i.tag,l!==void 0&&(i&2||i&4)&&eo(n,t,l),s=s.next}while(s!==r)}qt(e,t,n);break;case 1:if(!je&&(fr(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(a){re(n,t,a)}qt(e,t,n);break;case 21:qt(e,t,n);break;case 22:n.mode&1?(je=(r=je)||n.memoizedState!==null,qt(e,t,n),je=r):qt(e,t,n);break;default:qt(e,t,n)}}function $c(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new Ty),t.forEach(function(r){var s=Ay.bind(null,e,r);n.has(r)||(n.add(r),r.then(s,s))})}}function ot(e,t){var n=t.deletions;if(n!==null)for(var r=0;rs&&(s=l),r&=~i}if(r=s,r=ie()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*by(r/1960))-r,10e?16:e,dn===null)var r=!1;else{if(e=dn,dn=null,ll=0,Q&6)throw Error(j(331));var s=Q;for(Q|=4,L=e.current;L!==null;){var i=L,l=i.child;if(L.flags&16){var a=i.deletions;if(a!==null){for(var u=0;uie()-pu?Hn(e,0):hu|=n),Ae(e,t)}function Lh(e,t){t===0&&(e.mode&1?(t=ci,ci<<=1,!(ci&130023424)&&(ci=4194304)):t=1);var n=Fe();e=Bt(e,t),e!==null&&(Ws(e,t,n),Ae(e,n))}function Uy(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Lh(e,n)}function Ay(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,s=e.memoizedState;s!==null&&(n=s.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(j(314))}r!==null&&r.delete(t),Lh(e,n)}var Th;Th=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||ze.current)$e=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return $e=!1,Ry(e,t,n);$e=!!(e.flags&131072)}else $e=!1,Y&&t.flags&1048576&&Mf(t,Xi,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;Fi(e,t),e=t.pendingProps;var s=br(t,Pe.current);gr(t,n),s=au(null,t,r,e,s,n);var i=ou();return t.flags|=1,typeof s=="object"&&s!==null&&typeof s.render=="function"&&s.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,Ue(r)?(i=!0,Gi(t)):i=!1,t.memoizedState=s.state!==null&&s.state!==void 0?s.state:null,nu(t),s.updater=wl,t.stateNode=s,s._reactInternals=t,Va(t,r,e,n),t=Ya(null,t,r,!0,i,n)):(t.tag=0,Y&&i&&Go(t),Le(null,t,s,n),t=t.child),t;case 16:r=t.elementType;e:{switch(Fi(e,t),e=t.pendingProps,s=r._init,r=s(r._payload),t.type=r,s=t.tag=By(r),e=ut(r,e),s){case 0:t=Ga(null,t,r,e,n);break e;case 1:t=Oc(null,t,r,e,n);break e;case 11:t=Tc(null,t,r,e,n);break e;case 14:t=Fc(null,t,r,ut(r.type,e),n);break e}throw Error(j(306,r,""))}return t;case 0:return r=t.type,s=t.pendingProps,s=t.elementType===r?s:ut(r,s),Ga(e,t,r,s,n);case 1:return r=t.type,s=t.pendingProps,s=t.elementType===r?s:ut(r,s),Oc(e,t,r,s,n);case 3:e:{if(ph(t),e===null)throw Error(j(387));r=t.pendingProps,i=t.memoizedState,s=i.element,Af(e,t),el(t,r,null,n);var l=t.memoizedState;if(r=l.element,i.isDehydrated)if(i={element:r,isDehydrated:!1,cache:l.cache,pendingSuspenseBoundaries:l.pendingSuspenseBoundaries,transitions:l.transitions},t.updateQueue.baseState=i,t.memoizedState=i,t.flags&256){s=$r(Error(j(423)),t),t=bc(e,t,r,n,s);break e}else if(r!==s){s=$r(Error(j(424)),t),t=bc(e,t,r,n,s);break e}else for(We=mn(t.stateNode.containerInfo.firstChild),qe=t,Y=!0,ft=null,n=zf(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(Mr(),r===s){t=Ht(e,t,n);break e}Le(e,t,r,n)}t=t.child}return t;case 5:return Qf(t),e===null&&Ha(t),r=t.type,s=t.pendingProps,i=e!==null?e.memoizedProps:null,l=s.children,za(r,s)?l=null:i!==null&&za(r,i)&&(t.flags|=32),hh(e,t),Le(e,t,l,n),t.child;case 6:return e===null&&Ha(t),null;case 13:return mh(e,t,n);case 4:return ru(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=Dr(t,null,r,n):Le(e,t,r,n),t.child;case 11:return r=t.type,s=t.pendingProps,s=t.elementType===r?s:ut(r,s),Tc(e,t,r,s,n);case 7:return Le(e,t,t.pendingProps,n),t.child;case 8:return Le(e,t,t.pendingProps.children,n),t.child;case 12:return Le(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,s=t.pendingProps,i=t.memoizedProps,l=s.value,W(Ji,r._currentValue),r._currentValue=l,i!==null)if(yt(i.value,l)){if(i.children===s.children&&!ze.current){t=Ht(e,t,n);break e}}else for(i=t.child,i!==null&&(i.return=t);i!==null;){var a=i.dependencies;if(a!==null){l=i.child;for(var u=a.firstContext;u!==null;){if(u.context===r){if(i.tag===1){u=zt(-1,n&-n),u.tag=2;var c=i.updateQueue;if(c!==null){c=c.shared;var p=c.pending;p===null?u.next=u:(u.next=p.next,p.next=u),c.pending=u}}i.lanes|=n,u=i.alternate,u!==null&&(u.lanes|=n),Ka(i.return,n,t),a.lanes|=n;break}u=u.next}}else if(i.tag===10)l=i.type===t.type?null:i.child;else if(i.tag===18){if(l=i.return,l===null)throw Error(j(341));l.lanes|=n,a=l.alternate,a!==null&&(a.lanes|=n),Ka(l,n,t),l=i.sibling}else l=i.child;if(l!==null)l.return=i;else for(l=i;l!==null;){if(l===t){l=null;break}if(i=l.sibling,i!==null){i.return=l.return,l=i;break}l=l.return}i=l}Le(e,t,s.children,n),t=t.child}return t;case 9:return s=t.type,r=t.pendingProps.children,gr(t,n),s=st(s),r=r(s),t.flags|=1,Le(e,t,r,n),t.child;case 14:return r=t.type,s=ut(r,t.pendingProps),s=ut(r.type,s),Fc(e,t,r,s,n);case 15:return dh(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,s=t.pendingProps,s=t.elementType===r?s:ut(r,s),Fi(e,t),t.tag=1,Ue(r)?(e=!0,Gi(t)):e=!1,gr(t,n),oh(t,r,s),Va(t,r,s,n),Ya(null,t,r,!0,e,n);case 19:return yh(e,t,n);case 22:return fh(e,t,n)}throw Error(j(156,t.tag))};function Fh(e,t){return sf(e,t)}function Qy(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function nt(e,t,n,r){return new Qy(e,t,n,r)}function gu(e){return e=e.prototype,!(!e||!e.isReactComponent)}function By(e){if(typeof e=="function")return gu(e)?1:0;if(e!=null){if(e=e.$$typeof,e===Io)return 11;if(e===$o)return 14}return 2}function xn(e,t){var n=e.alternate;return n===null?(n=nt(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Mi(e,t,n,r,s,i){var l=2;if(r=e,typeof e=="function")gu(e)&&(l=1);else if(typeof e=="string")l=5;else e:switch(e){case rr:return Kn(n.children,s,i,t);case Do:l=8,s|=8;break;case va:return e=nt(12,n,t,s|2),e.elementType=va,e.lanes=i,e;case ga:return e=nt(13,n,t,s),e.elementType=ga,e.lanes=i,e;case xa:return e=nt(19,n,t,s),e.elementType=xa,e.lanes=i,e;case Ad:return El(n,s,i,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case zd:l=10;break e;case Ud:l=9;break e;case Io:l=11;break e;case $o:l=14;break e;case Yt:l=16,r=null;break e}throw Error(j(130,e==null?e:typeof e,""))}return t=nt(l,n,t,s),t.elementType=e,t.type=r,t.lanes=i,t}function Kn(e,t,n,r){return e=nt(7,e,r,t),e.lanes=n,e}function El(e,t,n,r){return e=nt(22,e,r,t),e.elementType=Ad,e.lanes=n,e.stateNode={isHidden:!1},e}function oa(e,t,n){return e=nt(6,e,null,t),e.lanes=n,e}function ua(e,t,n){return t=nt(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Hy(e,t,n,r,s){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Bl(0),this.expirationTimes=Bl(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Bl(0),this.identifierPrefix=r,this.onRecoverableError=s,this.mutableSourceEagerHydrationData=null}function xu(e,t,n,r,s,i,l,a,u){return e=new Hy(e,t,n,a,u),t===1?(t=1,i===!0&&(t|=8)):t=0,i=nt(3,null,null,t),e.current=i,i.stateNode=e,i.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},nu(i),e}function Ky(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(Dh)}catch(e){console.error(e)}}Dh(),Md.exports=Ye;var Yy=Md.exports,Wc=Yy;ma.createRoot=Wc.createRoot,ma.hydrateRoot=Wc.hydrateRoot;var Kr=class{constructor(){this.listeners=new Set,this.subscribe=this.subscribe.bind(this)}subscribe(e){return this.listeners.add(e),this.onSubscribe(),()=>{this.listeners.delete(e),this.onUnsubscribe()}}hasListeners(){return this.listeners.size>0}onSubscribe(){}onUnsubscribe(){}},Mn,en,wr,hd,Xy=(hd=class extends Kr{constructor(){super();F(this,Mn);F(this,en);F(this,wr);_(this,wr,t=>{if(typeof window<"u"&&window.addEventListener){const n=()=>t();return window.addEventListener("visibilitychange",n,!1),()=>{window.removeEventListener("visibilitychange",n)}}})}onSubscribe(){v(this,en)||this.setEventListener(v(this,wr))}onUnsubscribe(){var t;this.hasListeners()||((t=v(this,en))==null||t.call(this),_(this,en,void 0))}setEventListener(t){var n;_(this,wr,t),(n=v(this,en))==null||n.call(this),_(this,en,t(r=>{typeof r=="boolean"?this.setFocused(r):this.onFocus()}))}setFocused(t){v(this,Mn)!==t&&(_(this,Mn,t),this.onFocus())}onFocus(){const t=this.isFocused();this.listeners.forEach(n=>{n(t)})}isFocused(){var t;return typeof v(this,Mn)=="boolean"?v(this,Mn):((t=globalThis.document)==null?void 0:t.visibilityState)!=="hidden"}},Mn=new WeakMap,en=new WeakMap,wr=new WeakMap,hd),Eu=new Xy,Jy={setTimeout:(e,t)=>setTimeout(e,t),clearTimeout:e=>clearTimeout(e),setInterval:(e,t)=>setInterval(e,t),clearInterval:e=>clearInterval(e)},tn,Ro,pd,Zy=(pd=class{constructor(){F(this,tn,Jy);F(this,Ro,!1)}setTimeoutProvider(e){_(this,tn,e)}setTimeout(e,t){return v(this,tn).setTimeout(e,t)}clearTimeout(e){v(this,tn).clearTimeout(e)}setInterval(e,t){return v(this,tn).setInterval(e,t)}clearInterval(e){v(this,tn).clearInterval(e)}},tn=new WeakMap,Ro=new WeakMap,pd),bn=new Zy;function ev(e){setTimeout(e,0)}var tv=typeof window>"u"||"Deno"in globalThis;function Te(){}function nv(e,t){return typeof e=="function"?e(t):e}function oo(e){return typeof e=="number"&&e>=0&&e!==1/0}function Ih(e,t){return Math.max(e+(t||0)-Date.now(),0)}function wn(e,t){return typeof e=="function"?e(t):e}function Ke(e,t){return typeof e=="function"?e(t):e}function Vc(e,t){const{type:n="all",exact:r,fetchStatus:s,predicate:i,queryKey:l,stale:a}=e;if(l){if(r){if(t.queryHash!==Cu(l,t.options))return!1}else if(!$s(t.queryKey,l))return!1}if(n!=="all"){const u=t.isActive();if(n==="active"&&!u||n==="inactive"&&u)return!1}return!(typeof a=="boolean"&&t.isStale()!==a||s&&s!==t.state.fetchStatus||i&&!i(t))}function qc(e,t){const{exact:n,status:r,predicate:s,mutationKey:i}=e;if(i){if(!t.options.mutationKey)return!1;if(n){if(Xn(t.options.mutationKey)!==Xn(i))return!1}else if(!$s(t.options.mutationKey,i))return!1}return!(r&&t.state.status!==r||s&&!s(t))}function Cu(e,t){return((t==null?void 0:t.queryKeyHashFn)||Xn)(e)}function Xn(e){return JSON.stringify(e,(t,n)=>uo(n)?Object.keys(n).sort().reduce((r,s)=>(r[s]=n[s],r),{}):n)}function $s(e,t){return e===t?!0:typeof e!=typeof t?!1:e&&t&&typeof e=="object"&&typeof t=="object"?Object.keys(t).every(n=>$s(e[n],t[n])):!1}var rv=Object.prototype.hasOwnProperty;function $h(e,t,n=0){if(e===t)return e;if(n>500)return t;const r=Gc(e)&&Gc(t);if(!r&&!(uo(e)&&uo(t)))return t;const i=(r?e:Object.keys(e)).length,l=r?t:Object.keys(t),a=l.length,u=r?new Array(a):{};let c=0;for(let p=0;p{bn.setTimeout(t,e)})}function co(e,t,n){return typeof n.structuralSharing=="function"?n.structuralSharing(e,t):n.structuralSharing!==!1?$h(e,t):t}function iv(e,t,n=0){const r=[...e,t];return n&&r.length>n?r.slice(1):r}function lv(e,t,n=0){const r=[t,...e];return n&&r.length>n?r.slice(0,-1):r}var ju=Symbol();function zh(e,t){return!e.queryFn&&(t!=null&&t.initialPromise)?()=>t.initialPromise:!e.queryFn||e.queryFn===ju?()=>Promise.reject(new Error(`Missing queryFn: '${e.queryHash}'`)):e.queryFn}function Nu(e,t){return typeof e=="function"?e(...t):!!e}function av(e,t,n){let r=!1,s;return Object.defineProperty(e,"signal",{enumerable:!0,get:()=>(s??(s=t()),r||(r=!0,s.aborted?n():s.addEventListener("abort",n,{once:!0})),s)}),e}var zs=(()=>{let e=()=>tv;return{isServer(){return e()},setIsServer(t){e=t}}})();function fo(){let e,t;const n=new Promise((s,i)=>{e=s,t=i});n.status="pending",n.catch(()=>{});function r(s){Object.assign(n,s),delete n.resolve,delete n.reject}return n.resolve=s=>{r({status:"fulfilled",value:s}),e(s)},n.reject=s=>{r({status:"rejected",reason:s}),t(s)},n}var ov=ev;function uv(){let e=[],t=0,n=a=>{a()},r=a=>{a()},s=ov;const i=a=>{t?e.push(a):s(()=>{n(a)})},l=()=>{const a=e;e=[],a.length&&s(()=>{r(()=>{a.forEach(u=>{n(u)})})})};return{batch:a=>{let u;t++;try{u=a()}finally{t--,t||l()}return u},batchCalls:a=>(...u)=>{i(()=>{a(...u)})},schedule:i,setNotifyFunction:a=>{n=a},setBatchNotifyFunction:a=>{r=a},setScheduler:a=>{s=a}}}var ue=uv(),Sr,nn,kr,md,cv=(md=class extends Kr{constructor(){super();F(this,Sr,!0);F(this,nn);F(this,kr);_(this,kr,t=>{if(typeof window<"u"&&window.addEventListener){const n=()=>t(!0),r=()=>t(!1);return window.addEventListener("online",n,!1),window.addEventListener("offline",r,!1),()=>{window.removeEventListener("online",n),window.removeEventListener("offline",r)}}})}onSubscribe(){v(this,nn)||this.setEventListener(v(this,kr))}onUnsubscribe(){var t;this.hasListeners()||((t=v(this,nn))==null||t.call(this),_(this,nn,void 0))}setEventListener(t){var n;_(this,kr,t),(n=v(this,nn))==null||n.call(this),_(this,nn,t(this.setOnline.bind(this)))}setOnline(t){v(this,Sr)!==t&&(_(this,Sr,t),this.listeners.forEach(r=>{r(t)}))}isOnline(){return v(this,Sr)}},Sr=new WeakMap,nn=new WeakMap,kr=new WeakMap,md),cl=new cv;function dv(e){return Math.min(1e3*2**e,3e4)}function Uh(e){return(e??"online")==="online"?cl.isOnline():!0}var ho=class extends Error{constructor(e){super("CancelledError"),this.revert=e==null?void 0:e.revert,this.silent=e==null?void 0:e.silent}};function Ah(e){let t=!1,n=0,r;const s=fo(),i=()=>s.status!=="pending",l=S=>{var E;if(!i()){const f=new ho(S);m(f),(E=e.onCancel)==null||E.call(e,f)}},a=()=>{t=!0},u=()=>{t=!1},c=()=>Eu.isFocused()&&(e.networkMode==="always"||cl.isOnline())&&e.canRun(),p=()=>Uh(e.networkMode)&&e.canRun(),d=S=>{i()||(r==null||r(),s.resolve(S))},m=S=>{i()||(r==null||r(),s.reject(S))},x=()=>new Promise(S=>{var E;r=f=>{(i()||c())&&S(f)},(E=e.onPause)==null||E.call(e)}).then(()=>{var S;r=void 0,i()||(S=e.onContinue)==null||S.call(e)}),g=()=>{if(i())return;let S;const E=n===0?e.initialPromise:void 0;try{S=E??e.fn()}catch(f){S=Promise.reject(f)}Promise.resolve(S).then(d).catch(f=>{var P;if(i())return;const h=e.retry??(zs.isServer()?0:3),y=e.retryDelay??dv,k=typeof y=="function"?y(n,f):y,C=h===!0||typeof h=="number"&&nc()?void 0:x()).then(()=>{t?m(f):g()})})};return{promise:s,status:()=>s.status,cancel:l,continue:()=>(r==null||r(),s),cancelRetry:a,continueRetry:u,canStart:p,start:()=>(p()?g():x().then(g),s)}}var Dn,yd,Qh=(yd=class{constructor(){F(this,Dn)}destroy(){this.clearGcTimeout()}scheduleGc(){this.clearGcTimeout(),oo(this.gcTime)&&_(this,Dn,bn.setTimeout(()=>{this.optionalRemove()},this.gcTime))}updateGcTime(e){this.gcTime=Math.max(this.gcTime||0,e??(zs.isServer()?1/0:5*60*1e3))}clearGcTimeout(){v(this,Dn)!==void 0&&(bn.clearTimeout(v(this,Dn)),_(this,Dn,void 0))}},Dn=new WeakMap,yd);function fv(e){return{onFetch:(t,n)=>{var p,d,m,x,g;const r=t.options,s=(m=(d=(p=t.fetchOptions)==null?void 0:p.meta)==null?void 0:d.fetchMore)==null?void 0:m.direction,i=((x=t.state.data)==null?void 0:x.pages)||[],l=((g=t.state.data)==null?void 0:g.pageParams)||[];let a={pages:[],pageParams:[]},u=0;const c=async()=>{let S=!1;const E=y=>{av(y,()=>t.signal,()=>S=!0)},f=zh(t.options,t.fetchOptions),h=async(y,k,C)=>{if(S)return Promise.reject(t.signal.reason);if(k==null&&y.pages.length)return Promise.resolve(y);const R=(()=>{const te={client:t.client,queryKey:t.queryKey,pageParam:k,direction:C?"backward":"forward",meta:t.options.meta};return E(te),te})(),N=await f(R),{maxPages:b}=t.options,O=C?lv:iv;return{pages:O(y.pages,N,b),pageParams:O(y.pageParams,k,b)}};if(s&&i.length){const y=s==="backward",k=y?hv:Xc,C={pages:i,pageParams:l},P=k(r,C);a=await h(C,P,y)}else{const y=e??i.length;do{const k=u===0?l[0]??r.initialPageParam:Xc(r,a);if(u>0&&k==null)break;a=await h(a,k),u++}while(u{var S,E;return(E=(S=t.options).persister)==null?void 0:E.call(S,c,{client:t.client,queryKey:t.queryKey,meta:t.options.meta,signal:t.signal},n)}:t.fetchFn=c}}}function Xc(e,{pages:t,pageParams:n}){const r=t.length-1;return t.length>0?e.getNextPageParam(t[r],t,n[r],n):void 0}function hv(e,{pages:t,pageParams:n}){var r;return t.length>0?(r=e.getPreviousPageParam)==null?void 0:r.call(e,t[0],t,n[0],n):void 0}var Er,In,Cr,Ze,$n,fe,Us,zn,He,Bh,Lt,vd,pv=(vd=class extends Qh{constructor(t){super();F(this,He);F(this,Er);F(this,In);F(this,Cr);F(this,Ze);F(this,$n);F(this,fe);F(this,Us);F(this,zn);_(this,zn,!1),_(this,Us,t.defaultOptions),this.setOptions(t.options),this.observers=[],_(this,$n,t.client),_(this,Ze,v(this,$n).getQueryCache()),this.queryKey=t.queryKey,this.queryHash=t.queryHash,_(this,In,Zc(this.options)),this.state=t.state??v(this,In),this.scheduleGc()}get meta(){return this.options.meta}get queryType(){return v(this,Er)}get promise(){var t;return(t=v(this,fe))==null?void 0:t.promise}setOptions(t){if(this.options={...v(this,Us),...t},t!=null&&t._type&&_(this,Er,t._type),this.updateGcTime(this.options.gcTime),this.state&&this.state.data===void 0){const n=Zc(this.options);n.data!==void 0&&(this.setState(Jc(n.data,n.dataUpdatedAt)),_(this,In,n))}}optionalRemove(){!this.observers.length&&this.state.fetchStatus==="idle"&&v(this,Ze).remove(this)}setData(t,n){const r=co(this.state.data,t,this.options);return I(this,He,Lt).call(this,{data:r,type:"success",dataUpdatedAt:n==null?void 0:n.updatedAt,manual:n==null?void 0:n.manual}),r}setState(t){I(this,He,Lt).call(this,{type:"setState",state:t})}cancel(t){var r,s;const n=(r=v(this,fe))==null?void 0:r.promise;return(s=v(this,fe))==null||s.cancel(t),n?n.then(Te).catch(Te):Promise.resolve()}destroy(){super.destroy(),this.cancel({silent:!0})}get resetState(){return v(this,In)}reset(){this.destroy(),this.setState(this.resetState)}isActive(){return this.observers.some(t=>Ke(t.options.enabled,this)!==!1)}isDisabled(){return this.getObserversCount()>0?!this.isActive():this.options.queryFn===ju||!this.isFetched()}isFetched(){return this.state.dataUpdateCount+this.state.errorUpdateCount>0}isStatic(){return this.getObserversCount()>0?this.observers.some(t=>wn(t.options.staleTime,this)==="static"):!1}isStale(){return this.getObserversCount()>0?this.observers.some(t=>t.getCurrentResult().isStale):this.state.data===void 0||this.state.isInvalidated}isStaleByTime(t=0){return this.state.data===void 0?!0:t==="static"?!1:this.state.isInvalidated?!0:!Ih(this.state.dataUpdatedAt,t)}onFocus(){var n;const t=this.observers.find(r=>r.shouldFetchOnWindowFocus());t==null||t.refetch({cancelRefetch:!1}),(n=v(this,fe))==null||n.continue()}onOnline(){var n;const t=this.observers.find(r=>r.shouldFetchOnReconnect());t==null||t.refetch({cancelRefetch:!1}),(n=v(this,fe))==null||n.continue()}addObserver(t){this.observers.includes(t)||(this.observers.push(t),this.clearGcTimeout(),v(this,Ze).notify({type:"observerAdded",query:this,observer:t}))}removeObserver(t){this.observers.includes(t)&&(this.observers=this.observers.filter(n=>n!==t),this.observers.length||(v(this,fe)&&(v(this,zn)||I(this,He,Bh).call(this)?v(this,fe).cancel({revert:!0}):v(this,fe).cancelRetry()),this.scheduleGc()),v(this,Ze).notify({type:"observerRemoved",query:this,observer:t}))}getObserversCount(){return this.observers.length}invalidate(){this.state.isInvalidated||I(this,He,Lt).call(this,{type:"invalidate"})}async fetch(t,n){var c,p,d,m,x,g,S,E,f,h,y;if(this.state.fetchStatus!=="idle"&&((c=v(this,fe))==null?void 0:c.status())!=="rejected"){if(this.state.data!==void 0&&(n!=null&&n.cancelRefetch))this.cancel({silent:!0});else if(v(this,fe))return v(this,fe).continueRetry(),v(this,fe).promise}if(t&&this.setOptions(t),!this.options.queryFn){const k=this.observers.find(C=>C.options.queryFn);k&&this.setOptions(k.options)}const r=new AbortController,s=k=>{Object.defineProperty(k,"signal",{enumerable:!0,get:()=>(_(this,zn,!0),r.signal)})},i=()=>{const k=zh(this.options,n),P=(()=>{const R={client:v(this,$n),queryKey:this.queryKey,meta:this.meta};return s(R),R})();return _(this,zn,!1),this.options.persister?this.options.persister(k,P,this):k(P)},a=(()=>{const k={fetchOptions:n,options:this.options,queryKey:this.queryKey,client:v(this,$n),state:this.state,fetchFn:i};return s(k),k})(),u=v(this,Er)==="infinite"?fv(this.options.pages):this.options.behavior;u==null||u.onFetch(a,this),_(this,Cr,this.state),(this.state.fetchStatus==="idle"||this.state.fetchMeta!==((p=a.fetchOptions)==null?void 0:p.meta))&&I(this,He,Lt).call(this,{type:"fetch",meta:(d=a.fetchOptions)==null?void 0:d.meta}),_(this,fe,Ah({initialPromise:n==null?void 0:n.initialPromise,fn:a.fetchFn,onCancel:k=>{k instanceof ho&&k.revert&&this.setState({...v(this,Cr),fetchStatus:"idle"}),r.abort()},onFail:(k,C)=>{I(this,He,Lt).call(this,{type:"failed",failureCount:k,error:C})},onPause:()=>{I(this,He,Lt).call(this,{type:"pause"})},onContinue:()=>{I(this,He,Lt).call(this,{type:"continue"})},retry:a.options.retry,retryDelay:a.options.retryDelay,networkMode:a.options.networkMode,canRun:()=>!0}));try{const k=await v(this,fe).start();if(k===void 0)throw new Error(`${this.queryHash} data is undefined`);return this.setData(k),(x=(m=v(this,Ze).config).onSuccess)==null||x.call(m,k,this),(S=(g=v(this,Ze).config).onSettled)==null||S.call(g,k,this.state.error,this),k}catch(k){if(k instanceof ho){if(k.silent)return v(this,fe).promise;if(k.revert){if(this.state.data===void 0)throw k;return this.state.data}}throw I(this,He,Lt).call(this,{type:"error",error:k}),(f=(E=v(this,Ze).config).onError)==null||f.call(E,k,this),(y=(h=v(this,Ze).config).onSettled)==null||y.call(h,this.state.data,k,this),k}finally{this.scheduleGc()}}},Er=new WeakMap,In=new WeakMap,Cr=new WeakMap,Ze=new WeakMap,$n=new WeakMap,fe=new WeakMap,Us=new WeakMap,zn=new WeakMap,He=new WeakSet,Bh=function(){return this.state.fetchStatus==="paused"&&this.state.status==="pending"},Lt=function(t){const n=r=>{switch(t.type){case"failed":return{...r,fetchFailureCount:t.failureCount,fetchFailureReason:t.error};case"pause":return{...r,fetchStatus:"paused"};case"continue":return{...r,fetchStatus:"fetching"};case"fetch":return{...r,...Hh(r.data,this.options),fetchMeta:t.meta??null};case"success":const s={...r,...Jc(t.data,t.dataUpdatedAt),dataUpdateCount:r.dataUpdateCount+1,...!t.manual&&{fetchStatus:"idle",fetchFailureCount:0,fetchFailureReason:null}};return _(this,Cr,t.manual?s:void 0),s;case"error":const i=t.error;return{...r,error:i,errorUpdateCount:r.errorUpdateCount+1,errorUpdatedAt:Date.now(),fetchFailureCount:r.fetchFailureCount+1,fetchFailureReason:i,fetchStatus:"idle",status:"error",isInvalidated:!0};case"invalidate":return{...r,isInvalidated:!0};case"setState":return{...r,...t.state}}};this.state=n(this.state),ue.batch(()=>{this.observers.forEach(r=>{r.onQueryUpdate()}),v(this,Ze).notify({query:this,type:"updated",action:t})})},vd);function Hh(e,t){return{fetchFailureCount:0,fetchFailureReason:null,fetchStatus:Uh(t.networkMode)?"fetching":"paused",...e===void 0&&{error:null,status:"pending"}}}function Jc(e,t){return{data:e,dataUpdatedAt:t??Date.now(),error:null,isInvalidated:!1,status:"success"}}function Zc(e){const t=typeof e.initialData=="function"?e.initialData():e.initialData,n=t!==void 0,r=n?typeof e.initialDataUpdatedAt=="function"?e.initialDataUpdatedAt():e.initialDataUpdatedAt:0;return{data:t,dataUpdateCount:0,dataUpdatedAt:n?r??Date.now():0,error:null,errorUpdateCount:0,errorUpdatedAt:0,fetchFailureCount:0,fetchFailureReason:null,fetchMeta:null,isInvalidated:!1,status:n?"success":"pending",fetchStatus:"idle"}}var Me,A,As,_e,Un,jr,Ot,rn,Qs,Nr,Pr,An,Qn,sn,Rr,B,cs,po,mo,yo,vo,go,xo,wo,Kh,gd,mv=(gd=class extends Kr{constructor(t,n){super();F(this,B);F(this,Me);F(this,A);F(this,As);F(this,_e);F(this,Un);F(this,jr);F(this,Ot);F(this,rn);F(this,Qs);F(this,Nr);F(this,Pr);F(this,An);F(this,Qn);F(this,sn);F(this,Rr,new Set);this.options=n,_(this,Me,t),_(this,rn,null),_(this,Ot,fo()),this.bindMethods(),this.setOptions(n)}bindMethods(){this.refetch=this.refetch.bind(this)}onSubscribe(){this.listeners.size===1&&(v(this,A).addObserver(this),ed(v(this,A),this.options)?I(this,B,cs).call(this):this.updateResult(),I(this,B,vo).call(this))}onUnsubscribe(){this.hasListeners()||this.destroy()}shouldFetchOnReconnect(){return So(v(this,A),this.options,this.options.refetchOnReconnect)}shouldFetchOnWindowFocus(){return So(v(this,A),this.options,this.options.refetchOnWindowFocus)}destroy(){this.listeners=new Set,I(this,B,go).call(this),I(this,B,xo).call(this),v(this,A).removeObserver(this)}setOptions(t){const n=this.options,r=v(this,A);if(this.options=v(this,Me).defaultQueryOptions(t),this.options.enabled!==void 0&&typeof this.options.enabled!="boolean"&&typeof this.options.enabled!="function"&&typeof Ke(this.options.enabled,v(this,A))!="boolean")throw new Error("Expected enabled to be a boolean or a callback that returns a boolean");I(this,B,wo).call(this),v(this,A).setOptions(this.options),n._defaulted&&!ul(this.options,n)&&v(this,Me).getQueryCache().notify({type:"observerOptionsUpdated",query:v(this,A),observer:this});const s=this.hasListeners();s&&td(v(this,A),r,this.options,n)&&I(this,B,cs).call(this),this.updateResult(),s&&(v(this,A)!==r||Ke(this.options.enabled,v(this,A))!==Ke(n.enabled,v(this,A))||wn(this.options.staleTime,v(this,A))!==wn(n.staleTime,v(this,A)))&&I(this,B,po).call(this);const i=I(this,B,mo).call(this);s&&(v(this,A)!==r||Ke(this.options.enabled,v(this,A))!==Ke(n.enabled,v(this,A))||i!==v(this,sn))&&I(this,B,yo).call(this,i)}getOptimisticResult(t){const n=v(this,Me).getQueryCache().build(v(this,Me),t),r=this.createResult(n,t);return vv(this,r)&&(_(this,_e,r),_(this,jr,this.options),_(this,Un,v(this,A).state)),r}getCurrentResult(){return v(this,_e)}trackResult(t,n){return new Proxy(t,{get:(r,s)=>(this.trackProp(s),n==null||n(s),s==="promise"&&(this.trackProp("data"),!this.options.experimental_prefetchInRender&&v(this,Ot).status==="pending"&&v(this,Ot).reject(new Error("experimental_prefetchInRender feature flag is not enabled"))),Reflect.get(r,s))})}trackProp(t){v(this,Rr).add(t)}getCurrentQuery(){return v(this,A)}refetch({...t}={}){return this.fetch({...t})}fetchOptimistic(t){const n=v(this,Me).defaultQueryOptions(t),r=v(this,Me).getQueryCache().build(v(this,Me),n);return r.fetch().then(()=>this.createResult(r,n))}fetch(t){return I(this,B,cs).call(this,{...t,cancelRefetch:t.cancelRefetch??!0}).then(()=>(this.updateResult(),v(this,_e)))}createResult(t,n){var b;const r=v(this,A),s=this.options,i=v(this,_e),l=v(this,Un),a=v(this,jr),c=t!==r?t.state:v(this,As),{state:p}=t;let d={...p},m=!1,x;if(n._optimisticResults){const O=this.hasListeners(),te=!O&&ed(t,n),Je=O&&td(t,r,n,s);(te||Je)&&(d={...d,...Hh(p.data,t.options)}),n._optimisticResults==="isRestoring"&&(d.fetchStatus="idle")}let{error:g,errorUpdatedAt:S,status:E}=d;x=d.data;let f=!1;if(n.placeholderData!==void 0&&x===void 0&&E==="pending"){let O;i!=null&&i.isPlaceholderData&&n.placeholderData===(a==null?void 0:a.placeholderData)?(O=i.data,f=!0):O=typeof n.placeholderData=="function"?n.placeholderData((b=v(this,Pr))==null?void 0:b.state.data,v(this,Pr)):n.placeholderData,O!==void 0&&(E="success",x=co(i==null?void 0:i.data,O,n),m=!0)}if(n.select&&x!==void 0&&!f)if(i&&x===(l==null?void 0:l.data)&&n.select===v(this,Qs))x=v(this,Nr);else try{_(this,Qs,n.select),x=n.select(x),x=co(i==null?void 0:i.data,x,n),_(this,Nr,x),_(this,rn,null)}catch(O){_(this,rn,O)}v(this,rn)&&(g=v(this,rn),x=v(this,Nr),S=Date.now(),E="error");const h=d.fetchStatus==="fetching",y=E==="pending",k=E==="error",C=y&&h,P=x!==void 0,N={status:E,fetchStatus:d.fetchStatus,isPending:y,isSuccess:E==="success",isError:k,isInitialLoading:C,isLoading:C,data:x,dataUpdatedAt:d.dataUpdatedAt,error:g,errorUpdatedAt:S,failureCount:d.fetchFailureCount,failureReason:d.fetchFailureReason,errorUpdateCount:d.errorUpdateCount,isFetched:t.isFetched(),isFetchedAfterMount:d.dataUpdateCount>c.dataUpdateCount||d.errorUpdateCount>c.errorUpdateCount,isFetching:h,isRefetching:h&&!y,isLoadingError:k&&!P,isPaused:d.fetchStatus==="paused",isPlaceholderData:m,isRefetchError:k&&P,isStale:Pu(t,n),refetch:this.refetch,promise:v(this,Ot),isEnabled:Ke(n.enabled,t)!==!1};if(this.options.experimental_prefetchInRender){const O=N.data!==void 0,te=N.status==="error"&&!O,Je=Re=>{te?Re.reject(N.error):O&&Re.resolve(N.data)},at=()=>{const Re=_(this,Ot,N.promise=fo());Je(Re)},gt=v(this,Ot);switch(gt.status){case"pending":t.queryHash===r.queryHash&&Je(gt);break;case"fulfilled":(te||N.data!==gt.value)&&at();break;case"rejected":(!te||N.error!==gt.reason)&&at();break}}return N}updateResult(){const t=v(this,_e),n=this.createResult(v(this,A),this.options);if(_(this,Un,v(this,A).state),_(this,jr,this.options),v(this,Un).data!==void 0&&_(this,Pr,v(this,A)),ul(n,t))return;_(this,_e,n);const r=()=>{if(!t)return!0;const{notifyOnChangeProps:s}=this.options,i=typeof s=="function"?s():s;if(i==="all"||!i&&!v(this,Rr).size)return!0;const l=new Set(i??v(this,Rr));return this.options.throwOnError&&l.add("error"),Object.keys(v(this,_e)).some(a=>{const u=a;return v(this,_e)[u]!==t[u]&&l.has(u)})};I(this,B,Kh).call(this,{listeners:r()})}onQueryUpdate(){this.updateResult(),this.hasListeners()&&I(this,B,vo).call(this)}},Me=new WeakMap,A=new WeakMap,As=new WeakMap,_e=new WeakMap,Un=new WeakMap,jr=new WeakMap,Ot=new WeakMap,rn=new WeakMap,Qs=new WeakMap,Nr=new WeakMap,Pr=new WeakMap,An=new WeakMap,Qn=new WeakMap,sn=new WeakMap,Rr=new WeakMap,B=new WeakSet,cs=function(t){I(this,B,wo).call(this);let n=v(this,A).fetch(this.options,t);return t!=null&&t.throwOnError||(n=n.catch(Te)),n},po=function(){I(this,B,go).call(this);const t=wn(this.options.staleTime,v(this,A));if(zs.isServer()||v(this,_e).isStale||!oo(t))return;const r=Ih(v(this,_e).dataUpdatedAt,t)+1;_(this,An,bn.setTimeout(()=>{v(this,_e).isStale||this.updateResult()},r))},mo=function(){return(typeof this.options.refetchInterval=="function"?this.options.refetchInterval(v(this,A)):this.options.refetchInterval)??!1},yo=function(t){I(this,B,xo).call(this),_(this,sn,t),!(zs.isServer()||Ke(this.options.enabled,v(this,A))===!1||!oo(v(this,sn))||v(this,sn)===0)&&_(this,Qn,bn.setInterval(()=>{(this.options.refetchIntervalInBackground||Eu.isFocused())&&I(this,B,cs).call(this)},v(this,sn)))},vo=function(){I(this,B,po).call(this),I(this,B,yo).call(this,I(this,B,mo).call(this))},go=function(){v(this,An)!==void 0&&(bn.clearTimeout(v(this,An)),_(this,An,void 0))},xo=function(){v(this,Qn)!==void 0&&(bn.clearInterval(v(this,Qn)),_(this,Qn,void 0))},wo=function(){const t=v(this,Me).getQueryCache().build(v(this,Me),this.options);if(t===v(this,A))return;const n=v(this,A);_(this,A,t),_(this,As,t.state),this.hasListeners()&&(n==null||n.removeObserver(this),t.addObserver(this))},Kh=function(t){ue.batch(()=>{t.listeners&&this.listeners.forEach(n=>{n(v(this,_e))}),v(this,Me).getQueryCache().notify({query:v(this,A),type:"observerResultsUpdated"})})},gd);function yv(e,t){return Ke(t.enabled,e)!==!1&&e.state.data===void 0&&!(e.state.status==="error"&&Ke(t.retryOnMount,e)===!1)}function ed(e,t){return yv(e,t)||e.state.data!==void 0&&So(e,t,t.refetchOnMount)}function So(e,t,n){if(Ke(t.enabled,e)!==!1&&wn(t.staleTime,e)!=="static"){const r=typeof n=="function"?n(e):n;return r==="always"||r!==!1&&Pu(e,t)}return!1}function td(e,t,n,r){return(e!==t||Ke(r.enabled,e)===!1)&&(!n.suspense||e.state.status!=="error")&&Pu(e,n)}function Pu(e,t){return Ke(t.enabled,e)!==!1&&e.isStaleByTime(wn(t.staleTime,e))}function vv(e,t){return!ul(e.getCurrentResult(),t)}var Bs,St,Ce,Bn,kt,Gt,xd,gv=(xd=class extends Qh{constructor(t){super();F(this,kt);F(this,Bs);F(this,St);F(this,Ce);F(this,Bn);_(this,Bs,t.client),this.mutationId=t.mutationId,_(this,Ce,t.mutationCache),_(this,St,[]),this.state=t.state||Wh(),this.setOptions(t.options),this.scheduleGc()}setOptions(t){this.options=t,this.updateGcTime(this.options.gcTime)}get meta(){return this.options.meta}addObserver(t){v(this,St).includes(t)||(v(this,St).push(t),this.clearGcTimeout(),v(this,Ce).notify({type:"observerAdded",mutation:this,observer:t}))}removeObserver(t){_(this,St,v(this,St).filter(n=>n!==t)),this.scheduleGc(),v(this,Ce).notify({type:"observerRemoved",mutation:this,observer:t})}optionalRemove(){v(this,St).length||(this.state.status==="pending"?this.scheduleGc():v(this,Ce).remove(this))}continue(){var t;return((t=v(this,Bn))==null?void 0:t.continue())??this.execute(this.state.variables)}async execute(t){var l,a,u,c,p,d,m,x,g,S,E,f,h,y,k,C,P,R;const n=()=>{I(this,kt,Gt).call(this,{type:"continue"})},r={client:v(this,Bs),meta:this.options.meta,mutationKey:this.options.mutationKey};_(this,Bn,Ah({fn:()=>this.options.mutationFn?this.options.mutationFn(t,r):Promise.reject(new Error("No mutationFn found")),onFail:(N,b)=>{I(this,kt,Gt).call(this,{type:"failed",failureCount:N,error:b})},onPause:()=>{I(this,kt,Gt).call(this,{type:"pause"})},onContinue:n,retry:this.options.retry??0,retryDelay:this.options.retryDelay,networkMode:this.options.networkMode,canRun:()=>v(this,Ce).canRun(this)}));const s=this.state.status==="pending",i=!v(this,Bn).canStart();try{if(s)n();else{I(this,kt,Gt).call(this,{type:"pending",variables:t,isPaused:i}),v(this,Ce).config.onMutate&&await v(this,Ce).config.onMutate(t,this,r);const b=await((a=(l=this.options).onMutate)==null?void 0:a.call(l,t,r));b!==this.state.context&&I(this,kt,Gt).call(this,{type:"pending",context:b,variables:t,isPaused:i})}const N=await v(this,Bn).start();return await((c=(u=v(this,Ce).config).onSuccess)==null?void 0:c.call(u,N,t,this.state.context,this,r)),await((d=(p=this.options).onSuccess)==null?void 0:d.call(p,N,t,this.state.context,r)),await((x=(m=v(this,Ce).config).onSettled)==null?void 0:x.call(m,N,null,this.state.variables,this.state.context,this,r)),await((S=(g=this.options).onSettled)==null?void 0:S.call(g,N,null,t,this.state.context,r)),I(this,kt,Gt).call(this,{type:"success",data:N}),N}catch(N){try{await((f=(E=v(this,Ce).config).onError)==null?void 0:f.call(E,N,t,this.state.context,this,r))}catch(b){Promise.reject(b)}try{await((y=(h=this.options).onError)==null?void 0:y.call(h,N,t,this.state.context,r))}catch(b){Promise.reject(b)}try{await((C=(k=v(this,Ce).config).onSettled)==null?void 0:C.call(k,void 0,N,this.state.variables,this.state.context,this,r))}catch(b){Promise.reject(b)}try{await((R=(P=this.options).onSettled)==null?void 0:R.call(P,void 0,N,t,this.state.context,r))}catch(b){Promise.reject(b)}throw I(this,kt,Gt).call(this,{type:"error",error:N}),N}finally{v(this,Ce).runNext(this)}}},Bs=new WeakMap,St=new WeakMap,Ce=new WeakMap,Bn=new WeakMap,kt=new WeakSet,Gt=function(t){const n=r=>{switch(t.type){case"failed":return{...r,failureCount:t.failureCount,failureReason:t.error};case"pause":return{...r,isPaused:!0};case"continue":return{...r,isPaused:!1};case"pending":return{...r,context:t.context,data:void 0,failureCount:0,failureReason:null,error:null,isPaused:t.isPaused,status:"pending",variables:t.variables,submittedAt:Date.now()};case"success":return{...r,data:t.data,failureCount:0,failureReason:null,error:null,status:"success",isPaused:!1};case"error":return{...r,data:void 0,error:t.error,failureCount:r.failureCount+1,failureReason:t.error,isPaused:!1,status:"error"}}};this.state=n(this.state),ue.batch(()=>{v(this,St).forEach(r=>{r.onMutationUpdate(t)}),v(this,Ce).notify({mutation:this,type:"updated",action:t})})},xd);function Wh(){return{context:void 0,data:void 0,error:null,failureCount:0,failureReason:null,isPaused:!1,status:"idle",variables:void 0,submittedAt:0}}var bt,ct,Hs,wd,xv=(wd=class extends Kr{constructor(t={}){super();F(this,bt);F(this,ct);F(this,Hs);this.config=t,_(this,bt,new Set),_(this,ct,new Map),_(this,Hs,0)}build(t,n,r){const s=new gv({client:t,mutationCache:this,mutationId:++si(this,Hs)._,options:t.defaultMutationOptions(n),state:r});return this.add(s),s}add(t){v(this,bt).add(t);const n=ki(t);if(typeof n=="string"){const r=v(this,ct).get(n);r?r.push(t):v(this,ct).set(n,[t])}this.notify({type:"added",mutation:t})}remove(t){if(v(this,bt).delete(t)){const n=ki(t);if(typeof n=="string"){const r=v(this,ct).get(n);if(r)if(r.length>1){const s=r.indexOf(t);s!==-1&&r.splice(s,1)}else r[0]===t&&v(this,ct).delete(n)}}this.notify({type:"removed",mutation:t})}canRun(t){const n=ki(t);if(typeof n=="string"){const r=v(this,ct).get(n),s=r==null?void 0:r.find(i=>i.state.status==="pending");return!s||s===t}else return!0}runNext(t){var r;const n=ki(t);if(typeof n=="string"){const s=(r=v(this,ct).get(n))==null?void 0:r.find(i=>i!==t&&i.state.isPaused);return(s==null?void 0:s.continue())??Promise.resolve()}else return Promise.resolve()}clear(){ue.batch(()=>{v(this,bt).forEach(t=>{this.notify({type:"removed",mutation:t})}),v(this,bt).clear(),v(this,ct).clear()})}getAll(){return Array.from(v(this,bt))}find(t){const n={exact:!0,...t};return this.getAll().find(r=>qc(n,r))}findAll(t={}){return this.getAll().filter(n=>qc(t,n))}notify(t){ue.batch(()=>{this.listeners.forEach(n=>{n(t)})})}resumePausedMutations(){const t=this.getAll().filter(n=>n.state.isPaused);return ue.batch(()=>Promise.all(t.map(n=>n.continue().catch(Te))))}},bt=new WeakMap,ct=new WeakMap,Hs=new WeakMap,wd);function ki(e){var t;return(t=e.options.scope)==null?void 0:t.id}var Mt,ln,De,Dt,Ut,Di,ko,Sd,wv=(Sd=class extends Kr{constructor(n,r){super();F(this,Ut);F(this,Mt);F(this,ln);F(this,De);F(this,Dt);_(this,Mt,n),this.setOptions(r),this.bindMethods(),I(this,Ut,Di).call(this)}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(n){var s;const r=this.options;this.options=v(this,Mt).defaultMutationOptions(n),ul(this.options,r)||v(this,Mt).getMutationCache().notify({type:"observerOptionsUpdated",mutation:v(this,De),observer:this}),r!=null&&r.mutationKey&&this.options.mutationKey&&Xn(r.mutationKey)!==Xn(this.options.mutationKey)?this.reset():((s=v(this,De))==null?void 0:s.state.status)==="pending"&&v(this,De).setOptions(this.options)}onUnsubscribe(){var n;this.hasListeners()||(n=v(this,De))==null||n.removeObserver(this)}onMutationUpdate(n){I(this,Ut,Di).call(this),I(this,Ut,ko).call(this,n)}getCurrentResult(){return v(this,ln)}reset(){var n;(n=v(this,De))==null||n.removeObserver(this),_(this,De,void 0),I(this,Ut,Di).call(this),I(this,Ut,ko).call(this)}mutate(n,r){var s;return _(this,Dt,r),(s=v(this,De))==null||s.removeObserver(this),_(this,De,v(this,Mt).getMutationCache().build(v(this,Mt),this.options)),v(this,De).addObserver(this),v(this,De).execute(n)}},Mt=new WeakMap,ln=new WeakMap,De=new WeakMap,Dt=new WeakMap,Ut=new WeakSet,Di=function(){var r;const n=((r=v(this,De))==null?void 0:r.state)??Wh();_(this,ln,{...n,isPending:n.status==="pending",isSuccess:n.status==="success",isError:n.status==="error",isIdle:n.status==="idle",mutate:this.mutate,reset:this.reset})},ko=function(n){ue.batch(()=>{var r,s,i,l,a,u,c,p;if(v(this,Dt)&&this.hasListeners()){const d=v(this,ln).variables,m=v(this,ln).context,x={client:v(this,Mt),meta:this.options.meta,mutationKey:this.options.mutationKey};if((n==null?void 0:n.type)==="success"){try{(s=(r=v(this,Dt)).onSuccess)==null||s.call(r,n.data,d,m,x)}catch(g){Promise.reject(g)}try{(l=(i=v(this,Dt)).onSettled)==null||l.call(i,n.data,null,d,m,x)}catch(g){Promise.reject(g)}}else if((n==null?void 0:n.type)==="error"){try{(u=(a=v(this,Dt)).onError)==null||u.call(a,n.error,d,m,x)}catch(g){Promise.reject(g)}try{(p=(c=v(this,Dt)).onSettled)==null||p.call(c,void 0,n.error,d,m,x)}catch(g){Promise.reject(g)}}}this.listeners.forEach(d=>{d(v(this,ln))})})},Sd),Et,kd,Sv=(kd=class extends Kr{constructor(t={}){super();F(this,Et);this.config=t,_(this,Et,new Map)}build(t,n,r){const s=n.queryKey,i=n.queryHash??Cu(s,n);let l=this.get(i);return l||(l=new pv({client:t,queryKey:s,queryHash:i,options:t.defaultQueryOptions(n),state:r,defaultOptions:t.getQueryDefaults(s)}),this.add(l)),l}add(t){v(this,Et).has(t.queryHash)||(v(this,Et).set(t.queryHash,t),this.notify({type:"added",query:t}))}remove(t){const n=v(this,Et).get(t.queryHash);n&&(t.destroy(),n===t&&v(this,Et).delete(t.queryHash),this.notify({type:"removed",query:t}))}clear(){ue.batch(()=>{this.getAll().forEach(t=>{this.remove(t)})})}get(t){return v(this,Et).get(t)}getAll(){return[...v(this,Et).values()]}find(t){const n={exact:!0,...t};return this.getAll().find(r=>Vc(n,r))}findAll(t={}){const n=this.getAll();return Object.keys(t).length>0?n.filter(r=>Vc(t,r)):n}notify(t){ue.batch(()=>{this.listeners.forEach(n=>{n(t)})})}onFocus(){ue.batch(()=>{this.getAll().forEach(t=>{t.onFocus()})})}onOnline(){ue.batch(()=>{this.getAll().forEach(t=>{t.onOnline()})})}},Et=new WeakMap,kd),ne,an,on,_r,Lr,un,Tr,Fr,Ed,kv=(Ed=class{constructor(e={}){F(this,ne);F(this,an);F(this,on);F(this,_r);F(this,Lr);F(this,un);F(this,Tr);F(this,Fr);_(this,ne,e.queryCache||new Sv),_(this,an,e.mutationCache||new xv),_(this,on,e.defaultOptions||{}),_(this,_r,new Map),_(this,Lr,new Map),_(this,un,0)}mount(){si(this,un)._++,v(this,un)===1&&(_(this,Tr,Eu.subscribe(async e=>{e&&(await this.resumePausedMutations(),v(this,ne).onFocus())})),_(this,Fr,cl.subscribe(async e=>{e&&(await this.resumePausedMutations(),v(this,ne).onOnline())})))}unmount(){var e,t;si(this,un)._--,v(this,un)===0&&((e=v(this,Tr))==null||e.call(this),_(this,Tr,void 0),(t=v(this,Fr))==null||t.call(this),_(this,Fr,void 0))}isFetching(e){return v(this,ne).findAll({...e,fetchStatus:"fetching"}).length}isMutating(e){return v(this,an).findAll({...e,status:"pending"}).length}getQueryData(e){var n;const t=this.defaultQueryOptions({queryKey:e});return(n=v(this,ne).get(t.queryHash))==null?void 0:n.state.data}ensureQueryData(e){const t=this.defaultQueryOptions(e),n=v(this,ne).build(this,t),r=n.state.data;return r===void 0?this.fetchQuery(e):(e.revalidateIfStale&&n.isStaleByTime(wn(t.staleTime,n))&&this.prefetchQuery(t),Promise.resolve(r))}getQueriesData(e){return v(this,ne).findAll(e).map(({queryKey:t,state:n})=>{const r=n.data;return[t,r]})}setQueryData(e,t,n){const r=this.defaultQueryOptions({queryKey:e}),s=v(this,ne).get(r.queryHash),i=s==null?void 0:s.state.data,l=nv(t,i);if(l!==void 0)return v(this,ne).build(this,r).setData(l,{...n,manual:!0})}setQueriesData(e,t,n){return ue.batch(()=>v(this,ne).findAll(e).map(({queryKey:r})=>[r,this.setQueryData(r,t,n)]))}getQueryState(e){var n;const t=this.defaultQueryOptions({queryKey:e});return(n=v(this,ne).get(t.queryHash))==null?void 0:n.state}removeQueries(e){const t=v(this,ne);ue.batch(()=>{t.findAll(e).forEach(n=>{t.remove(n)})})}resetQueries(e,t){const n=v(this,ne);return ue.batch(()=>(n.findAll(e).forEach(r=>{r.reset()}),this.refetchQueries({type:"active",...e},t)))}cancelQueries(e,t={}){const n={revert:!0,...t},r=ue.batch(()=>v(this,ne).findAll(e).map(s=>s.cancel(n)));return Promise.all(r).then(Te).catch(Te)}invalidateQueries(e,t={}){return ue.batch(()=>(v(this,ne).findAll(e).forEach(n=>{n.invalidate()}),(e==null?void 0:e.refetchType)==="none"?Promise.resolve():this.refetchQueries({...e,type:(e==null?void 0:e.refetchType)??(e==null?void 0:e.type)??"active"},t)))}refetchQueries(e,t={}){const n={...t,cancelRefetch:t.cancelRefetch??!0},r=ue.batch(()=>v(this,ne).findAll(e).filter(s=>!s.isDisabled()&&!s.isStatic()).map(s=>{let i=s.fetch(void 0,n);return n.throwOnError||(i=i.catch(Te)),s.state.fetchStatus==="paused"?Promise.resolve():i}));return Promise.all(r).then(Te)}fetchQuery(e){const t=this.defaultQueryOptions(e);t.retry===void 0&&(t.retry=!1);const n=v(this,ne).build(this,t);return n.isStaleByTime(wn(t.staleTime,n))?n.fetch(t):Promise.resolve(n.state.data)}prefetchQuery(e){return this.fetchQuery(e).then(Te).catch(Te)}fetchInfiniteQuery(e){return e._type="infinite",this.fetchQuery(e)}prefetchInfiniteQuery(e){return this.fetchInfiniteQuery(e).then(Te).catch(Te)}ensureInfiniteQueryData(e){return e._type="infinite",this.ensureQueryData(e)}resumePausedMutations(){return cl.isOnline()?v(this,an).resumePausedMutations():Promise.resolve()}getQueryCache(){return v(this,ne)}getMutationCache(){return v(this,an)}getDefaultOptions(){return v(this,on)}setDefaultOptions(e){_(this,on,e)}setQueryDefaults(e,t){v(this,_r).set(Xn(e),{queryKey:e,defaultOptions:t})}getQueryDefaults(e){const t=[...v(this,_r).values()],n={};return t.forEach(r=>{$s(e,r.queryKey)&&Object.assign(n,r.defaultOptions)}),n}setMutationDefaults(e,t){v(this,Lr).set(Xn(e),{mutationKey:e,defaultOptions:t})}getMutationDefaults(e){const t=[...v(this,Lr).values()],n={};return t.forEach(r=>{$s(e,r.mutationKey)&&Object.assign(n,r.defaultOptions)}),n}defaultQueryOptions(e){if(e._defaulted)return e;const t={...v(this,on).queries,...this.getQueryDefaults(e.queryKey),...e,_defaulted:!0};return t.queryHash||(t.queryHash=Cu(t.queryKey,t)),t.refetchOnReconnect===void 0&&(t.refetchOnReconnect=t.networkMode!=="always"),t.throwOnError===void 0&&(t.throwOnError=!!t.suspense),!t.networkMode&&t.persister&&(t.networkMode="offlineFirst"),t.queryFn===ju&&(t.enabled=!1),t}defaultMutationOptions(e){return e!=null&&e._defaulted?e:{...v(this,on).mutations,...(e==null?void 0:e.mutationKey)&&this.getMutationDefaults(e.mutationKey),...e,_defaulted:!0}}clear(){v(this,ne).clear(),v(this,an).clear()}},ne=new WeakMap,an=new WeakMap,on=new WeakMap,_r=new WeakMap,Lr=new WeakMap,un=new WeakMap,Tr=new WeakMap,Fr=new WeakMap,Ed),Vh=w.createContext(void 0),vt=e=>{const t=w.useContext(Vh);if(!t)throw new Error("No QueryClient set, use QueryClientProvider to set one");return t},Ev=({client:e,children:t})=>(w.useEffect(()=>(e.mount(),()=>{e.unmount()}),[e]),o.jsx(Vh.Provider,{value:e,children:t})),qh=w.createContext(!1),Cv=()=>w.useContext(qh);qh.Provider;function jv(){let e=!1;return{clearReset:()=>{e=!1},reset:()=>{e=!0},isReset:()=>e}}var Nv=w.createContext(jv()),Pv=()=>w.useContext(Nv),Rv=(e,t,n)=>{const r=n!=null&&n.state.error&&typeof e.throwOnError=="function"?Nu(e.throwOnError,[n.state.error,n]):e.throwOnError;(e.suspense||e.experimental_prefetchInRender||r)&&(t.isReset()||(e.retryOnMount=!1))},_v=e=>{w.useEffect(()=>{e.clearReset()},[e])},Lv=({result:e,errorResetBoundary:t,throwOnError:n,query:r,suspense:s})=>e.isError&&!t.isReset()&&!e.isFetching&&r&&(s&&e.data===void 0||Nu(n,[e.error,r])),Tv=e=>{if(e.suspense){const n=s=>s==="static"?s:Math.max(s??1e3,1e3),r=e.staleTime;e.staleTime=typeof r=="function"?(...s)=>n(r(...s)):n(r),typeof e.gcTime=="number"&&(e.gcTime=Math.max(e.gcTime,1e3))}},Fv=(e,t)=>e.isLoading&&e.isFetching&&!t,Ov=(e,t)=>(e==null?void 0:e.suspense)&&t.isPending,nd=(e,t,n)=>t.fetchOptimistic(e).catch(()=>{n.clearReset()});function bv(e,t,n){var x,g,S,E;const r=Cv(),s=Pv(),i=vt(),l=i.defaultQueryOptions(e);(g=(x=i.getDefaultOptions().queries)==null?void 0:x._experimental_beforeQuery)==null||g.call(x,l);const a=i.getQueryCache().get(l.queryHash),u=e.subscribed!==!1;l._optimisticResults=r?"isRestoring":u?"optimistic":void 0,Tv(l),Rv(l,s,a),_v(s);const c=!i.getQueryCache().get(l.queryHash),[p]=w.useState(()=>new t(i,l)),d=p.getOptimisticResult(l),m=!r&&u;if(w.useSyncExternalStore(w.useCallback(f=>{const h=m?p.subscribe(ue.batchCalls(f)):Te;return p.updateResult(),h},[p,m]),()=>p.getCurrentResult(),()=>p.getCurrentResult()),w.useEffect(()=>{p.setOptions(l)},[l,p]),Ov(l,d))throw nd(l,p,s);if(Lv({result:d,errorResetBoundary:s,throwOnError:l.throwOnError,query:a,suspense:l.suspense}))throw d.error;if((E=(S=i.getDefaultOptions().queries)==null?void 0:S._experimental_afterQuery)==null||E.call(S,l,d),l.experimental_prefetchInRender&&!zs.isServer()&&Fv(d,r)){const f=c?nd(l,p,s):a==null?void 0:a.promise;f==null||f.catch(Te).finally(()=>{p.updateResult()})}return l.notifyOnChangeProps?d:p.trackResult(d)}function me(e,t){return bv(e,mv)}function Ne(e,t){const n=vt(),[r]=w.useState(()=>new wv(n,e));w.useEffect(()=>{r.setOptions(e)},[r,e]);const s=w.useSyncExternalStore(w.useCallback(l=>r.subscribe(ue.batchCalls(l)),[r]),()=>r.getCurrentResult(),()=>r.getCurrentResult()),i=w.useCallback((l,a)=>{r.mutate(l,a).catch(Te)},[r]);if(s.error&&Nu(r.options.throwOnError,[s.error]))throw s.error;return{...s,mutate:i,mutateAsync:s.mutate}}/** - * react-router v7.18.3 - * - * Copyright (c) Remix Software Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE.md file in the root directory of this source tree. - * - * @license MIT - */var Ru=/^(?:[a-z][a-z0-9+.-]*:|[\\/]{2})/i,Gh=/^[\\/]{2}/;function Mv(e,t){return t+e.replace(/\\/g,"/")}var rd="popstate";function sd(e){return typeof e=="object"&&e!=null&&"pathname"in e&&"search"in e&&"hash"in e&&"state"in e&&"key"in e}function Dv(e={}){function t(r,s){var c;let i=(c=s.state)==null?void 0:c.masked,{pathname:l,search:a,hash:u}=i||r.location;return Eo("",{pathname:l,search:a,hash:u},s.state&&s.state.usr||null,s.state&&s.state.key||"default",i?{pathname:r.location.pathname,search:r.location.search,hash:r.location.hash}:void 0)}function n(r,s){return typeof s=="string"?s:Ur(s)}return $v(t,n,null,e)}function Z(e,t){if(e===!1||e===null||typeof e>"u")throw new Error(t)}function Pt(e,t){if(!e){typeof console<"u"&&console.warn(t);try{throw new Error(t)}catch{}}}function Iv(){return Math.random().toString(36).substring(2,10)}function id(e,t){return{usr:e.state,key:e.key,idx:t,masked:e.mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function Eo(e,t,n=null,r,s){return{pathname:typeof e=="string"?e:e.pathname,search:"",hash:"",...typeof t=="string"?Wr(t):t,state:n,key:t&&t.key||r||Iv(),mask:s}}function Ur({pathname:e="/",search:t="",hash:n=""}){return t&&t!=="?"&&(e+=t.charAt(0)==="?"?t:"?"+t),n&&n!=="#"&&(e+=n.charAt(0)==="#"?n:"#"+n),e}function Wr(e){let t={};if(e){let n=e.indexOf("#");n>=0&&(t.hash=e.substring(n),e=e.substring(0,n));let r=e.indexOf("?");r>=0&&(t.search=e.substring(r),e=e.substring(0,r)),e&&(t.pathname=e)}return t}function $v(e,t,n,r={}){let{window:s=document.defaultView,v5Compat:i=!1}=r,l=s.history,a="POP",u=null,c=p();c==null&&(c=0,l.replaceState({...l.state,idx:c},""));function p(){return(l.state||{idx:null}).idx}function d(){a="POP";let E=p(),f=E==null?null:E-c;c=E,u&&u({action:a,location:S.location,delta:f})}function m(E,f){a="PUSH";let h=sd(E)?E:Eo(S.location,E,f);c=p()+1;let y=id(h,c),k=S.createHref(h.mask||h);try{l.pushState(y,"",k)}catch(C){if(C instanceof DOMException&&C.name==="DataCloneError")throw C;s.location.assign(k)}i&&u&&u({action:a,location:S.location,delta:1})}function x(E,f){a="REPLACE";let h=sd(E)?E:Eo(S.location,E,f);c=p();let y=id(h,c),k=S.createHref(h.mask||h);l.replaceState(y,"",k),i&&u&&u({action:a,location:S.location,delta:0})}function g(E){return zv(s,E)}let S={get action(){return a},get location(){return e(s,l)},listen(E){if(u)throw new Error("A history only accepts one active listener");return s.addEventListener(rd,d),u=E,()=>{s.removeEventListener(rd,d),u=null}},createHref(E){return t(s,E)},createURL:g,encodeLocation(E){let f=g(E);return{pathname:f.pathname,search:f.search,hash:f.hash}},push:m,replace:x,go(E){return l.go(E)}};return S}function zv(e,t,n=!1){let r="http://localhost";e&&(r=e.location.origin!=="null"?e.location.origin:e.location.href),Z(r,"No window.location.(origin|href) available to create URL");let s=typeof t=="string"?t:Ur(t);return s=s.replace(/ $/,"%20"),!n&&Gh.test(s)&&(s=r+s),new URL(s,r)}function Yh(e,t,n="/"){return Uv(e,t,n,!1)}function Uv(e,t,n,r,s){let i=typeof t=="string"?Wr(t):t,l=Kt(i.pathname||"/",n);if(l==null)return null;let a=Av(e),u=null,c=Jv(l);for(let p=0;u==null&&p{let p={relativePath:c===void 0?l.path||"":c,caseSensitive:l.caseSensitive===!0,childrenIndex:a,route:l};if(p.relativePath.startsWith("/")){if(!p.relativePath.startsWith(r)&&u)return;Z(p.relativePath.startsWith(r),`Absolute route path "${p.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),p.relativePath=p.relativePath.slice(r.length)}let d=mt([r,p.relativePath]),m=n.concat(p);l.children&&l.children.length>0&&(Z(l.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${d}".`),Xh(l.children,t,m,d,u)),!(l.path==null&&!l.index)&&t.push({path:d,score:Gv(d,l.index),routesMeta:m.map((x,g)=>{let[S,E]=ep(x.relativePath,x.caseSensitive,g===m.length-1);return{...x,matcher:S,compiledParams:E}})})};return e.forEach((l,a)=>{var u;if(l.path===""||!((u=l.path)!=null&&u.includes("?")))i(l,a);else for(let c of Jh(l.path))i(l,a,!0,c)}),t}function Jh(e){let t=e.split("/");if(t.length===0)return[];let[n,...r]=t,s=n.endsWith("?"),i=n.replace(/\?$/,"");if(r.length===0)return s?[i,""]:[i];let l=Jh(r.join("/")),a=[];return a.push(...l.map(u=>u===""?i:[i,u].join("/"))),s&&a.push(...l),a.map(u=>e.startsWith("/")&&u===""?"/":u)}function Qv(e){e.sort((t,n)=>t.score!==n.score?n.score-t.score:Yv(t.routesMeta.map(r=>r.childrenIndex),n.routesMeta.map(r=>r.childrenIndex)))}var Bv=/^:[\w-]+$/,Hv=3,Kv=2,Wv=1,Vv=10,qv=-2,ld=e=>e==="*";function Gv(e,t){let n=e.split("/"),r=n.length;return n.some(ld)&&(r+=qv),t&&(r+=Kv),n.filter(s=>!ld(s)).reduce((s,i)=>s+(Bv.test(i)?Hv:i===""?Wv:Vv),r)}function Yv(e,t){return e.length===t.length&&e.slice(0,-1).every((r,s)=>r===t[s])?e[e.length-1]-t[t.length-1]:0}function Xv(e,t,n=!1){let{routesMeta:r}=e,s={},i="/",l=[];for(let a=0;a{if(p==="*"){let g=a[m]||"";l=Ar(i.slice(0,i.length-g.length),1)}const x=a[m];return d&&!x?c[p]=void 0:c[p]=(x||"").replace(/%2F/g,"/"),c},{}),pathname:i,pathnameBase:l,pattern:e}}function ep(e,t=!1,n=!0){Pt(e==="*"||!e.endsWith("*")||e.endsWith("/*"),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,"/*")}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,"/*")}".`);let r=[],s="^"+e.replace(/\/*\*?$/,"").replace(/^\/*/,"/").replace(/[\\.*+^${}|()[\]]/g,"\\$&").replace(/\/:([\w-]+)(\?)?/g,(l,a,u,c,p)=>{if(r.push({paramName:a,isOptional:u!=null}),u){let d=p.charAt(c+l.length);return d&&d!=="/"?"/([^\\/]*)":"(?:/([^\\/]*))?"}return"/([^\\/]+)"}).replace(/\/([\w-]+)\?(\/|$)/g,"(/$1)?$2");return e.endsWith("*")?(r.push({paramName:"*"}),s+=e==="*"||e==="/*"?"(.*)$":"(?:\\/(.+)|\\/*)$"):n?s+="\\/*$":e!==""&&e!=="/"&&(s+="(?:(?=\\/|$))"),[new RegExp(s,t?void 0:"i"),r]}function Jv(e){try{return e.split("/").map(t=>decodeURIComponent(t).replace(/\//g,"%2F")).join("/")}catch(t){return Pt(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function Kt(e,t){if(t==="/")return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith("/")?t.length-1:t.length,r=e.charAt(n);return r&&r!=="/"?null:e.slice(n)||"/"}function Zv(e,t="/"){let{pathname:n,search:r="",hash:s=""}=typeof e=="string"?Wr(e):e,i;return n?(n=np(n),n.startsWith("/")||n.startsWith("\\")?i=ad(n.substring(1),"/"):i=ad(n,t)):i=t,{pathname:i,search:ng(r),hash:rg(s)}}function ad(e,t){let n=Ar(t).split("/");return e.split("/").forEach(s=>{s===".."?n.length>1&&n.pop():s!=="."&&n.push(s)}),n.length>1?n.join("/"):"/"}function ca(e,t,n,r){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function eg(e){return e.filter((t,n)=>n===0||t.route.path&&t.route.path.length>0)}function tp(e){let t=eg(e);return t.map((n,r)=>r===t.length-1?n.pathname:n.pathnameBase)}function _u(e,t,n,r=!1){let s;typeof e=="string"?s=Wr(e):(s={...e},Z(!s.pathname||!s.pathname.includes("?"),ca("?","pathname","search",s)),Z(!s.pathname||!s.pathname.includes("#"),ca("#","pathname","hash",s)),Z(!s.search||!s.search.includes("#"),ca("#","search","hash",s)));let i=e===""||s.pathname==="",l=i?"/":s.pathname,a;if(l==null)a=n;else{let d=t.length-1;if(!r&&l.startsWith("..")){let m=l.split("/");for(;m[0]==="..";)m.shift(),d-=1;s.pathname=m.join("/")}a=d>=0?t[d]:"/"}let u=Zv(s,a),c=l&&l!=="/"&&l.endsWith("/"),p=(i||l===".")&&n.endsWith("/");return!u.pathname.endsWith("/")&&(c||p)&&(u.pathname+="/"),u}var np=e=>e.replace(/[\\/]{2,}/g,"/"),mt=e=>np(e.join("/"));function Ar(e,t=0){let n=e.length;for(;n>t&&e.charCodeAt(n-1)===47;)n--;return n===e.length?e:e.slice(0,n)}var tg=e=>Ar(e).replace(/^\/*/,"/"),ng=e=>!e||e==="?"?"":e.startsWith("?")?e:"?"+e,rg=e=>!e||e==="#"?"":e.startsWith("#")?e:"#"+e,sg=class{constructor(e,t,n,r=!1){this.status=e,this.statusText=t||"",this.internal=r,n instanceof Error?(this.data=n.toString(),this.error=n):this.data=n}};function ig(e){return e!=null&&typeof e.status=="number"&&typeof e.statusText=="string"&&typeof e.internal=="boolean"&&"data"in e}function lg(e){let t=e.map(n=>n.route.path).filter(Boolean);return mt(t)||"/"}var rp=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function sp(e,t){let n=e;if(typeof n!="string"||!Ru.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,s=!1;if(rp)try{let i=new URL(window.location.href),l=Gh.test(n)?new URL(Mv(n,i.protocol)):new URL(n),a=Kt(l.pathname,t);l.origin===i.origin&&a!=null?n=a+l.search+l.hash:s=!0}catch{Pt(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:r,isExternal:s,to:n}}Object.getOwnPropertyNames(Object.prototype).sort().join("\0");var od=new URL("http://localhost");function ip(e){if(e.createURL)return e.createURL("/");try{return new URL(e.createHref("/"),od)}catch{return od}}function da(e,t){return e.origin===t.origin&&(e.origin!=="null"||e.protocol===t.protocol&&e.host===t.host)}function ag(e,t){if(e.startsWith("//"))return!0;let n=t.protocol.toLowerCase();return e.toLowerCase().startsWith(n)?t.host===""||e.slice(n.length).startsWith("//"):!1}function lp(e,t,n,r){let s=null;try{s=e==null?null:new URL(e,n)}catch{}let i=new URL(t,n),l=s!=null&&!da(s,n),a=!da(i,n);if(r==="reject"){if(l||a)throw new Error("External navigation is not allowed")}else if(a&&(s==null||!ag(e,s)||!da(s,i)))throw new Error("External navigation is not allowed")}var ap=["POST","PUT","PATCH","DELETE"];new Set(ap);var og=["GET",...ap];new Set(og);var ug=["about:","blob:","chrome:","chrome-untrusted:","content:","data:","devtools:","file:","filesystem:","javascript:"];function cg(e){try{return ug.includes(new URL(e).protocol)}catch{return!1}}var Vr=w.createContext(null);Vr.displayName="DataRouter";var Rl=w.createContext(null);Rl.displayName="DataRouterState";var op=w.createContext(!1);function dg(){return w.useContext(op)}var up=w.createContext({isTransitioning:!1});up.displayName="ViewTransition";var fg=w.createContext(new Map);fg.displayName="Fetchers";var hg=w.createContext(null);hg.displayName="Await";var lt=w.createContext(null);lt.displayName="Navigation";var Ys=w.createContext(null);Ys.displayName="Location";var Rt=w.createContext({outlet:null,matches:[],isDataRoute:!1});Rt.displayName="Route";var Lu=w.createContext(null);Lu.displayName="RouteError";var cp="REACT_ROUTER_ERROR",pg="REDIRECT",mg="ROUTE_ERROR_RESPONSE";function yg(e){if(e.startsWith(`${cp}:${pg}:{`))try{let t=JSON.parse(e.slice(28));if(typeof t=="object"&&t&&typeof t.status=="number"&&typeof t.statusText=="string"&&typeof t.location=="string"&&typeof t.reloadDocument=="boolean"&&typeof t.replace=="boolean")return t}catch{}}function vg(e){if(e.startsWith(`${cp}:${mg}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t=="object"&&t&&typeof t.status=="number"&&typeof t.statusText=="string")return new sg(t.status,t.statusText,t.data)}catch{}}function gg(e,{relative:t}={}){Z(Xs(),"useHref() may be used only in the context of a component.");let{basename:n,navigator:r}=w.useContext(lt),{hash:s,pathname:i,search:l}=Zs(e,{relative:t}),a=i;return n!=="/"&&(a=i==="/"?n:mt([n,i])),r.createHref({pathname:a,search:l,hash:s})}function Xs(){return w.useContext(Ys)!=null}function _t(){return Z(Xs(),"useLocation() may be used only in the context of a component."),w.useContext(Ys).location}var dp="You should call navigate() in a React.useEffect(), not when your component is first rendered.";function fp(e){w.useContext(lt).static||w.useLayoutEffect(e)}function Js(){let{isDataRoute:e}=w.useContext(Rt);return e?Tg():xg()}function xg(){Z(Xs(),"useNavigate() may be used only in the context of a component.");let e=w.useContext(Vr),{basename:t,navigator:n}=w.useContext(lt),{matches:r}=w.useContext(Rt),{pathname:s}=_t(),i=JSON.stringify(tp(r)),l=w.useRef(!1);return fp(()=>{l.current=!0}),w.useCallback((u,c={})=>{if(Pt(l.current,dp),!l.current)return;if(typeof u=="number"){n.go(u);return}let p=_u(u,JSON.parse(i),s,c.relative==="path");e==null&&t!=="/"&&(p.pathname=p.pathname==="/"?t:mt([t,p.pathname])),lp(typeof u=="string"?u:Ur(u),n.createHref(p),ip(n),"reject"),(c.replace?n.replace:n.push)(p,c.state,c)},[t,n,i,s,e])}w.createContext(null);function _l(){let{matches:e}=w.useContext(Rt),t=e[e.length-1];return(t==null?void 0:t.params)??{}}function Zs(e,{relative:t}={}){let{matches:n}=w.useContext(Rt),{pathname:r}=_t(),s=JSON.stringify(tp(n));return w.useMemo(()=>_u(e,JSON.parse(s),r,t==="path"),[e,s,r,t])}function wg(e,t){return hp(e,t)}function hp(e,t,n){var E;Z(Xs(),"useRoutes() may be used only in the context of a component.");let{navigator:r}=w.useContext(lt),{matches:s}=w.useContext(Rt),i=s[s.length-1],l=i?i.params:{},a=i?i.pathname:"/",u=i?i.pathnameBase:"/",c=i&&i.route;{let f=c&&c.path||"";mp(a,!c||f.endsWith("*")||f.endsWith("*?"),`You rendered descendant (or called \`useRoutes()\`) at "${a}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. - -Please change the parent to .`)}let p=_t(),d;if(t){let f=typeof t=="string"?Wr(t):t;Z(u==="/"||((E=f.pathname)==null?void 0:E.startsWith(u)),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${u}" but pathname "${f.pathname}" was given in the \`location\` prop.`),d=f}else d=p;let m=d.pathname||"/",x=m;if(u!=="/"){let f=u.replace(/^\//,"").split("/");x="/"+m.replace(/^\//,"").split("/").slice(f.length).join("/")}let g=n&&n.state.matches.length?n.state.matches.map(f=>Object.assign(f,{route:n.manifest[f.route.id]||f.route})):Yh(e,{pathname:x});Pt(c||g!=null,`No routes matched location "${d.pathname}${d.search}${d.hash}" `),Pt(g==null||g[g.length-1].route.element!==void 0||g[g.length-1].route.Component!==void 0||g[g.length-1].route.lazy!==void 0,`Matched leaf route at location "${d.pathname}${d.search}${d.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let S=jg(g&&g.map(f=>Object.assign({},f,{params:Object.assign({},l,f.params),pathname:mt([u,r.encodeLocation?r.encodeLocation(f.pathname.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:f.pathname]),pathnameBase:f.pathnameBase==="/"?u:mt([u,r.encodeLocation?r.encodeLocation(f.pathnameBase.replace(/%/g,"%25").replace(/\?/g,"%3F").replace(/#/g,"%23")).pathname:f.pathnameBase])})),s,n);return t&&S?w.createElement(Ys.Provider,{value:{location:{pathname:"/",search:"",hash:"",state:null,key:"default",mask:void 0,...d},navigationType:"POP"}},S):S}function Sg(){let e=Lg(),t=ig(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,r="rgba(200,200,200, 0.5)",s={padding:"0.5rem",backgroundColor:r},i={padding:"2px 4px",backgroundColor:r},l=null;return console.error("Error handled by React Router default ErrorBoundary:",e),l=w.createElement(w.Fragment,null,w.createElement("p",null,"💿 Hey developer 👋"),w.createElement("p",null,"You can provide a way better UX than this when your app throws errors by providing your own ",w.createElement("code",{style:i},"ErrorBoundary")," or"," ",w.createElement("code",{style:i},"errorElement")," prop on your route.")),w.createElement(w.Fragment,null,w.createElement("h2",null,"Unexpected Application Error!"),w.createElement("h3",{style:{fontStyle:"italic"}},t),n?w.createElement("pre",{style:s},n):null,l)}var kg=w.createElement(Sg,null),pp=class extends w.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,t){return t.location!==e.location||t.revalidation!=="idle"&&e.revalidation==="idle"?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error!==void 0?e.error:t.error,location:t.location,revalidation:e.revalidation||t.revalidation}}componentDidCatch(e,t){this.props.onError?this.props.onError(e,t):console.error("React Router caught the following error during render",e)}render(){let e=this.state.error;if(this.context&&typeof e=="object"&&e&&"digest"in e&&typeof e.digest=="string"){const n=vg(e.digest);n&&(e=n)}let t=e!==void 0?w.createElement(Rt.Provider,{value:this.props.routeContext},w.createElement(Lu.Provider,{value:e,children:this.props.component})):this.props.children;return this.context?w.createElement(Eg,{error:e},t):t}};pp.contextType=op;var fa=new WeakMap;function Eg({children:e,error:t}){let{basename:n,navigator:r}=w.useContext(lt);if(typeof t=="object"&&t&&"digest"in t&&typeof t.digest=="string"){let s=yg(t.digest);if(s){let i=fa.get(t);if(i)throw i;let l=sp(s.location,n),a=l.absoluteURL||l.to;if(lp(s.location,a,ip(r),"allow-explicit"),cg(a))throw new Error("Invalid redirect location");if(rp&&!fa.get(t))if(l.isExternal||s.reloadDocument)window.location.href=a;else{const u=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(l.to,{replace:s.replace}));throw fa.set(t,u),u}return w.createElement("meta",{httpEquiv:"refresh",content:`0;url=${a}`})}}return e}function Cg({routeContext:e,match:t,children:n}){let r=w.useContext(Vr);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),w.createElement(Rt.Provider,{value:e},n)}function jg(e,t=[],n){let r=n==null?void 0:n.state;if(e==null){if(!r)return null;if(r.errors)e=r.matches;else if(t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let s=e,i=r==null?void 0:r.errors;if(i!=null){let p=s.findIndex(d=>d.route.id&&(i==null?void 0:i[d.route.id])!==void 0);Z(p>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(i).join(",")}`),s=s.slice(0,Math.min(s.length,p+1))}let l=!1,a=-1;if(n&&r){l=r.renderFallback;for(let p=0;p=0?s=s.slice(0,a+1):s=[s[0]];break}}}}let u=n==null?void 0:n.onError,c=r&&u?(p,d)=>{var m,x;u(p,{location:r.location,params:((x=(m=r.matches)==null?void 0:m[0])==null?void 0:x.params)??{},pattern:lg(r.matches),errorInfo:d})}:void 0;return s.reduceRight((p,d,m)=>{let x,g=!1,S=null,E=null;r&&(x=i&&d.route.id?i[d.route.id]:void 0,S=d.route.errorElement||kg,l&&(a<0&&m===0?(mp("route-fallback",!1,"No `HydrateFallback` element provided to render during initial hydration"),g=!0,E=null):a===m&&(g=!0,E=d.route.hydrateFallbackElement||null)));let f=t.concat(s.slice(0,m+1)),h=()=>{let y;return x?y=S:g?y=E:d.route.Component?y=w.createElement(d.route.Component,null):d.route.element?y=d.route.element:y=p,w.createElement(Cg,{match:d,routeContext:{outlet:p,matches:f,isDataRoute:r!=null},children:y})};return r&&(d.route.ErrorBoundary||d.route.errorElement||m===0)?w.createElement(pp,{location:r.location,revalidation:r.revalidation,component:S,error:x,children:h(),routeContext:{outlet:null,matches:f,isDataRoute:!0},onError:c}):h()},null)}function Tu(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function Ng(e){let t=w.useContext(Vr);return Z(t,Tu(e)),t}function Pg(e){let t=w.useContext(Rl);return Z(t,Tu(e)),t}function Rg(e){let t=w.useContext(Rt);return Z(t,Tu(e)),t}function Fu(e){let t=Rg(e),n=t.matches[t.matches.length-1];return Z(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function _g(){return Fu("useRouteId")}function Lg(){var r;let e=w.useContext(Lu),t=Pg("useRouteError"),n=Fu("useRouteError");return e!==void 0?e:(r=t.errors)==null?void 0:r[n]}function Tg(){let{router:e}=Ng("useNavigate"),t=Fu("useNavigate"),n=w.useRef(!1);return fp(()=>{n.current=!0}),w.useCallback(async(s,i={})=>{Pt(n.current,dp),n.current&&(typeof s=="number"?await e.navigate(s):await e.navigate(s,{fromRouteId:t,...i}))},[e,t])}var ud={};function mp(e,t,n){!t&&!ud[e]&&(ud[e]=!0,Pt(!1,n))}w.memo(Fg);function Fg({routes:e,manifest:t,future:n,state:r,isStatic:s,onError:i}){return hp(e,void 0,{manifest:t,state:r,isStatic:s,onError:i})}function Ln(e){Z(!1,"A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .")}function Og({basename:e="/",children:t=null,location:n,navigationType:r="POP",navigator:s,static:i=!1,useTransitions:l}){Z(!Xs(),"You cannot render a inside another . You should never have more than one in your app.");let a=e.replace(/^\/*/,"/"),u=w.useMemo(()=>({basename:a,navigator:s,static:i,useTransitions:l,future:{}}),[a,s,i,l]);typeof n=="string"&&(n=Wr(n));let{pathname:c="/",search:p="",hash:d="",state:m=null,key:x="default",mask:g}=n,S=w.useMemo(()=>{let E=Kt(c,a);return E==null?null:{location:{pathname:E,search:p,hash:d,state:m,key:x,mask:g},navigationType:r}},[a,c,p,d,m,x,r,g]);return Pt(S!=null,` is not able to match the URL "${c}${p}${d}" because it does not start with the basename, so the won't render anything.`),S==null?null:w.createElement(lt.Provider,{value:u},w.createElement(Ys.Provider,{children:t,value:S}))}function bg({children:e,location:t}){return wg(Co(e),t)}function Co(e,t=[]){let n=[];return w.Children.forEach(e,(r,s)=>{if(!w.isValidElement(r))return;let i=[...t,s];if(r.type===w.Fragment){n.push.apply(n,Co(r.props.children,i));return}Z(r.type===Ln,`[${typeof r.type=="string"?r.type:r.type.name}] is not a component. All component children of must be a or `),Z(!r.props.index||!r.props.children,"An index route cannot have child routes.");let l={id:r.props.id||i.join("-"),caseSensitive:r.props.caseSensitive,element:r.props.element,Component:r.props.Component,index:r.props.index,path:r.props.path,middleware:r.props.middleware,loader:r.props.loader,action:r.props.action,hydrateFallbackElement:r.props.hydrateFallbackElement,HydrateFallback:r.props.HydrateFallback,errorElement:r.props.errorElement,ErrorBoundary:r.props.ErrorBoundary,hasErrorBoundary:r.props.hasErrorBoundary===!0||r.props.ErrorBoundary!=null||r.props.errorElement!=null,shouldRevalidate:r.props.shouldRevalidate,handle:r.props.handle,lazy:r.props.lazy};r.props.children&&(l.children=Co(r.props.children,i)),n.push(l)}),n}var Ii="get",$i="application/x-www-form-urlencoded";function Ll(e){return typeof HTMLElement<"u"&&e instanceof HTMLElement}function Mg(e){return Ll(e)&&e.tagName.toLowerCase()==="button"}function Dg(e){return Ll(e)&&e.tagName.toLowerCase()==="form"}function Ig(e){return Ll(e)&&e.tagName.toLowerCase()==="input"}function $g(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function zg(e,t){return e.button===0&&(!t||t==="_self")&&!$g(e)}var Ei=null;function Ug(){if(Ei===null)try{new FormData(document.createElement("form"),0),Ei=!1}catch{Ei=!0}return Ei}var Ag=new Set(["application/x-www-form-urlencoded","multipart/form-data","text/plain"]);function ha(e){return e!=null&&!Ag.has(e)?(Pt(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${$i}"`),null):e}function Qg(e,t){let n,r,s,i,l;if(Dg(e)){let a=e.getAttribute("action");r=a?Kt(a,t):null,n=e.getAttribute("method")||Ii,s=ha(e.getAttribute("enctype"))||$i,i=new FormData(e)}else if(Mg(e)||Ig(e)&&(e.type==="submit"||e.type==="image")){let a=e.form;if(a==null)throw new Error('Cannot submit a + )} + {meta.data?.demo && demo} + {meta.data?.read_only && read-only} + {meta.data && {meta.data.version}} + {meta.isError && engine unreachable} + + + + {demo && session.data && ( +
+ + Your session-scoped sample project expires{" "} + . Native database access + is available locally. + + + + +
+ )} + + {sessionNotice && !sessionLost && ( +

+ {sessionNotice} +

+ )} + {meta.data?.demo && (sessionLost || session.isError) && ( +
+

+ {sessionLost + ? "Your sample session has ended" + : "Your sample session could not start"} +

+

+ Sample data expires and may reset when the demo restarts. Start a + new session to continue. +

+ + {recover.isError && } +
+ )} + {meta.isPending && } + {meta.isError && } + {meta.data?.demo && session.isPending && } + {meta.data?.demo && session.isError && ( + + )} + {meta.data && (!meta.data.demo || (demo && !session.isError)) && ( +
+ + } /> + } /> + } + /> + } /> + } /> + + nothing here. +

+ } + /> +
+
+ )} + + {meta.data?.demo && } + + {demo && tourKey !== null && ( + setTourKey(null)} + /> + )} + + ); +} diff --git a/web/src/Tour.tsx b/web/src/Tour.tsx new file mode 100644 index 0000000..8323fda --- /dev/null +++ b/web/src/Tour.tsx @@ -0,0 +1,240 @@ +import { LocalLink } from "./funnel"; +import { useEffect, useMemo, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { api } from "./api"; +import { Button } from "./ui"; + +const DONE_KEY = "argon.tour.done"; + +export function tourWasSeen(): boolean { + try { + return localStorage.getItem(DONE_KEY) === "1"; + } catch { + return false; + } +} + +type Step = { + title: string; + body: string; + // Where this step lives: "project" stays on the project page, "branch" + // needs the agent-run branch (created on the run step). + page: "project" | "branch"; + anchor?: string; // data-tour target to scroll to + ring +}; + +const STEPS: Step[] = [ + { + title: "Welcome — this is your own sandbox", + body: "This is a session-scoped, temporary sample project on a hosted Argon engine. Native MongoDB connections are available in your local deployment. Argon gives MongoDB Git-like branches, time travel, merges, and per-actor undo. Let's walk the whole loop in about a minute.", + page: "project", + }, + { + title: "1 · Run an agent session", + body: "Next forks planner and executor from the same pinned $49 order. The script accepts planner’s $44 proposal into main. Executor proposes $1 on its separate branch. Review its real conflict, then undo or discard it.", + page: "project", + anchor: "scenario", + }, + { + title: "2 · Every write has an actor", + body: "Executor’s branch carries its actor in history. Planner uses a separate branch from the same pin. For native drivers, attribution belongs to the branch or run, not to individual clients sharing a connection.", + page: "branch", + anchor: "agents", + }, + { + title: "3 · Time-travel the data", + body: "Pick a collection (try orders), then hit ▶ play — or drag the slider. You're watching the collection exactly as it stood at each point in history, reconstructed from the log. Only retained history with complete capture images can be reconstructed safely.", + page: "branch", + anchor: "timetravel", + }, + { + title: "4 · Diff and merge — a data pull request", + body: "Click 'compute diff against parent', then 'create merge plan'. The plan is a reviewable data PR. Planner’s accepted $44 and executor’s proposed $1 conflict on the same order. Inspect all three versions before selecting a resolution strategy.", + page: "branch", + anchor: "diff", + }, + { + title: "5 · Undo one agent", + body: "Back in the Agents panel, 'undo this actor →' reverts a single agent's writes with append-only compensations — nothing is destroyed, the undo is itself history. This is the undo button for AI agents.", + page: "branch", + anchor: "agents", + }, + { + title: "That's the loop", + body: "Continue with the same order example on your local engine. The quickstart includes MongoDB replica-set setup, capture supervision and the runnable Python assertions.", + page: "project", + }, +]; + +// Ring the target panel; retry briefly since it may mount after navigation. +function highlight(anchor: string | undefined): () => void { + let el: HTMLElement | null = null; + let tries = 0; + const clear = () => el?.classList.remove("tour-ring"); + const tick = () => { + if (!anchor) return; + const found = document.querySelector( + `[data-tour="${anchor}"]`, + ); + if (found) { + el = found; + el.classList.add("tour-ring"); + el.scrollIntoView({ behavior: "smooth", block: "center" }); + } else if (tries++ < 12) { + setTimeout(tick, 100); + } + }; + tick(); + return clear; +} + +export default function Tour({ + project, + onClose, +}: { + project: string; + onClose: () => void; +}) { + const navigate = useNavigate(); + const [i, setI] = useState(0); + const [branch, setBranch] = useState(null); + const step = STEPS[i]; + + const run = useMemo( + () => ({ + scenario: async () => { + // Reuse an existing run branch if the visitor already made one. + const { branches } = await api.branches(project); + const existing = (branches ?? []).find((b) => + b.name.startsWith("agent-run-"), + ); + if (existing) return existing.name; + const res = await api.demoScenario(); + return res.branch; + }, + }), + [project], + ); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + + // Opening (or restarting) the tour brings the top of the page into view, + // so the welcome step and the card are unmistakably together. + useEffect(() => { + window.scrollTo({ top: 0, behavior: "smooth" }); + }, []); + + // On each step, land on the right page and ring its anchor. + useEffect(() => { + const target = + step.page === "branch" && branch + ? `/p/${project}/b/${branch}` + : `/p/${project}`; + if (window.location.pathname !== target) navigate(target); + const clear = highlight(step.anchor); + return clear; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [i, branch]); + + const finish = () => { + try { + localStorage.setItem(DONE_KEY, "1"); + } catch { + /* Storage is optional. */ + } + onClose(); + }; + + const next = async () => { + // Leaving the "run" step: make sure a branch exists first. + if (step.anchor === "scenario" && !branch) { + setBusy(true); + setError(null); + try { + setBranch(await run.scenario()); + } catch (e) { + setError( + e instanceof Error + ? e.message + : "Could not run the scenario. Try again.", + ); + return; + } finally { + setBusy(false); + } + } + if (i === STEPS.length - 1) finish(); + else setI((n) => n + 1); + }; + + // Steps without a panel to point at (welcome, finish) show as a centered + // dialog behind a dim backdrop — unmissable, and clearly the thing the + // "guided tour" button opened. Steps that reference a panel dock to the + // corner and ring their target, so the page stays usable. + const centered = !step.anchor; + + const card = ( +
+
+ + Guided tour · {i + 1}/{STEPS.length} + + +
+
+

{step.title}

+

+ {step.body} +

+ {error && ( +

+ {error} +

+ )} + {i === STEPS.length - 1 && ( +

+ +

+ )} +
+ {i > 0 && ( + + )} +
+ +
+
+
+
+ ); + + if (centered) { + return ( +
+ {card} +
+ ); + } + return card; +} diff --git a/web/src/api.ts b/web/src/api.ts new file mode 100644 index 0000000..2490ed0 --- /dev/null +++ b/web/src/api.ts @@ -0,0 +1,341 @@ +// Typed client for the engine's REST control plane. Same-origin by +// default (the engine serves this SPA); a different origin and a bearer +// token can be set in localStorage for development against a remote API. + +export type Project = { + id: string; + name: string; + main_branch_id: string; + created_at: string; +}; + +export type Branch = { + id: string; + project_id: string; + name: string; + parent_id?: string; + head_lsn: number; + base_lsn: number; + created_at: string; + created_lsn: number; + is_deleted: boolean; + physical_db?: string; + state?: string; + checked_out_lsn?: number; + expires_at?: string | null; +}; + +export type Entry = { + id?: string; + lsn: number; + timestamp: string; + project_id: string; + branch_id: string; + operation: string; + collection?: string; + document_id?: string; + txn_id?: string; + actor?: string; +}; + +// TimeTravelInfo has no json tags server-side; fields arrive as Go names. +export type TimeTravelInfo = { + BranchID: string; + BranchName: string; + EarliestLSN: number; + LatestLSN: number; + EarliestTime?: string; + LatestTime?: string; + EntryCount: number; +}; + +export type MergeChange = { + collection: string; + document_id: string; + delete?: boolean; + document?: Record; +}; + +export type MergeConflict = { + collection: string; + document_id: string; + base?: Record; + ours?: Record; + theirs?: Record; +}; + +export type MergePlan = { + id: string; + project_id: string; + source_branch: string; + target_branch: string; + source_head: number; + target_head: number; + base_lsn: number; + changes: MergeChange[] | null; + conflicts: MergeConflict[] | null; + status: string; + strategy?: string; + created_at: string; + applied_at?: string; +}; + +export type Meta = { + version: string; + read_only: boolean; + demo?: boolean; + demo_ttl_minutes?: number; + native_connections?: boolean; + actor_scope?: string; + ddl_capture?: boolean; +}; + +export type CaptureStatus = { + branch_id: string; + state: string; + actor?: string; + error?: string; + updated_at?: string; + last_captured_at?: string; + head_lsn?: number; + last_event_lag_ms?: number; +}; + +export type DemoSession = { project: string; expires_at: string }; + +export type Sandbox = { branch: Branch; connection_string?: string }; + +export type Pin = { + id?: string; + project_id: string; + branch_id: string; + name: string; + lsn: number; + note?: string; + created_at: string; +}; + +export type CheckoutInfo = { + connection_string: string; + physical_db: string; + lsn: number; + collections: number; + documents: number; +}; + +export type SandboxInfo = { + branch: string; + connection_string: string; + expires_at: string; + forked_from: string; + fork_lsn: number; + pin?: string; +}; + +export type UndoResult = { + from_lsn: number; + to_lsn: number; + compensations: number; + conflicts: number; + unrecoverable: number; + dry_run: boolean; + restored?: number; + deleted?: number; +}; + +export const DEMO_SESSION_LOST = "argon:demo-session-lost"; +function stored(key: string): string | null { + try { + return localStorage.getItem(key); + } catch { + return null; + } +} +const base = stored("argon.api") ?? ""; +const token = stored("argon.token"); +// A renewal supersedes every request started under the previous cookie. +// Query cancellation alone cannot prevent a late response from arriving. +let demoGeneration = 0; + +async function req( + path: string, + init?: { method?: string; body?: unknown }, +): Promise { + const requestGeneration = demoGeneration; + const headers: Record = {}; + if (token) headers.Authorization = `Bearer ${token}`; + if (init?.body !== undefined) headers["Content-Type"] = "application/json"; + const res = await fetch(base + path, { + method: init?.method ?? "GET", + headers, + body: init?.body !== undefined ? JSON.stringify(init.body) : undefined, + }); + const body = await res.json().catch(() => ({})); + if (!res.ok) { + if ( + res.status === 401 && + requestGeneration === demoGeneration && + body.error === "no demo session; POST /api/v1/demo/session first" + ) { + window.dispatchEvent(new Event(DEMO_SESSION_LOST)); + } + throw new Error(body.error ?? `${res.status} ${res.statusText}`); + } + if (path === "/api/v1/demo/session") demoGeneration += 1; + return body as T; +} + +const post = (path: string, body?: unknown) => + req(path, { method: "POST", body: body ?? {} }); +const del = (path: string) => req(path, { method: "DELETE" }); + +const enc = encodeURIComponent; + +export const api = { + meta: () => req("/api/v1/meta"), + ingesters: () => + req<{ ingesters: string[]; count: number; capture?: CaptureStatus[] }>( + "/api/v1/status/ingesters", + ), + + demoSession: () => post("/api/v1/demo/session"), + demoScenario: () => + post<{ + branch: string; + branches?: string[]; + accepted_branch?: string; + pin?: string; + actors: string[]; + hint: string; + }>("/api/v1/demo/scenario"), + + projects: () => req<{ projects: Project[] | null }>("/api/v1/projects"), + createProject: (name: string) => post("/api/v1/projects", { name }), + + branches: (p: string) => + req<{ branches: Branch[] | null }>(`/api/v1/projects/${enc(p)}/branches`), + branch: (p: string, b: string) => + req<{ branch: Branch; connection_string?: string }>( + `/api/v1/projects/${enc(p)}/branches/${enc(b)}`, + ), + createBranch: (p: string, name: string, from: string) => + post(`/api/v1/projects/${enc(p)}/branches`, { name, from }), + deleteBranch: (p: string, b: string) => + del<{ deleted: boolean }>(`/api/v1/projects/${enc(p)}/branches/${enc(b)}`), + checkout: (p: string, b: string) => + post( + `/api/v1/projects/${enc(p)}/branches/${enc(b)}/checkout`, + ), + release: (p: string, b: string) => + post<{ released: boolean }>( + `/api/v1/projects/${enc(p)}/branches/${enc(b)}/release`, + ), + createSnapshot: (p: string, b: string) => + post<{ snapshots: number; lsn: number }>( + `/api/v1/projects/${enc(p)}/branches/${enc(b)}/snapshots`, + ), + + entries: (p: string, b: string, params: Record) => { + const q = new URLSearchParams( + Object.entries(params).map(([k, v]) => [k, String(v)]), + ); + return req<{ entries: Entry[] | null; has_more: boolean }>( + `/api/v1/projects/${enc(p)}/branches/${enc(b)}/entries?${q}`, + ); + }, + timeTravelInfo: (p: string, b: string) => + req( + `/api/v1/projects/${enc(p)}/branches/${enc(b)}/time-travel`, + ), + timeTravelSummary: (p: string, b: string, lsn: number) => + req<{ lsn: number; collections: Record }>( + `/api/v1/projects/${enc(p)}/branches/${enc(b)}/time-travel/query?lsn=${lsn}`, + ), + timeTravelDocs: ( + p: string, + b: string, + lsn: number, + collection: string, + skip = 0, + limit = 25, + ) => + req<{ + lsn: number; + collection: string; + total: number; + documents: Record[]; + }>( + `/api/v1/projects/${enc(p)}/branches/${enc(b)}/time-travel/query?lsn=${lsn}&collection=${enc(collection)}&skip=${skip}&limit=${limit}`, + ), + + diff: (p: string, b: string) => + req(`/api/v1/projects/${enc(p)}/branches/${enc(b)}/diff`), + mergePreview: (p: string, b: string) => + post( + `/api/v1/projects/${enc(p)}/branches/${enc(b)}/merge-preview`, + ), + mergePlans: (p: string) => + req<{ plans: MergePlan[] | null }>(`/api/v1/merge-plans?project=${enc(p)}`), + mergePlan: (id: string) => req(`/api/v1/merge-plans/${enc(id)}`), + mergeApply: (id: string, strategy?: string) => + post<{ applied: number; conflicts_resolved: number }>( + `/api/v1/merge-plans/${enc(id)}/apply`, + strategy ? { strategy } : {}, + ), + + undo: ( + p: string, + b: string, + args: { + from_lsn: number; + to_lsn?: number; + actor?: string; + dry_run: boolean; + }, + ) => + post( + `/api/v1/projects/${enc(p)}/branches/${enc(b)}/undo`, + args, + ), + + pins: (p: string) => + req<{ pins: Pin[] | null }>(`/api/v1/projects/${enc(p)}/pins`), + createPin: ( + p: string, + args: { name: string; branch?: string; lsn?: number; note?: string }, + ) => post(`/api/v1/projects/${enc(p)}/pins`, args), + deletePin: (p: string, name: string) => + del<{ deleted: boolean }>(`/api/v1/projects/${enc(p)}/pins/${enc(name)}`), + branchFromPin: (p: string, pin: string, name: string) => + post(`/api/v1/projects/${enc(p)}/pins/${enc(pin)}/branches`, { + name, + }), + sandboxFromPin: (p: string, pin: string, ttl_minutes?: number) => + post(`/api/v1/projects/${enc(p)}/pins/${enc(pin)}/sandboxes`, { + ...(ttl_minutes ? { ttl_minutes } : {}), + }), + + sandboxes: (p: string) => + req<{ sandboxes: Sandbox[] | null }>( + `/api/v1/projects/${enc(p)}/sandboxes`, + ), + createSandbox: ( + p: string, + args: { name?: string; from?: string; ttl_minutes?: number }, + ) => post(`/api/v1/projects/${enc(p)}/sandboxes`, args), + discardSandbox: (p: string, b: string) => + del<{ discarded: boolean }>( + `/api/v1/projects/${enc(p)}/sandboxes/${enc(b)}`, + ), + extendSandbox: (p: string, b: string, ttl_minutes: number) => + post<{ expires_at: string }>( + `/api/v1/projects/${enc(p)}/sandboxes/${enc(b)}/extend`, + { + ttl_minutes, + }, + ), + keepSandbox: (p: string, b: string) => + post<{ kept: boolean }>( + `/api/v1/projects/${enc(p)}/sandboxes/${enc(b)}/keep`, + ), +}; diff --git a/web/src/funnel.tsx b/web/src/funnel.tsx new file mode 100644 index 0000000..4198364 --- /dev/null +++ b/web/src/funnel.tsx @@ -0,0 +1,85 @@ +import { useEffect, useState } from "react"; +const EVENTS = [ + "demo_entered", + "first_diff", + "first_merge", + "first_undo", + "quickstart_opened", +] as const; +type Event = (typeof EVENTS)[number]; +const CHOICE = "argon.analytics"; +export const QUICKSTART = "https://argonlabs.tech/quickstart"; +export function track(event: Event) { + try { + if ( + !EVENTS.includes(event) || + navigator.doNotTrack === "1" || + localStorage.getItem(CHOICE) !== "yes" + ) + return; + const key = `argon.funnel.${event}`; + if (sessionStorage.getItem(key)) return; + sessionStorage.setItem(key, "1"); + void fetch("/api/v1/events", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ event }), + keepalive: true, + credentials: "omit", + }).catch(() => {}); + } catch { + /* Restricted browser storage must not break the console. */ + } +} +export function LocalLink({ + children = "Continue locally →", +}: { + children?: React.ReactNode; +}) { + return ( +
track("quickstart_opened")} + > + {children} + + ); +} +export function NativeNotice() { + return ( +

+ The hosted demo supports sample-data reviews. Native MongoDB connections + and physical sandboxes require your local engine. +

+ ); +} +export function FunnelChoice() { + const [enabled, setEnabled] = useState(false); + useEffect(() => { + try { + setEnabled(localStorage.getItem(CHOICE) === "yes"); + } catch {} + }, []); + return ( + + ); +} diff --git a/web/src/hooks.ts b/web/src/hooks.ts new file mode 100644 index 0000000..7ad34eb --- /dev/null +++ b/web/src/hooks.ts @@ -0,0 +1,39 @@ +import { useQuery } from "@tanstack/react-query"; +import { api } from "./api"; + +export function useMeta() { + return useQuery({ queryKey: ["meta"], queryFn: api.meta }); +} + +// Write actions disappear when the server says it is read-only — the +// hosted demo serves some projects look-but-don't-touch. +export function useReadOnly(): boolean { + const meta = useMeta(); + return meta.data?.read_only ?? false; +} + +export function useDemo(): boolean { + const meta = useMeta(); + return meta.data?.demo ?? false; +} + +// In demo mode the session cookie is the identity. App owns the expiry timer, +// 401 recovery and explicit renewal; never silently swap projects in the background. +export function useDemoSession() { + const demo = useDemo(); + return useQuery({ + queryKey: ["demo-session"], + queryFn: api.demoSession, + enabled: demo, + staleTime: Infinity, + retry: 2, + }); +} + +// Do not expose native actions before capability metadata arrives. +export function useNativeConnections(): boolean { + const meta = useMeta(); + return ( + !!meta.data && !meta.data.demo && meta.data.native_connections !== false + ); +} diff --git a/web/src/index.css b/web/src/index.css new file mode 100644 index 0000000..25c5c95 --- /dev/null +++ b/web/src/index.css @@ -0,0 +1,60 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + @apply bg-brand-dark text-brand-text antialiased; +} + +/* The guided tour rings the panel it's describing. */ +.tour-ring { + box-shadow: + 0 0 0 1px #96a7ff, + 0 0 0 6px rgba(150, 167, 255, 0.18); +} + +/* The tour card animates in on every (re)open, so clicking "guided tour" + always produces visible motion — even when a card is already showing. */ +@keyframes tourIn { + from { + opacity: 0; + transform: translateY(14px) scale(0.97); + } + to { + opacity: 1; + transform: none; + } +} +.tour-card { + animation: tourIn 0.3s ease-out; +} + +/* Preserve the complete document/actor text without widening mobile pages. */ +@media (max-width: 640px) { + table { + display: block; + overflow-x: auto; + max-width: 100%; + } + th, + td { + min-width: 4rem; + padding-right: 0.75rem; + } +} +a:focus-visible, +button:focus-visible, +input:focus-visible, +select:focus-visible { + outline: 2px solid #96a7ff; + outline-offset: 3px; +} +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + animation: none !important; + transition: none !important; + } +} diff --git a/web/src/main.tsx b/web/src/main.tsx new file mode 100644 index 0000000..801e0a8 --- /dev/null +++ b/web/src/main.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { BrowserRouter } from "react-router-dom"; +import App from "./App"; +import "./index.css"; + +const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: 1, staleTime: 3000 } }, +}); + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + + + , +); diff --git a/web/src/pages/Branch.tsx b/web/src/pages/Branch.tsx new file mode 100644 index 0000000..792a7bf --- /dev/null +++ b/web/src/pages/Branch.tsx @@ -0,0 +1,952 @@ +import { LocalLink, NativeNotice, track } from "../funnel"; +import { useEffect, useMemo, useRef, useState } from "react"; +import { Link, useNavigate, useParams } from "react-router-dom"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { api, Entry, UndoResult } from "../api"; +import { useReadOnly, useNativeConnections, useDemo } from "../hooks"; +import { + actorColor, + Badge, + Button, + ConfirmButton, + Dot, + ErrorNote, + Json, + Loading, + Mono, + Panel, + Select, + TextInput, +} from "../ui"; + +// A prefill request from the agent lens into the undo panel: which actor's +// writes to stage, and from which LSN. The nonce lets the same actor be +// re-requested (it changes even when actor/fromLSN repeat). +type UndoRequest = { fromLSN: number; actor: string; nonce: number }; + +// One ascending pass over the branch's history, shared (react-query dedupes +// the key) by the scrubber's frame stops and the agent lens's per-actor +// stats. 500 entries is plenty for a console view; the timeline paginates +// separately for the full log. +function useHistory(project: string, branch: string) { + return useQuery({ + queryKey: ["history-asc", project, branch], + queryFn: () => api.entries(project, branch, { order: "asc", limit: 500 }), + refetchInterval: 5000, + }); +} + +type ActorStat = { + actor: string; + count: number; + minLSN: number; + collections: string[]; +}; + +function actorStats(entries: Entry[]): ActorStat[] { + const by = new Map(); + for (const e of entries) { + if (!e.actor || !e.collection) continue; + const s = by.get(e.actor) ?? { + actor: e.actor, + count: 0, + minLSN: e.lsn, + collections: [], + }; + s.count += 1; + s.minLSN = Math.min(s.minLSN, e.lsn); + if (!s.collections.includes(e.collection)) s.collections.push(e.collection); + by.set(e.actor, s); + } + return [...by.values()].sort((a, b) => a.minLSN - b.minLSN); +} + +function Actions({ project, branch }: { project: string; branch: string }) { + const native = useNativeConnections(); + const status = useQuery({ + queryKey: ["ingesters"], + queryFn: api.ingesters, + refetchInterval: 5000, + }); + const readOnly = useReadOnly(); + const navigate = useNavigate(); + const qc = useQueryClient(); + const q = useQuery({ + queryKey: ["branch", project, branch], + queryFn: () => api.branch(project, branch), + refetchInterval: 5000, + }); + const refresh = () => { + qc.invalidateQueries({ queryKey: ["branch", project, branch] }); + qc.invalidateQueries({ queryKey: ["branches", project] }); + qc.invalidateQueries({ queryKey: ["ingesters"] }); + }; + const checkout = useMutation({ + mutationFn: () => api.checkout(project, branch), + onSuccess: refresh, + }); + const release = useMutation({ + mutationFn: () => api.release(project, branch), + onSuccess: refresh, + }); + const snapshot = useMutation({ + mutationFn: () => api.createSnapshot(project, branch), + }); + const remove = useMutation({ + mutationFn: () => api.deleteBranch(project, branch), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["branches", project] }); + navigate(`/p/${project}`); + }, + }); + + const capture = status.data?.capture?.find( + (c) => c.branch_id === q.data?.branch.id, + ); + const err = checkout.error ?? release.error ?? snapshot.error ?? remove.error; + return ( + + {q.isError && } + {q.data && ( + <> +
+ + head {q.data.branch.head_lsn} · base {q.data.branch.base_lsn} + + {q.data.branch.state === "live" && ( + + live · {q.data.branch.physical_db} + + )} + {q.data.branch.expires_at && sandbox} +
+ + {native && q.data.connection_string && ( +
+ + {q.data.connection_string} + + +
+ )} + + {!readOnly && ( +
+ {native && + (q.data.branch.state === "live" ? ( + + ) : ( + + ))} + + {branch !== "main" && ( + remove.mutate()} + > + delete branch + + )} + {snapshot.isSuccess && ( + + snapshot @ lsn {snapshot.data.lsn} + + )} +
+ )} + {checkout.isSuccess && !q.data.connection_string && ( +

+ checked out — refreshing… +

+ )} + {err && ( +
+ +
+ )} + {!native && ( +
+ +
+ )} + {status.isError && } + {capture && ( +
+

+ Capture: {capture.state} · actor:{" "} + {capture.actor || "unassigned"} +

+ {capture.updated_at && ( +

+ Status updated {new Date(capture.updated_at).toLocaleString()} +

+ )} + {capture.last_captured_at && ( +

+ Last checkpoint{" "} + {new Date(capture.last_captured_at).toLocaleString()} · + captured LSN {capture.head_lsn ?? "unknown"} +

+ )} + {capture.last_event_lag_ms !== undefined && ( +

+ Last observed capture delay: {capture.last_event_lag_ms} ms + (not current queue lag) +

+ )} + {capture.error && ( +

+ {capture.error} +

+ )} +

+ Actor attribution is shared by this branch/run, not individual + driver clients. Drop/rename DDL is unsupported and marks history + incomplete. +

+
+ )} + {!readOnly && native && q.data.branch.state === "live" && ( +

+ Keep the engine running. Confirm capture is healthy before + reviewing native writes; incomplete images can prevent undo. +

+ )} + + )} +
+ ); +} + +function Diff({ project, branch }: { project: string; branch: string }) { + const readOnly = useReadOnly(); + const navigate = useNavigate(); + const qc = useQueryClient(); + const [open, setOpen] = useState(false); + const diff = useQuery({ + queryKey: ["diff", project, branch], + queryFn: () => api.diff(project, branch), + enabled: open, + }); + const preview = useMutation({ + mutationFn: () => api.mergePreview(project, branch), + onSuccess: (plan) => { + qc.invalidateQueries({ queryKey: ["plans", project] }); + navigate(`/p/${project}/merges/${plan.id}`); + }, + }); + + useEffect(() => { + if (open && diff.isSuccess) track("first_diff"); + }, [open, diff.isSuccess]); + + if (branch === "main") return null; + return ( + + {!open && ( + + )} + {open && diff.isFetching && } + {open && diff.isError && } + {open && diff.data && ( + <> +

+ {diff.data.source_branch} → {diff.data.target_branch} ·{" "} + {(diff.data.changes ?? []).length} changes ·{" "} + + {(diff.data.conflicts ?? []).length} conflicts + +

+
+ {(diff.data.changes ?? []).slice(0, 20).map((ch, i) => ( +

+ + {ch.delete ? "delete" : "put"} + {" "} + + {ch.collection}/{ch.document_id} + +

+ ))} + {(diff.data.changes ?? []).length > 20 && ( +

+ … {(diff.data.changes ?? []).length - 20} more +

+ )} +
+ {!readOnly && (diff.data.changes ?? []).length > 0 && ( +
+ + + persists a reviewable data PR + +
+ )} + {preview.isError && } + + )} +
+ ); +} + +function Undo({ + project, + branch, + actors, + request, +}: { + project: string; + branch: string; + actors: string[]; + request: UndoRequest | null; +}) { + const readOnly = useReadOnly(); + const qc = useQueryClient(); + const [fromLSN, setFromLSN] = useState(""); + const [toLSN, setToLSN] = useState(""); + const [actor, setActor] = useState(""); + type Scope = { from_lsn: number; to_lsn?: number; actor?: string }; + type Review = { result: UndoResult; scope: Scope }; + const [review, setReview] = useState(null); + const revision = useRef(0); + const plan = review?.result; + const invalidateReview = () => { + revision.current += 1; + setReview(null); + }; + + const run = useMutation({ + mutationFn: (args: { dry: boolean; scope: Scope; revision: number }) => + api.undo(project, branch, { ...args.scope, dry_run: args.dry }), + onSuccess: (res, args) => { + // A slow response must not restore a review invalidated by an edit. + if (args.revision === revision.current) { + setReview({ + result: res, + scope: { + from_lsn: res.from_lsn, + to_lsn: res.to_lsn, + ...(args.scope.actor ? { actor: args.scope.actor } : {}), + }, + }); + } + if (!res.dry_run) { + track("first_undo"); + qc.invalidateQueries({ queryKey: ["entries", project, branch] }); + qc.invalidateQueries({ queryKey: ["history-asc", project, branch] }); + qc.invalidateQueries({ queryKey: ["branch", project, branch] }); + qc.invalidateQueries({ queryKey: ["tt-info", project, branch] }); + } + }, + }); + const runMut = run.mutate; + + // The agent lens can stage an actor's writes: fill the form and dry-run + // immediately, so "undo agent:executor" is one click over there. + useEffect(() => { + if (!request) return; + const f = String(request.fromLSN); + setFromLSN(f); + setToLSN(""); + setActor(request.actor); + revision.current += 1; + setReview(null); + runMut({ + dry: true, + scope: { from_lsn: request.fromLSN, actor: request.actor }, + revision: revision.current, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [request?.nonce]); + + if (readOnly) return null; + return ( + +
+ from lsn + { + invalidateReview(); + setFromLSN(value); + }} + width="w-24" + placeholder="990" + /> + + to lsn (opt) + + { + invalidateReview(); + setToLSN(value); + }} + width="w-24" + placeholder="head" + /> + + actor (opt) + + { + setActor(v); + setPages(1); + }} + options={[ + { value: "", label: "all" }, + ...actors.map((a) => ({ value: a, label: a })), + ]} + /> +
+ + {q.isPending && } + {q.isError && } + {q.data && entries.length === 0 && ( +

+ {demo ? ( + <> + No writes yet. Run the sample agent session from the project page, + or start locally. + + ) : ( + <> + No captured writes yet. Check out a branch and keep capture + healthy while writing with your driver. CLI:{" "} + argon checkout + argon watch. + + )} +

+ )} + {entries.length > 0 && ( + <> + + + + + + + + + + + + {entries.map((e: Entry) => ( + + + + + + + + ))} + +
lsnopdocumentactorat
{e.lsn} + + {e.operation} + + + {e.collection + ? `${e.collection}/${e.document_id ?? ""}` + : "—"} + + {e.actor && ( + + {e.actor} + + )} + + {new Date(e.timestamp).toLocaleTimeString()} +
+ {q.data?.has_more && ( +
+ +
+ )} + + )} +
+ ); +} + +function TimeTravel({ project, branch }: { project: string; branch: string }) { + const info = useQuery({ + queryKey: ["tt-info", project, branch], + queryFn: () => api.timeTravelInfo(project, branch), + }); + const history = useHistory(project, branch); + + const [lsn, setLsn] = useState(null); + const [collection, setCollection] = useState(""); + const [playing, setPlaying] = useState(false); + const effectiveLSN = lsn ?? info.data?.LatestLSN ?? 0; + + // The frames worth landing on: LSNs where a data entry changed state. + // Falls back to the endpoints if nothing carries a collection yet. + const stops = useMemo(() => { + const s = new Set(); + for (const e of history.data?.entries ?? []) { + if (e.collection) s.add(e.lsn); + } + if (info.data) { + s.add(info.data.EarliestLSN); + s.add(info.data.LatestLSN); + } + return [...s].sort((a, b) => a - b); + }, [history.data, info.data]); + + // Playback: advance to the next stop on a timer; stop at the end. + useEffect(() => { + if (!playing || stops.length === 0) return; + const id = setInterval(() => { + setLsn((cur) => { + const at = cur ?? stops[stops.length - 1]; + const next = stops.find((s) => s > at); + if (next === undefined) { + setPlaying(false); + return at; + } + return next; + }); + }, 900); + return () => clearInterval(id); + }, [playing, stops]); + + const summary = useQuery({ + queryKey: ["tt-summary", project, branch, effectiveLSN], + queryFn: () => api.timeTravelSummary(project, branch, effectiveLSN), + enabled: !!info.data && info.data.EntryCount > 0, + placeholderData: (prev) => prev, // hold the last frame while the next loads — no flicker + }); + const docs = useQuery({ + queryKey: ["tt-docs", project, branch, effectiveLSN, collection], + queryFn: () => + api.timeTravelDocs(project, branch, effectiveLSN, collection), + enabled: !!collection && !!info.data, + placeholderData: (prev) => prev, + }); + + if (info.isPending) + return ( + + + + ); + if (info.isError) + return ( + + + + ); + const i = info.data!; + if (i.EntryCount === 0) { + return ( + +

+ No data entries to travel through yet. +

+
+ ); + } + + const stepTo = (dir: 1 | -1) => { + setPlaying(false); + setLsn((cur) => { + const at = cur ?? i.LatestLSN; + const next = + dir === 1 + ? stops.find((s) => s > at) + : [...stops].reverse().find((s) => s < at); + return next ?? at; + }); + }; + + return ( + +
+ + + + + lsn {i.EarliestLSN} + + { + setPlaying(false); + setLsn(Number(e.target.value)); + }} + className="w-56 accent-[#96A7FF]" + /> + + {i.LatestLSN} + + @ {effectiveLSN} + {effectiveLSN !== i.LatestLSN && ( + + )} +
+ +
+ {!summary.data && summary.isPending && } + {summary.data && ( +
+ {Object.entries(summary.data.collections) + .sort() + .map(([name, count]) => ( + + ))} + {Object.keys(summary.data.collections).length === 0 && ( +

+ Empty at this LSN. +

+ )} +
+ )} +
+ + {collection && ( +
+ {!docs.data && docs.isPending && } + {docs.isError && } + {docs.data && ( + <> +

+ {collection} @ lsn {docs.data.lsn} · {docs.data.total} docs + {docs.data.total > docs.data.documents.length && + ` (showing ${docs.data.documents.length})`} +

+ + + )} +
+ )} +
+ ); +} + +// The agent lens: who wrote what on this branch, and one-click undo of a +// single actor's writes. This is the "undo button for AI agents" made +// concrete — a session is a set of actors, and any one can be reverted. +function AgentLens({ + project, + branch, + onUndoActor, +}: { + project: string; + branch: string; + onUndoActor: (fromLSN: number, actor: string) => void; +}) { + const readOnly = useReadOnly(); + const history = useHistory(project, branch); + const stats = useMemo( + () => actorStats(history.data?.entries ?? []), + [history.data], + ); + + if (history.isPending) + return ( + + + + ); + if (stats.length === 0) return null; + + return ( + +

+ Scripted API writes carry explicit actors. Native drivers share the + actor configured for their branch/run. Undo requires complete, retained + history; always inspect the dry run. +

+
+ {stats.map((s) => ( +
+ + {s.actor} + + + {s.count} {s.count === 1 ? "write" : "writes"} · from lsn{" "} + {s.minLSN} · {s.collections.join(", ")} + + {!readOnly && ( + + )} +
+ ))} +
+
+ ); +} + +export default function Branch() { + const { project = "", branch = "" } = useParams(); + const [actors, setActors] = useState([]); + const [undoRequest, setUndoRequest] = useState(null); + const undoRef = useRef(null); + + const requestUndo = (fromLSN: number, actor: string) => { + setUndoRequest({ fromLSN, actor, nonce: Date.now() }); + undoRef.current?.scrollIntoView({ behavior: "smooth", block: "start" }); + }; + + return ( +
+ + + + + + + +
+ +
+
+ ); +} diff --git a/web/src/pages/MergePlan.tsx b/web/src/pages/MergePlan.tsx new file mode 100644 index 0000000..44ea602 --- /dev/null +++ b/web/src/pages/MergePlan.tsx @@ -0,0 +1,202 @@ +import { LocalLink, track } from "../funnel"; +import { useState } from "react"; +import { Link, useParams } from "react-router-dom"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { api } from "../api"; +import { useReadOnly } from "../hooks"; +import { + Badge, + ConfirmButton, + ErrorNote, + Json, + Loading, + Panel, + Select, +} from "../ui"; + +export default function MergePlan() { + const { project = "", id = "" } = useParams(); + const readOnly = useReadOnly(); + const qc = useQueryClient(); + const q = useQuery({ + queryKey: ["plan", id], + queryFn: () => api.mergePlan(id), + }); + const [strategy, setStrategy] = useState(""); + + const apply = useMutation({ + mutationFn: () => api.mergeApply(id, strategy || undefined), + onSuccess: () => { + track("first_merge"); + qc.invalidateQueries({ queryKey: ["plan", id] }); + qc.invalidateQueries({ queryKey: ["plans", project] }); + qc.invalidateQueries({ queryKey: ["branches", project] }); + }, + }); + + const conflicts = q.data?.conflicts ?? []; + const canApply = q.data?.status === "pending"; + + return ( +
+ + + {q.isPending && } + {q.isError && } + {q.data && ( + <> + +
+ {q.data.status} + {q.data.strategy && strategy: {q.data.strategy}} + + source head {q.data.source_head} · target head{" "} + {q.data.target_head} · base {q.data.base_lsn} + + {q.data.applied_at && ( + + applied {new Date(q.data.applied_at).toLocaleString()} + + )} +
+ + {!readOnly && canApply && ( +
+ {conflicts.length > 0 && ( + <> + + {conflicts.length} conflicts — pick a strategy + + ({ value: b.name, label: b.name }))} + /> + + {create.isError && } +
+ ); +} + +function Sandboxes({ + project, + branches, +}: { + project: string; + branches: Branch[]; +}) { + const readOnly = useReadOnly(); + const qc = useQueryClient(); + const q = useQuery({ + queryKey: ["sandboxes", project], + queryFn: () => api.sandboxes(project), + refetchInterval: 15000, + }); + const [from, setFrom] = useState("main"); + const [mins, setMins] = useState("60"); + const refresh = () => { + qc.invalidateQueries({ queryKey: ["sandboxes", project] }); + qc.invalidateQueries({ queryKey: ["branches", project] }); + }; + const create = useMutation({ + mutationFn: () => + api.createSandbox(project, { from, ttl_minutes: Number(mins) || 60 }), + onSuccess: refresh, + }); + const act = useMutation({ + mutationFn: async ({ + verb, + name, + }: { + verb: "extend" | "keep" | "discard"; + name: string; + }): Promise => { + if (verb === "extend") + return api.extendSandbox(project, name, Number(mins) || 60); + if (verb === "keep") return api.keepSandbox(project, name); + return api.discardSandbox(project, name); + }, + onSuccess: refresh, + }); + + const boxes = q.data?.sandboxes ?? []; + return ( + + {q.isPending && } + {q.isError && } + {q.data && boxes.length === 0 && ( +

+ No live sandboxes. A sandbox forks a branch, checks it out and expires + on a TTL. +

+ )} + {boxes.length > 0 && ( + + + + + + + + + {boxes.map((s) => ( + + + + + + ))} + +
sandboxexpires +
+ + {s.branch.name} + + + {s.branch.expires_at && ( + {ttl(s.branch.expires_at)} + )} + + {!readOnly && ( + + + + + act.mutate({ verb: "discard", name: s.branch.name }) + } + > + discard + + + )} +
+ )} + {act.isError && } + {!readOnly && ( +
+ from + ({ value: b.name, label: b.name }))} + /> + + {create.isError && } +
+ )} +
+ ); +} + +function AgentScenario({ project }: { project: string }) { + const qc = useQueryClient(); + const run = useMutation({ + mutationFn: api.demoScenario, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["branches", project] }); + qc.invalidateQueries({ queryKey: ["plans", project] }); + }, + }); + return ( + +

+ Two agents fork the same baseline pin. Planner proposes $44 and the + script accepts it into main; executor proposes $1 on a separate branch. + Review that conflict, undo executor on its branch, or undo the accepted + merge on main. +

+
+ + {run.data && ( + + {run.data.hint} + + )} +
+ {run.data && ( +
+

+ Shared pin:{" "} + + {run.data.pin ?? "baseline"} + +

+
+ {(run.data.branches ?? [run.data.branch]).map((name) => ( + + {name === run.data.accepted_branch + ? "Planner (accepted)" + : "Executor (review conflict)"}{" "} + → + + ))} + + Main (accepted data) → + +
+
+ )} + {run.isError && ( +
+ +
+ )} +
+ ); +} + +export default function Project() { + const { project = "" } = useParams(); + const readOnly = useReadOnly(); + const demo = useDemo(); + const native = useNativeConnections(); + const branches = useQuery({ + queryKey: ["branches", project], + queryFn: () => api.branches(project), + refetchInterval: 5000, + }); + const ingesters = useQuery({ + queryKey: ["ingesters"], + queryFn: api.ingesters, + refetchInterval: 5000, + }); + const plans = useQuery({ + queryKey: ["plans", project], + queryFn: () => api.mergePlans(project), + }); + + const capture = new Map( + (ingesters.data?.capture ?? []).map((c) => [c.branch_id, c]), + ); + const list = branches.data?.branches ?? []; + + return ( +
+ + + {demo && } + + +

+ LSN is a history position. HEAD is the latest recorded change; BASE is + the fork point. A live database needs healthy capture before its + writes are reviewable. +

+ {ingesters.isError && } + {branches.isPending && } + {branches.isError && } + {branches.data && ( + + + + + + + + + + + {outline(list).map(({ branch: b, depth }) => ( + + + + + + + ))} + +
branchheadbasestate
+ + {depth > 0 ? "└ " : ""} + + + {b.name} + + {b.head_lsn} + {b.base_lsn} + + + {b.state === "live" && ( + + live + + )} + {capture.has(b.id) && ( + {`capture: ${capture.get(b.id)!.state}`} + )} + {b.state === "live" && !capture.has(b.id) && ( + capture not running + )} + {b.expires_at && ( + {ttl(b.expires_at)} + )} + +
+ )} + {!readOnly && branches.data && ( + + )} +
+ + {native ? ( + + ) : ( + + + + )} + +
+ ); +} diff --git a/web/src/pages/Projects.tsx b/web/src/pages/Projects.tsx new file mode 100644 index 0000000..b853752 --- /dev/null +++ b/web/src/pages/Projects.tsx @@ -0,0 +1,75 @@ +import { useState } from "react"; +import { Link } from "react-router-dom"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { api } from "../api"; +import { useReadOnly, useDemo } from "../hooks"; +import { Button, ErrorNote, Loading, Mono, Panel, TextInput } from "../ui"; + +export default function Projects() { + const q = useQuery({ queryKey: ["projects"], queryFn: api.projects }); + const readOnly = useReadOnly(); + const demo = useDemo(); + const qc = useQueryClient(); + const [name, setName] = useState(""); + + const create = useMutation({ + mutationFn: () => api.createProject(name.trim()), + onSuccess: () => { + setName(""); + qc.invalidateQueries({ queryKey: ["projects"] }); + }, + }); + + return ( + + {q.isPending && } + {q.isError && } + {q.data && + ((q.data.projects ?? []).length === 0 ? ( +

+ No projects yet. Create one below, or import an existing database + with argon import database. +

+ ) : ( + + + + + + + + + + {(q.data.projects ?? []).map((p) => ( + + + + + + ))} + +
namecreatedid
+ + {p.name} + + + {new Date(p.created_at).toLocaleString()} + {p.id}
+ ))} + + {!readOnly && !demo && ( +
+ + + {create.isError && } +
+ )} +
+ ); +} diff --git a/web/src/ui.tsx b/web/src/ui.tsx new file mode 100644 index 0000000..ab06144 --- /dev/null +++ b/web/src/ui.tsx @@ -0,0 +1,241 @@ +// Small shared pieces of the spec-sheet look: 1px borders, mono labels, +// status dots, zero decoration. + +import { ReactNode, useEffect, useState } from "react"; + +export function Panel({ + title, + children, + anchor, +}: { + title?: string; + children: ReactNode; + anchor?: string; // named target for the guided tour to scroll to + highlight +}) { + return ( +
+ {title && ( +
+ {title} +
+ )} +
{children}
+
+ ); +} + +export function Tile({ size = 32 }: { size?: number }) { + return ( + + + 18 + + Ar + + ); +} + +export function Dot({ on }: { on: boolean }) { + return ( + + ); +} + +export function Badge({ + children, + tone = "muted", +}: { + children: ReactNode; + tone?: "muted" | "primary"; +}) { + return ( + + {children} + + ); +} + +export function Mono({ children }: { children: ReactNode }) { + return {children}; +} + +export function Loading({ what }: { what: string }) { + return

loading {what}…

; +} + +export function ErrorNote({ error }: { error: unknown }) { + return ( +

+ {error instanceof Error ? error.message : String(error)} +

+ ); +} + +// Countdown renders "in 54m" and ticks once a minute. +export function Countdown({ to }: { to: string }) { + const [, tick] = useState(0); + useEffect(() => { + const id = setInterval(() => tick((n) => n + 1), 30_000); + return () => clearInterval(id); + }, []); + const ms = new Date(to).getTime() - Date.now(); + if (ms <= 0) return now; + const min = Math.round(ms / 60_000); + return in {min >= 90 ? `${Math.round(min / 60)}h` : `${min}m`}; +} + +// Stable soft color per actor so timelines read at a glance. +const actorPalette = [ + "#96A7FF", + "#7FD1AE", + "#E8B87F", + "#D18FD1", + "#8FC7E8", + "#E88F8F", +]; +export function actorColor(actor: string): string { + let h = 0; + for (let i = 0; i < actor.length; i++) + h = (h * 31 + actor.charCodeAt(i)) >>> 0; + return actorPalette[h % actorPalette.length]; +} + +export function Json({ value }: { value: unknown }) { + return ( +
+      {JSON.stringify(value, null, 2)}
+    
+ ); +} + +// --- action primitives (hidden entirely in read-only mode) --- + +export function Button({ + children, + onClick, + tone = "quiet", + disabled, + title, +}: { + children: ReactNode; + onClick: () => void; + tone?: "quiet" | "solid" | "danger"; + disabled?: boolean; + title?: string; +}) { + const tones = { + quiet: + "border-brand-edge text-brand-text-darker hover:border-brand-primary hover:text-brand-primary", + solid: + "border-brand-primary bg-brand-primary text-brand-dark hover:bg-brand-secondary", + danger: "border-red-400/50 text-red-400 hover:border-red-400", + }[tone]; + return ( + + ); +} + +// Two-step inline confirmation — no native dialogs (they block the tab +// and read as foreign chrome). Click once to arm, again to fire. +export function ConfirmButton({ + children, + onConfirm, + tone = "danger", + disabled, +}: { + children: ReactNode; + onConfirm: () => void; + tone?: "quiet" | "solid" | "danger"; + disabled?: boolean; +}) { + const [armed, setArmed] = useState(false); + if (!armed) { + return ( + + ); + } + return ( + + + + + ); +} + +export function TextInput({ + value, + onChange, + placeholder, + width = "w-44", +}: { + value: string; + onChange: (v: string) => void; + placeholder?: string; + width?: string; +}) { + return ( + onChange(e.target.value)} + placeholder={placeholder} + /> + ); +} + +export function Select({ + value, + onChange, + options, +}: { + value: string; + onChange: (v: string) => void; + options: { value: string; label: string }[]; +}) { + return ( + + ); +} diff --git a/web/tailwind.config.js b/web/tailwind.config.js new file mode 100644 index 0000000..b6b6b5a --- /dev/null +++ b/web/tailwind.config.js @@ -0,0 +1,24 @@ +/** @type {import('tailwindcss').Config} */ +// Brand tokens shared with argon-website (noble-gas identity). +export default { + content: ["./index.html", "./src/**/*.{ts,tsx}"], + theme: { + extend: { + colors: { + "brand-dark": "#0B0E14", // page background + "brand-surface": "#111521", // panels + "brand-edge": "#222838", // 1px borders + "brand-muted": "#8F9BB3", // secondary labels + "brand-primary": "#96A7FF", // argon glow + "brand-secondary": "#6D7FE0", // deeper glow (hover / fills) + "brand-text": "#E8EBF2", + "brand-text-darker": "#9AA3B5", + }, + fontFamily: { + sans: ["Inter", "system-ui", "sans-serif"], + mono: ["ui-monospace", "JetBrains Mono", "SFMono-Regular", "Menlo", "monospace"], + }, + }, + }, + plugins: [], +}; diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..986b0ff --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "useDefineForClassFields": true, + "allowImportingTsExtensions": true, + "noEmit": true + }, + "include": ["src"] +} diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000..d3cb5b2 --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +// In dev, proxy the API to a local engine (`argon console --no-browser` +// or the standalone api binary). In production the SPA is served by the +// engine itself, so everything is same-origin. +const apiOrigin = process.env.ARGON_API_ORIGIN ?? "http://127.0.0.1:1818"; + +export default defineConfig({ + plugins: [react()], + server: { + proxy: { + "/api": apiOrigin, + "/health": apiOrigin, + }, + }, +}); From efa237a53d7805d43053d36fea06f043f663769b Mon Sep 17 00:00:00 2001 From: Jake Wang <2645794+jakezwang@users.noreply.github.com> Date: Thu, 24 Sep 2026 03:36:42 -0400 Subject: [PATCH 6/7] document console fixes and canonical public links --- CHANGELOG.md | 7 +++++++ README.md | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a53037c..b37f926 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,10 @@ entry — a synchronization barrier, not in-proxy capture). resumable boundary and stops cleanly instead of marking a permanent history gap. - Go consumers use valid `github.com/argon-lab/argon/v2` and `github.com/argon-lab/argon/api/v2` modules, with separate matching release tags. +- Console Undo applies the exact actor and bounded LSN range just reviewed; + changing scope invalidates review, including delayed preview responses. +- Expired/reset demo sessions offer a fresh start, clear stale queries and + recover old deep links. Late old-session failures cannot end a renewed session. ### Import and release hardening @@ -36,6 +40,9 @@ entry — a synchronization barrier, not in-proxy capture). - Release publication depends on the complete CI suite for the exact tagged source, consistent committed version metadata and matching root/API tags. CI also compiles an external Go consumer of both v2 modules. +- The MIT console source is public in `web/`. Locked builds reproduce the + embedded assets; browser regressions run against the same engine commit. + Release provenance records engine/UI source and every binary checksum. - Publishing and performance documentation now describes the actual release flow, source-quiescence contract and completed small-scale benchmark evidence. diff --git a/README.md b/README.md index 4942e87..7320a2a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Argon — Git for MongoDB

- + Argon — Git for MongoDB, built for AI agents: branch off prod, run an agent, undo the session

@@ -152,7 +152,7 @@ The full agent workflow: [docs/AGENTS.md](docs/AGENTS.md). [Issues](https://github.com/argon-lab/argon/issues) · [Discussions](https://github.com/argon-lab/argon/discussions) · [Contributing](CONTRIBUTING.md) · -[argonlabs.tech](https://www.argonlabs.tech) +[argonlabs.tech](https://argonlabs.tech) · [Blog](https://argonlabs.tech/blog) --- From c8f9c240b8450da06f2a1aba01b1bad79d97e4a9 Mon Sep 17 00:00:00 2001 From: Jake Wang <2645794+jakezwang@users.noreply.github.com> Date: Thu, 24 Sep 2026 03:39:29 -0400 Subject: [PATCH 7/7] pin available upstream MinIO fixture for required S3 tests --- .github/workflows/ci.yml | 2 +- CONTRIBUTING.md | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c80b24..71ea18b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -118,7 +118,7 @@ jobs: run: | docker run -d --name ci-minio -p 9010:9000 \ -e MINIO_ROOT_USER=argon -e MINIO_ROOT_PASSWORD=argon12345 \ - minio/minio server /data + quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e server /data for i in $(seq 1 30); do curl -sf http://localhost:9010/minio/health/live && break sleep 1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9e382c3..7c8f629 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -70,11 +70,13 @@ golangci-lint run ./... # run in each module you touched The S3 chunk-store tests are gated: they skip unless `ARGON_TEST_S3_ENDPOINT` / `ARGON_TEST_S3_BUCKET` (plus `AWS_*` -credentials) point at an S3-compatible store. Locally, MinIO works: +credentials) point at an S3-compatible store. For a disposable local test fixture, use the pinned historical MinIO image +below. It is not a production storage recommendation; community binaries are +[no longer maintained](https://github.com/minio/minio#source-only-distribution). ```bash docker run -d --name argon-minio -p 9010:9000 \ - -e MINIO_ROOT_USER=argon -e MINIO_ROOT_PASSWORD=argon12345 minio/minio server /data + -e MINIO_ROOT_USER=argon -e MINIO_ROOT_PASSWORD=argon12345 quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z server /data ARGON_TEST_S3_ENDPOINT=http://localhost:9010 ARGON_TEST_S3_BUCKET=argon-test \ AWS_ACCESS_KEY_ID=argon AWS_SECRET_ACCESS_KEY=argon12345 AWS_REGION=us-east-1 \ go test ./tests/wal/ -run TestChunkStore -count=1