Skip to content

Perf/collection mount#1

Open
chirag-bruno wants to merge 17 commits into
feat/benchmark-collection-mountfrom
perf/collection-mount
Open

Perf/collection mount#1
chirag-bruno wants to merge 17 commits into
feat/benchmark-collection-mountfrom
perf/collection-mount

Conversation

@chirag-bruno
Copy link
Copy Markdown
Owner

Description

Contribution Checklist:

  • I've used AI significantly to create this pull request
  • The pull request only addresses one issue or adds one feature.
  • The pull request does not introduce any breaking changes
  • I have added screenshots or gifs to help explain the change if applicable.
  • I have read the contribution guidelines.
  • Create an issue and link to the pull request.

Note: Keeping the PR small and focused helps make it easier to review and merge. If you have multiple changes you want to make, please consider submitting them as separate pull requests.

Publishing to New Package Managers

Please see here for more information.

cchirag added 17 commits May 14, 2026 00:41
Introduces a sqlite-backed Index class under bruno-electron's mount
pipeline that provides git-like status/stage operations against a
collection on disk. Status walks the collection, compares mtime then
content hash against stored entries, and reports added/updated/removed.
Stage upserts or deletes individual entries; transaction wraps batches.

Uses node:sqlite with an in-memory option for tests, path.matchesGlob
for caller-supplied denylist filtering, and SHA-256 of file bytes for
change detection.
Adds a Parser class under bruno-electron's mount pipeline that runs
file parsing across a piscina worker pool. Each worker reads the file,
captures mtime and content hash, and dispatches to the @usebruno/filestore
parser by (format, type): bru/yml requests, collections, folders,
environments, dotenv, and json config.

Public API:
- parse(collectionPath, relativePath, format, type) -> Promise<result>
- parseAll(collectionPath, entries[]) -> Promise<Map>
- results getter, clear(), close()

Result shape: { relativePath, mtime, hash, data, format, type }.
Errors are surfaced both as promise rejections and as entries in the
results Map ({ relativePath, error }), so parseAll does not fail the
batch when a single file is malformed.

Pins piscina at 5.1.4.
Adds buildTree(collectionPath, parserResults) under bruno-electron's
mount pipeline. Consumes the Map produced by the worker-pool Parser
and emits a tree shaped to match what the renderer already expects:

  { pathname, brunoConfig, root, items, environments }

