Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
## 2026-05-16 - [Batch FeathersJS user fetches with $in operator to fix N+1 query]
**Learning:** FeathersJS allows passing `$in` clauses through the query parameter (e.g. `user_id: { $in: ownerIds }`). When writing custom Feathers service logic, you can easily parse this array and pass it to Drizzle's `inArray()` to perform a batched query, instead of looping over `service.get(id)` causing N+1 database roundtrips.
**Action:** When implementing or updating custom Feathers `find()` methods, extract and parse the `$in` parameters to support batched Drizzle `inArray()` lookups, and always replace `Promise.all(ids.map(id => service.get(id)))` with a single batched `find()` call.
## 2025-02-12 - [Optimize bulk worktree creation by hoisting loadConfig]
**Learning:** When performing bulk array mapping operations inside a class method (e.g. `create` taking `data[]`), repeatedly calling internal instance methods that perform synchronous disk I/O or configuration parsing (like `loadConfig()`) can easily trigger performance degradation via N simultaneous file reads.
**Action:** When mapping over an array to process records and an underlying shared configuration or static data is needed, evaluate whether it can be loaded once *before* the mapping operation and explicitly passed into the individual worker method to prevent O(N) configuration parsing/disk I/O.
20 changes: 16 additions & 4 deletions apps/agor-daemon/src/services/worktrees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ import { existsSync } from 'node:fs';
import { mkdir } from 'node:fs/promises';
import { homedir } from 'node:os';
import { dirname, join } from 'node:path';
import { ENVIRONMENT, isWorktreeRbacEnabled, loadConfig, PAGINATION } from '@agor/core/config';
import {
type AgorConfig,
ENVIRONMENT,
isWorktreeRbacEnabled,
loadConfig,
PAGINATION,
} from '@agor/core/config';
import { type Database, WorktreeRepository, type WorktreeWithZoneAndSessions } from '@agor/core/db';
import { renderWorktreeSnapshot } from '@agor/core/environment/render-snapshot';
import type { Application } from '@agor/core/feathers';
Expand Down Expand Up @@ -226,8 +232,11 @@ export class WorktreesService extends DrizzleService<Worktree, Partial<Worktree>
* so admins can set org-wide defaults in config.yaml. Explicit values on the
* input always win; defaults fill in only when the caller omits the field.
*/
private async applyWorktreeCreateDefaults(data: Partial<Worktree>): Promise<Partial<Worktree>> {
const config = await loadConfig();
private async applyWorktreeCreateDefaults(
data: Partial<Worktree>,
preloadedConfig?: AgorConfig
): Promise<Partial<Worktree>> {
const config = preloadedConfig ?? (await loadConfig());
const defaults = config.worktrees;
if (!defaults) return data;

Expand All @@ -252,8 +261,11 @@ export class WorktreesService extends DrizzleService<Worktree, Partial<Worktree>
params?: WorktreeParams
): Promise<Worktree | Worktree[]> {
if (Array.isArray(data)) {
// ⚑ Bolt Performance Optimization:
// Load config once before the loop to prevent N simultaneous disk I/O reads.
const config = await loadConfig();
const withDefaults = await Promise.all(
data.map((item) => this.applyWorktreeCreateDefaults(item))
data.map((item) => this.applyWorktreeCreateDefaults(item, config))
);
return super.create(withDefaults, params) as Promise<Worktree[]>;
}
Expand Down
22 changes: 22 additions & 0 deletions plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
1. **Identify the Bottleneck**:
- `loadConfig()` reads and parses the `~/.agor/config.yaml` file on every single call.
- In `apps/agor-daemon/src/services/worktrees.ts`, the `create` method processes bulk inputs using `Promise.all(data.map(item => this.applyWorktreeCreateDefaults(item)))`.
- Since `applyWorktreeCreateDefaults` internally calls `await loadConfig()`, this triggers N simultaneous disk I/O reads (N = number of items). This can cause major performance issues when processing large arrays.

2. **Implement the Optimization**:
- Instead of calling `await loadConfig()` inside `applyWorktreeCreateDefaults` (which gets called in the map loop), we can load the config **once** outside the loop in the `create` method.
- Update `applyWorktreeCreateDefaults` to accept the loaded `config` as an optional parameter (or always pass it from `create`).
- If not passed, default to loading it once (to support other places that might use it).
- We will modify `apps/agor-daemon/src/services/worktrees.ts`.

3. **Verify**:
- Run tests for `@agor/daemon`.
- Run linter/formatter.

4. **Pre-commit**:
- Complete pre commit steps to ensure proper testing, verification, review, and reflection are done.

5. **Create PR**:
- Branch: `perf/worktree-bulk-create`
- Title: `⚑ Bolt: Optimize bulk worktree creation by hoisting loadConfig`
- PR description explaining the N disk I/O reduction.