Skip to content

Commit c5a834c

Browse files
committed
feat: load directory debounce and unlimited watchDir timeout
1 parent 605f56c commit c5a834c

2 files changed

Lines changed: 68 additions & 13 deletions

File tree

src/features/dashboard/sandbox/inspect/context.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ export function SandboxInspectProvider({
4747
const storeRef = useRef<FilesystemStore | null>(null)
4848
const eventManagerRef = useRef<FilesystemEventManager | null>(null)
4949
const operationsRef = useRef<FilesystemOperations | null>(null)
50-
const [sandbox, setSandbox] = useState<Sandbox | null>(null)
5150

5251
const router = useRouter()
5352

@@ -174,8 +173,6 @@ export function SandboxInspectProvider({
174173
secure: true,
175174
})
176175

177-
setSandbox(sandbox)
178-
179176
eventManagerRef.current = new FilesystemEventManager(
180177
storeRef.current,
181178
sandbox,
@@ -190,7 +187,7 @@ export function SandboxInspectProvider({
190187
}
191188
}, [sandboxId, teamId, rootPath, router])
192189

193-
if (!storeRef.current || !operationsRef.current || !sandbox) {
190+
if (!storeRef.current || !operationsRef.current) {
194191
return null // should never happen, but satisfies type-checker
195192
}
196193

src/features/dashboard/sandbox/inspect/filesystem/events-manager.ts

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,19 @@ export class FilesystemEventManager {
1616
private store: FilesystemStore
1717
private sandbox: Sandbox
1818

19+
// ms delay used when batching rapid load requests
20+
private static readonly LOAD_DEBOUNCE_MS = 300
21+
22+
private loadTimers: Map<string, ReturnType<typeof setTimeout>> = new Map()
23+
private pendingLoads: Map<
24+
string,
25+
{
26+
promise: Promise<void>
27+
resolve: () => void
28+
reject: (err: unknown) => void
29+
}
30+
> = new Map()
31+
1932
constructor(store: FilesystemStore, sandbox: Sandbox, rootPath: string) {
2033
this.store = store
2134
this.sandbox = sandbox
@@ -32,7 +45,7 @@ export class FilesystemEventManager {
3245
this.watchHandle = await this.sandbox.files.watchDir(
3346
this.rootPath,
3447
(event) => this.handleFilesystemEvent(event),
35-
{ recursive: true }
48+
{ recursive: true, timeout: 0, timeoutMs: 0 }
3649
)
3750
} catch (error) {
3851
console.error(`Failed to start root watcher on ${this.rootPath}:`, error)
@@ -120,6 +133,49 @@ export class FilesystemEventManager {
120133
}
121134

122135
async loadDirectory(path: string): Promise<void> {
136+
console.log('LOAD_DIRECTORY', path)
137+
138+
const normalizedPath = normalizePath(path)
139+
140+
// if there is already a scheduled load for this path, reset the timer and return its promise
141+
const existingTimer = this.loadTimers.get(normalizedPath)
142+
if (existingTimer) {
143+
clearTimeout(existingTimer)
144+
}
145+
146+
let pending = this.pendingLoads.get(normalizedPath)
147+
if (!pending) {
148+
let res!: () => void
149+
let rej!: (err: unknown) => void
150+
const promise = new Promise<void>((resolve, reject) => {
151+
res = resolve
152+
rej = reject
153+
})
154+
pending = { promise, resolve: res, reject: rej }
155+
this.pendingLoads.set(normalizedPath, pending)
156+
}
157+
158+
const timer = setTimeout(async () => {
159+
// once the timer fires, perform the actual load then resolve/reject all waiters
160+
this.loadTimers.delete(normalizedPath)
161+
try {
162+
await this.loadDirectoryImmediate(normalizedPath)
163+
pending!.resolve()
164+
} catch (err) {
165+
pending!.reject(err)
166+
} finally {
167+
this.pendingLoads.delete(normalizedPath)
168+
}
169+
}, FilesystemEventManager.LOAD_DEBOUNCE_MS)
170+
171+
this.loadTimers.set(normalizedPath, timer)
172+
173+
return pending.promise
174+
}
175+
176+
private async loadDirectoryImmediate(path: string): Promise<void> {
177+
console.log('LOAD_DIRECTORY_IMMEDIATE', path)
178+
123179
const normalizedPath = normalizePath(path)
124180
const state = this.store.getState()
125181
const node = state.getNode(normalizedPath)
@@ -160,6 +216,15 @@ export class FilesystemEventManager {
160216
})
161217

162218
state.addNodes(normalizedPath, nodes)
219+
220+
const newChildrenSet = new Set(nodes.map((n) => normalizePath(n.path)))
221+
222+
for (const childPath of [...node.children]) {
223+
if (!newChildrenSet.has(childPath)) {
224+
state.removeNode(childPath)
225+
}
226+
}
227+
163228
state.updateNode(normalizedPath, { isLoaded: true })
164229
} catch (error) {
165230
const errorMessage =
@@ -175,16 +240,9 @@ export class FilesystemEventManager {
175240
const normalizedPath = normalizePath(path)
176241
const state = this.store.getState()
177242

243+
// mark directory as stale but keep existing children until fresh data arrives
178244
state.updateNode(normalizedPath, { isLoaded: false })
179245

180-
const node = state.getNode(normalizedPath)
181-
if (node && node.type === FileType.DIR) {
182-
const childrenPaths = [...node.children]
183-
for (const childPath of childrenPaths) {
184-
state.removeNode(childPath)
185-
}
186-
}
187-
188246
await this.loadDirectory(normalizedPath)
189247
}
190248
}

0 commit comments

Comments
 (0)