Skip to content

CS-10622: Reimplement boxel realm track command#4657

Draft
FadhlanR wants to merge 2 commits intocs-10623-reimplement-boxel-realm-watch-commandfrom
cs-10622-reimplement-boxel-realm-track-command
Draft

CS-10622: Reimplement boxel realm track command#4657
FadhlanR wants to merge 2 commits intocs-10623-reimplement-boxel-realm-watch-commandfrom
cs-10622-reimplement-boxel-realm-track-command

Conversation

@FadhlanR
Copy link
Copy Markdown
Contributor

@FadhlanR FadhlanR commented May 5, 2026

Summary

Port boxel realm track from the standalone cardstack/boxel-cli into the monorepo as boxel realm track. Track is the write-side counterpart to boxel realm watch (PR #4554, CS-10623): it watches the local filesystem via fs.watch + 2s polling, debounces edits, hash-gates noop saves, creates checkpoints in .boxel-history/, and with --push batch-uploads add/update changes to the realm via /_atomic.

Marquee workflow: collaborate on a card while teammates view changes in the web UI.

What's in this PR

  • RealmTracker extends RealmSyncBase — fs.watch + 2s polling, debounced flush, min-interval gate, hash-gated noop suppression, retry-on-fail for transient push failures.
  • boxel realm track <local-dir> <realm-url> with -d/--debounce, -i/--interval, -q/--quiet, -p/--push, -v/--verbose, --realm-secret-seed.
  • Generalized lock module sync-lock.ts (replaces watch-lock.ts) with LockKind = 'watch' | 'track' and bidirectional cross-guard. Track refuses if .boxel-watch.lock is held by a live PID; watch refuses if .boxel-track.lock is. Prevents the push/pull loop on the same dir.
  • Push semantics mirror RealmPusher's: addPaths discriminates op:add vs op:update by manifest membership so the atomic endpoint surfaces 409 from concurrent creates. Modules (.gts/.ts/.js) are sorted before instances (.json) inside the atomic doc.
  • 15 integration tests covering local-only behavior, --push, and lock orchestration.

Out of scope (deferred)

Atomic deletions on --push

op: 'remove' is defined in runtime-common/atomic-document.ts:6 but not implemented server-side: filterAtomicOperations (atomic-document.ts:35) strips no-data ops, and the handler at realm.ts:1498 only iterates add/update. Implementing it requires non-trivial changes to runtime-common/realm.ts (validation, dispatch, indexing/invalidation hooks) and is its own ticket.

Track records deletions in the local checkpoint and logs a verbose skip on push. The deferred-deletion behavior matches the legacy track.ts TODO.

boxel realm stop (CS-10624)

Track lays down .boxel-track.lock so a future realm stop can use it as a discovery source.

Linear

CS-10622

Plan doc

See docs/cs-10622-boxel-realm-track-plan.md (added in this branch; will be removed in the cleanup commit before merge — same convention as CS-10623).

Depends on

PR #4554 (CS-10623) — this branch is based on cs-10623-... so the diff shows only the track delta. Once #4554 merges, this PR rebases onto main.

Test plan

  • pnpm --filter @cardstack/boxel-cli build — succeeds (219KB bundle).
  • pnpm --filter @cardstack/boxel-cli lint — clean.
  • pnpm --filter @cardstack/boxel-cli exec tsc --noEmit — clean.
  • vitest run tests/integration/realm-track.test.ts — 15/15 passing (add/modify/delete/burst/min-interval/hash-gate/push add+update/.gts-first/skip-deletions/no-manifest/409-retain/lock-self/lock-cross/stale-lock-overwrite/abort-flush). Full integration suite also runs the new realm-watch cross-guard cases (refuses-when-track-live, ignores-stale-track-lock).
  • boxel realm track --help documents -i, -d, -q, -p, -v, --realm-secret-seed.
  • Manual smoke against staging:
    • boxel realm sync ./scratch <staging-url> (manifest established).
    • boxel realm track ./scratch <staging-url> --push -v.
    • Edit a .gts and matching .json; confirm local checkpoint, atomic POST visible in verbose output with .gts op listed first, manifest hash updated.
    • In a second terminal: boxel realm watch <staging-url> ./scratch refuses with .boxel-track.lock conflict.
    • Delete a file locally; confirm checkpoint logs deleted and verbose log shows Skipping delete on push (deferred). Remote file remains.
    • Ctrl+C; confirm pending changes flushed, lock released.

Follow-up tickets to file

  • Implement server-side op: 'remove' so track and realm push --delete can use a single atomic doc for delete operations.

🤖 Generated with Claude Code

FadhlanR and others added 2 commits May 5, 2026 20:53
Rename watch-lock.ts to sync-lock.ts and parameterize by
LockKind = 'watch' | 'track'. acquireSyncLock refuses if either the
same kind is held by a live process or the *other* kind is — running
`boxel realm track` and `boxel realm watch` against the same dir
would otherwise create a push/pull loop (track pushes -> server
changes -> watch pulls -> mtime moves -> track pushes again).

A stale same-kind lock is overwritten as before; a stale other-kind
lock is left alone (its owner reclaims it on next run).

Watch updated to call acquireSyncLock(localDir, 'watch', realmUrl).
Two new tests in realm-watch.test.ts cover the cross-guard:
- refuses when a live track lock exists at the same localDir
- ignores a stale track lock from a dead pid and proceeds

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Port the legacy `boxel track` command from the standalone boxel-cli
repo into the monorepo as `boxel realm track`. Track is the write-side
counterpart to `boxel realm watch`: it watches the local filesystem
via fs.watch + 2s polling, debounces edits, hash-gates noop saves,
creates checkpoints in .boxel-history, and with --push batch-uploads
add/update changes to the realm via /_atomic.

Push semantics mirror RealmPusher's: addPaths discriminates op:add vs
op:update by manifest membership so the atomic endpoint can surface
409 from concurrent creates instead of silently overwriting peer
changes. Modules (.gts/.ts/.js) are sorted before instances (.json)
within the atomic doc so definitions land before instances at write
time. Files whose push fails transiently are retained in pending for
the next cycle.

Deletions on --push are deferred:
- `op: 'remove'` is defined in runtime-common/atomic-document.ts but
  not implemented server-side (filterAtomicOperations strips no-data
  ops, the handler iterates only add/update).
- Per-file DELETE is out of scope for this PR.
Track records deletions in the local checkpoint and logs a verbose
skip on push. Filed as follow-up.

Track's lock (.boxel-track.lock) interlocks with watch's via the
generalized sync-lock module landed in the previous commit.

Integration tests cover 15 scenarios across local-only behavior,
--push, and lock orchestration. See docs/cs-10622-... for the full
plan; the doc will be removed in the cleanup commit before merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant