Skip to content

Commit d52bdf6

Browse files
committed
perf(files): parallelize buffer fetches, fix N+1 folder queries, stabilize drag useMemo
- download route: fan out all fetchWorkspaceFileBuffer calls with Promise.all before zip assembly so 100 files resolve in one round-trip instead of sequentially - getWorkspaceFileFolder: replace per-ancestor SELECTs with a single workspace-wide folder load + buildWorkspaceFileFolderPathMap, making depth irrelevant to query count - ensureWorkspaceFileFolderPath: pre-load all workspace folders in one SELECT before the segment loop; resolve existing segments from an in-memory map; only hit the DB to CREATE missing segments; conflict retry path preserved and also updates the map - files.tsx rowDragDropConfig: move activeDropTargetId into a ref so the useMemo does not recompute on every drag-over event
1 parent a6556a5 commit d52bdf6

3 files changed

Lines changed: 81 additions & 16 deletions

File tree

apps/sim/app/api/workspaces/[id]/files/download/route.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,15 @@ export const GET = withRouteHandler(
111111
)
112112
}
113113

114+
// Phase 1: fetch all buffers in parallel
115+
const buffers = await Promise.all(filesToZip.map((file) => fetchWorkspaceFileBuffer(file)))
116+
117+
// Phase 2: assemble zip synchronously so path deduplication is deterministic
114118
const zip = new JSZip()
115119
const usedPaths = new Set<string>()
116-
for (const file of filesToZip) {
117-
const buffer = await fetchWorkspaceFileBuffer(file)
120+
for (let i = 0; i < filesToZip.length; i++) {
121+
const file = filesToZip[i]
122+
const buffer = buffers[i]
118123
const folderPath = file.folderId ? folderPaths.get(file.folderId) : null
119124
const basePath =
120125
safeZipPath(folderPath ? `${folderPath}/${file.name}` : file.name) ||

apps/sim/app/workspace/[workspaceId]/files/files.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,10 @@ export function Files() {
243243
const [saveStatus, setSaveStatus] = useState<SaveStatus>('idle')
244244
const [selectedRowIds, setSelectedRowIds] = useState<Set<string>>(() => new Set())
245245
const [activeDropTargetId, setActiveDropTargetId] = useState<string | null>(null)
246+
const activeDropTargetIdRef = useRef(activeDropTargetId)
247+
useEffect(() => {
248+
activeDropTargetIdRef.current = activeDropTargetId
249+
}, [activeDropTargetId])
246250
const [draggedRowIds, setDraggedRowIds] = useState<Set<string>>(() => new Set())
247251
const [previewMode, setPreviewMode] = useState<PreviewMode>(() => {
248252
if (isNewFile) return 'editor'
@@ -679,7 +683,7 @@ export function Files() {
679683

680684
const rowDragDropConfig = useMemo<RowDragDropConfig>(
681685
() => ({
682-
activeDropTargetId,
686+
activeDropTargetId: activeDropTargetIdRef.current,
683687
draggedRowIds,
684688
isAnyDragActive: draggedRowIds.size > 0,
685689
isRowDraggable: (rowId) => canEdit && listRename.editingId !== rowId,
@@ -813,7 +817,6 @@ export function Files() {
813817
},
814818
}),
815819
[
816-
activeDropTargetId,
817820
draggedRowIds,
818821
canEdit,
819822
listRename.editingId,

apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,24 @@ export async function getWorkspaceFileFolder(
282282
): Promise<WorkspaceFileFolderRecord | null> {
283283
const { includeDeleted = false } = options ?? {}
284284
const folder = await getRawWorkspaceFileFolder(workspaceId, folderId, { includeDeleted })
285-
return folder ? mapFolderWithPath(workspaceId, folder, { includeDeleted }) : null
285+
if (!folder) return null
286+
287+
// Load all folders in one query to build the path map instead of chaining
288+
// per-ancestor SELECTs inside buildWorkspaceFileFolderPath.
289+
const allFolders = await db
290+
.select()
291+
.from(workspaceFileFolder)
292+
.where(
293+
includeDeleted
294+
? eq(workspaceFileFolder.workspaceId, workspaceId)
295+
: and(
296+
eq(workspaceFileFolder.workspaceId, workspaceId),
297+
isNull(workspaceFileFolder.deletedAt)
298+
)
299+
)
300+
301+
const paths = buildWorkspaceFileFolderPathMap(allFolders)
302+
return mapFolder(folder, paths)
286303
}
287304

288305
export async function assertWorkspaceFileFolderTarget(
@@ -391,36 +408,76 @@ export async function ensureWorkspaceFileFolderPath(params: {
391408
userId: string
392409
pathSegments: string[]
393410
}): Promise<string | null> {
411+
if (params.pathSegments.length === 0) return null
412+
413+
// Load all active folders once and build a lookup keyed by "name|parentId"
414+
// so we can resolve existing segments without a per-segment SELECT.
415+
const existingFolders = await db
416+
.select()
417+
.from(workspaceFileFolder)
418+
.where(
419+
and(
420+
eq(workspaceFileFolder.workspaceId, params.workspaceId),
421+
isNull(workspaceFileFolder.deletedAt)
422+
)
423+
)
424+
425+
/** Key format: `${name}|${parentId ?? ''}` */
426+
const folderByNameParent = new Map<string, RawWorkspaceFileFolder>()
427+
for (const folder of existingFolders) {
428+
folderByNameParent.set(`${folder.name}|${folder.parentId ?? ''}`, folder)
429+
}
430+
394431
let parentId: string | null = null
432+
395433
for (const rawSegment of params.pathSegments) {
396434
const name = normalizeWorkspaceFileItemName(rawSegment, 'Folder')
435+
const lookupKey = `${name}|${parentId ?? ''}`
397436

398-
const existing = await findRawWorkspaceFileFolderByName(params.workspaceId, name, parentId)
399-
if (existing) {
400-
parentId = existing.id
437+
const cached = folderByNameParent.get(lookupKey)
438+
if (cached) {
439+
parentId = cached.id
401440
continue
402441
}
403442

404443
try {
405-
parentId = (
406-
await createWorkspaceFileFolder({
407-
workspaceId: params.workspaceId,
408-
userId: params.userId,
409-
name,
410-
parentId,
411-
})
412-
).id
444+
const created = await createWorkspaceFileFolder({
445+
workspaceId: params.workspaceId,
446+
userId: params.userId,
447+
name,
448+
parentId,
449+
})
450+
// Insert the newly created folder into the in-memory map so subsequent
451+
// segments in this path can find their parent without extra DB round trips.
452+
folderByNameParent.set(`${created.name}|${created.parentId ?? ''}`, {
453+
id: created.id,
454+
workspaceId: created.workspaceId,
455+
userId: created.userId,
456+
name: created.name,
457+
parentId: created.parentId,
458+
sortOrder: created.sortOrder,
459+
deletedAt: created.deletedAt,
460+
createdAt: created.createdAt,
461+
updatedAt: created.updatedAt,
462+
})
463+
parentId = created.id
413464
} catch (error) {
414465
if (
415466
error instanceof WorkspaceFileFolderConflictError ||
416467
getPostgresErrorCode(error) === '23505'
417468
) {
469+
// A concurrent request created this folder between our initial load and
470+
// the INSERT — fall back to a targeted SELECT to get its id.
418471
const concurrentExisting = await findRawWorkspaceFileFolderByName(
419472
params.workspaceId,
420473
name,
421474
parentId
422475
)
423476
if (concurrentExisting) {
477+
folderByNameParent.set(
478+
`${concurrentExisting.name}|${concurrentExisting.parentId ?? ''}`,
479+
concurrentExisting
480+
)
424481
parentId = concurrentExisting.id
425482
continue
426483
}

0 commit comments

Comments
 (0)