Folders are synthesized from nested request paths; folder.bru data
attaches name/seq/root to its folder (and creates the folder even
when it has no descendants). collection.bru/yml and bruno.json are
extracted into tree.root and tree.brunoConfig. environments/*.bru
land in tree.environments instead of tree.items.

Sort fidelity matches the current renderer: folders before requests
at every level, requests sorted by seq, folders sorted via
sortByNameThenSequence (alpha baseline + splice by seq + group on
collision) ported verbatim.

Uids are deterministic: sha256(toPosix(absolutePath)). Hashing the
absolute path (not the relative path) ensures two collections with
identical internal layouts get distinct uids. Updates git-lite's
Index.id to the same formula so snapshot and renderer share one
identifier scheme.
Introduces a callback-based Watcher around chokidar that surfaces
'add'/'change'/'unlink'/'addDir'/'unlinkDir' events with the same
ignored semantics as the existing collection watcher (DENY_DIRS plus
caller-supplied denylist globs).

The Mount orchestrator at ipc/mount/index.js plugs the four mount
modules together. On start, it hydrates an in-memory state Map from
the snapshot DB, runs an initial reconcile against disk, emits the
first tree, and awaits chokidar's ready before returning. Subsequent
FS events trigger a debounced reconcile:

  Index.status() -> Parser.parseAll(added+updated) ->
  Index.transaction(stage all) -> buildTree -> onTree(tree)

Path-based classify() dispatches each relative path to the right
(format, type) pair for the parser; files that don't match any
known type are skipped.

Adds Index.entries(collectionPath) to read back all stored rows as a
Map<relPath, { data }>, used by Mount to repopulate state across
restarts.
Adds a 'mount-v2' beta preference that, when enabled, routes
renderer:mount-collection through the new sqlite-backed Mount
pipeline (Index + Parser + Watcher + buildTree) instead of the
existing chokidar-driven per-file event stream.

Main:
- New src/ipc/mount-v2.js owns singleton Index and Parser,
  manages per-collection Mount instances, and sends
  main:collection-tree-loaded plus main:collection-loading-state-updated.
- renderer:mount-collection branches on the flag.
- renderer:remove-collection unmounts the Mount.
- before-quit shuts down singletons.

Renderer:
- BETA_FEATURES.MOUNT_V2 registered, surfaced in Preferences > Beta.
- New main:collection-tree-loaded listener in useIpcEvents.
- New collectionLoadedFromTree reducer that replaces collection.items,
  environments, root, and brunoConfig from the tree blob and runs
  addDepth(items).

The renderer's existing collection actions are unchanged; the flag
check happens server-side so all entry points pick up the new path.
Moves the mount pipeline v2 toggle from Preferences > Beta into a
File cache entry under Preferences > Cache, alongside the existing
SSL session cache. The flag itself moves from beta['mount-v2'] to
cache.file.enabled with a matching isFileCacheEnabled() helper.

Adds renderer:get-file-cache-size and renderer:clear-file-cache IPCs
backed by the snapshot sqlite file (Index.clear() drops all rows,
Index.dbPath exposes the location). The Cache UI shows the current
cache size and a Clear cache button per row.

Cache section redesigned to a card layout: gray header with title,
beta badge where applicable, and toggle; white body with description,
size readout (for File cache), and a bordered Clear cache button.

Fixes ToggleSwitch using a hardcoded id so multiple instances on the
same page now get unique label-input pairs via React.useId(), which
was making the SSL toggle redirect clicks to the File cache toggle.
Removes the cache.file.enabled flag branch from renderer:mount-collection
in ipc/collection.js, restoring the old handler to its pre-v2 state.
The v2 pipeline now has its own dedicated IPC channels:

- renderer:mount-collection-v2
- renderer:unmount-collection-v2
- main:collection-loading-state-updated-v2

Both handlers create their own transient directory and use
BrowserWindow.fromWebContents(event.sender) so the v2 path no longer
shares any code path with the old chokidar-based watcher. Strict
separation: the frontend chooses which channel to call based on the
flag; main has no flag-branching.
Renderer:
- mountCollection picks renderer:mount-collection-v2 when
  cache.file.enabled is on, else renderer:mount-collection.
- removeCollection invokes renderer:unmount-collection-v2 alongside
  renderer:remove-collection so cleanup runs for either path
  (both IPCs are idempotent).
- New listeners for main:collection-loading-state-updated-v2 and
  main:bruno-config-update-v2; both dispatch the existing shared
  reducer actions (updateCollectionLoadingState, brunoConfigUpdateEvent)
  since the state shape is identical and the trigger paths are separate.

Main:
- mount-v2 sendTree now runs transformBrunoConfigAfterRead on
  tree.brunoConfig, stores the result via setBrunoConfig, emits
  main:bruno-config-update-v2, and only then dispatches the tree.
- tree-builder splits yml collection root: when the parsed payload
  carries both collectionRoot and brunoConfig, hoist brunoConfig to
  the tree level so the v2 path matches the legacy yml flow.
tree-builder:
- Hydrate per-item uuids on request data (params, headers, vars,
  assertions, body lists) and on examples (deterministic example uid
  via idFor(absolutePath#example#index) plus parent itemUid). Matches
  the legacy hydrateRequestWithUuid behavior.
- Apply the same hydration to tree.root and each folder.root.

mount-v2:
- Hydrate environments after build: per-variable uuid plus secret
  value decryption via EnvironmentSecretsStore + decryptStringSafe.
- After mount.start() resolves, look up snapshotManager.getCollection
  and emit main:hydrate-app-with-ui-state-snapshot-v2 so the renderer
  can restore tabs and active environment, mirroring the legacy
  onWatcherSetupComplete path.

useIpcEvents: add a v2 snapshot-hydration listener that dispatches the
existing hydrateCollectionWithUiStateSnapshot action.
watcher:
- On chokidar error with code ENOSPC or EMFILE, close the failing
  watcher and re-attach with usePolling: true. One-shot fallback;
  subsequent unrelated errors propagate to onError. Matches the
  fallback behavior in collection-watcher.js.

mount:
- Detect renames during reconcile: when an added entry's hash matches
  a removed entry's hash, treat the pair as a rename and migrate the
  request uid for the new absolute path. Maintains an in-memory
  uidOverrides Map so per-session renames preserve the same uid the
  renderer was already keyed on.

tree-builder:
- buildTree takes options.uidOverrides; buildRequestNode consults it
  before falling back to the deterministic sha256(absPath) uid.

git-lite:
- Index.status includes hash on removed entries so rename detection
  can match by content.
Adds a TransientWatcher (mount/transient-watcher/index.js) that
watches a collection's temp directory at depth 1, ignores
metadata.json, parses each transient file via the shared Parser,
and maintains an in-memory state Map. Fires onUpdate whenever the
state changes.

mount-v2 now creates one TransientWatcher per collection alongside
the Mount. Both update channels feed a shared emitMergedTree path:
mount.getTree({ transientEntries }) is called with the watcher's
current entries, and the result is dispatched via the existing
v2 tree IPC. Transient items appear as flat top-level leaves with
isTransient: true and participate in the same root-level sort.

tree-builder:
- Splits buildRequestNode into a collectionPath-relative helper and a
  buildRequestNodeForAbsolutePath core that accepts an absolute path
  directly, so transient nodes (which live outside collectionPath)
  reuse the same hydration and uid logic.
- buildTree accepts options.transientEntries; appends them to root
  items, flags them isTransient: true, sorts the resulting items.

mount:
- Exposes getTree(options) so callers can pass transient entries
  through to buildTree without touching private state. The existing
  tree getter delegates to getTree().
mount-v2:
- Attach dotenv-watcher (shared with v1) on mountCollectionV2 so the
  v2 path gets .env/.env.* shell-environment propagation. Detach on
  unmountCollectionV2.
- Subsequent mountCollectionV2 calls for an already-mounted collection
  refresh the stored win reference and re-emit the current tree plus
  UI state snapshot. This is how the v2 path recovers after a renderer
  reload: the renderer re-invokes renderer:mount-collection-v2 on
  startup; the still-running Mount serves the existing state without
  re-walking disk.
- Move the win reference onto the mount entry so callbacks read it
  through the entry instead of capturing it in a closure.
Replaces crypto.randomUUID() with sha256-derived ids for every
child node generated by hydration. Stable IDs are required for any
snapshot/persistence layer that pins to uids across app restarts
(otherwise a clean reload reissues random ids every parse).

tree-builder:
- Per-item uids in request.params/headers/vars.req/vars.res/assertions/
  body.formUrlEncoded/body.multipartForm/body.file are now
  sha256(toPosix(absolutePath)#section#index).
- Example child lists follow the same scheme seeded by the example uid.

mount-v2 environment hydration:
- Variable uid is sha256(envUid#var#name) (falls back to index when
  name is missing). Stable across restarts; only changes when a
  variable is renamed.
Bruno's shared uidSchema enforces exactly 21 alphanumeric chars
(packages/bruno-schema/src/common/index.js). Full sha256 hex was
64 chars, which made the renderer reject every v2 collection item
with 'uid must be 21 characters in length'.

Every uid generator in the v2 pipeline now slices the sha256 hex
to its first 21 chars:
- tree-builder idFor, idForChild, and example uid (toUid helper)
- mount Mount.defaultUidFor (rename uid migration)
- mount-v2 hydrateEnvironments variable uid (toUid helper)
- git-lite Index.idForAbsolutePath (DB id, kept consistent for
  parity with the renderer-side uid)

21 hex chars = ~84 bits of entropy; collision probability stays
negligible for any realistic collection size.

Updates the tree-builder spec helper to truncate too so existing
deterministic-uid expectations still match.
findCollectionPathByItemPath in ipc/collection.js previously queried
only collectionWatcher.getAllWatcherPaths(), which is empty for v2
collections. Renderer-driven actions like renderer:new-request that
resolve the owning collection from an item pathname were failing with
'Collection not found for the given pathname' whenever the user had
the file cache enabled.

mount-v2 now exports getMountedCollectionPaths() which returns the
list of currently-mounted v2 collection paths. The path-resolver
concatenates both sources before matching.

No flow mixing: each pipeline still owns its own registry; the
resolver is a read-only directory utility that needs to know about
all mounted collections regardless of which pipeline put them there.
collectionLoadedFromTree was replacing collection.items wholesale on
every tree push, which wiped every renderer-only field on a request
item (response, requestSent, cancelTokenUid, run timeline, ...).
Saving a request and then re-receiving the tree would erase the
visible response.

The reducer now merges by uid (stable in v2):
- Matched request items keep all existing fields; only the file-derived
  fields (name, type, seq, tags, request, settings, examples, filename,
  pathname, partial, loading, size, error, isTransient) are overwritten.
- Seq-only updates take a fast path: only seq (and draft.seq if a draft
  exists) are updated, preserving the rest. Matches the legacy
  collectionChangeFileEvent behavior for sort/re-order operations.
- Drafts are cleared only when the file content matches the draft
  (autosave caught up). Otherwise the draft survives, preserving
  characters typed after the most recent save.
- Matched folders keep runtime state (collapsed, etc.); only the
  file-derived fields are overwritten. Children are merged recursively.
- New items (no existing uid) are inserted as-is.
- Existing items missing from the new tree are dropped (deleted on disk).
Three coupled fixes that together let v2 own the scratch-collection
flow end-to-end:

1. workspaces/actions.js mountScratchCollection branches on
   cache.file.enabled. When the flag is on it calls
   renderer:mount-collection-v2 instead of the v1
   renderer:add-collection-watcher. Eliminates the v1 chokidar watcher
   that was firing for scratch collections regardless of the flag.

2. tasks/middleware.js extracts the open-request logic into
   tryOpenRequestTasks and adds a parallel listener on
   collectionLoadedFromTree. The v2 reducer never dispatched
   collectionAddFileEvent, so the OPEN_REQUEST task for a freshly
   created transient request stayed queued and the tab never opened.

3. collections/index.js collectionLoadedFromTree annotates items as
   isTransient when their pathname lives under
   state.tempDirectories[collectionUid]. Mirrors how the v1 per-event
   reducer computed isTransientFile, so scratch collections (where the
   temp dir IS the collection path) get every item flagged transient.
   Regular collections are unaffected since their items live under
   collection.pathname, not the temp dir.
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.

2 participants