Skip to content

Commit 902c60c

Browse files
authored
fix(devtools): scope storage watchers to avoid EMFILE (#934)
1 parent 706a760 commit 902c60c

4 files changed

Lines changed: 66 additions & 12 deletions

File tree

packages/devtools/src/server-rpc/server-routes.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import type { Nitro } from 'nitropack'
22
import type { NuxtDevtoolsServerContext, ServerFunctions, ServerRouteInfo } from '../types'
33
import { debounce } from 'perfect-debounce'
4+
import { watchStorageMount } from './storage-watch'
45

56
export function setupServerRoutesRPC({ nuxt, refresh }: NuxtDevtoolsServerContext) {
67
let nitro: Nitro
8+
let unwatchStorage: (() => Promise<void> | void) | undefined
79

810
let cache: ServerRouteInfo[] | null = null
911

@@ -18,13 +20,22 @@ export function setupServerRoutesRPC({ nuxt, refresh }: NuxtDevtoolsServerContex
1820
refresh('getServerRoutes')
1921
})
2022

21-
nuxt.hook('ready', () => {
22-
nitro?.storage.watch((event, key) => {
23+
nuxt.hook('ready', async () => {
24+
if (!nitro)
25+
return
26+
27+
await unwatchStorage?.()
28+
unwatchStorage = await watchStorageMount(nitro.storage, 'src', (_event, key) => {
2329
if (key.startsWith('src:api:') || key.startsWith('src:routes:'))
2430
refreshDebounced()
2531
})
2632
})
2733

34+
nuxt.hook('close', async () => {
35+
await unwatchStorage?.()
36+
unwatchStorage = undefined
37+
})
38+
2839
function scan() {
2940
if (cache)
3041
return cache

packages/devtools/src/server-rpc/server-tasks.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import type { Nitro } from 'nitropack'
22
import type { NuxtDevtoolsServerContext, ScannedNitroTasks, ServerFunctions } from '../types'
33
import { debounce } from 'perfect-debounce'
4+
import { watchStorageMount } from './storage-watch'
45

56
export function setupServerTasksRPC({ nuxt, refresh }: NuxtDevtoolsServerContext) {
67
let nitro: Nitro
8+
let unwatchStorage: (() => Promise<void> | void) | undefined
79

810
let cache: ScannedNitroTasks | null = null
911

@@ -18,13 +20,22 @@ export function setupServerTasksRPC({ nuxt, refresh }: NuxtDevtoolsServerContext
1820
refresh('getServerTasks')
1921
})
2022

21-
nuxt.hook('ready', () => {
22-
nitro?.storage.watch((event, key) => {
23+
nuxt.hook('ready', async () => {
24+
if (!nitro)
25+
return
26+
27+
await unwatchStorage?.()
28+
unwatchStorage = await watchStorageMount(nitro.storage, 'src', (_event, key) => {
2329
if (key.startsWith('src:tasks:'))
2430
refreshDebounced()
2531
})
2632
})
2733

34+
nuxt.hook('close', async () => {
35+
await unwatchStorage?.()
36+
unwatchStorage = undefined
37+
})
38+
2839
function scan() {
2940
if (cache)
3041
return cache
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { Storage } from 'unstorage'
2+
import { normalizeBaseKey, normalizeKey } from 'unstorage'
3+
4+
export type UnwatchStorageMount = () => Promise<void> | void
5+
type WatchEvent = 'update' | 'remove'
6+
type WatchCallback = (event: WatchEvent, key: string) => void
7+
8+
export async function watchStorageMount(storage: Storage, mountName: string, onChange: WatchCallback): Promise<UnwatchStorageMount> {
9+
const mountKey = normalizeBaseKey(mountName)
10+
const mount = storage.getMount(mountKey)
11+
if (!mount || normalizeBaseKey(mount.base) !== mountKey || !mount.driver?.watch)
12+
return () => {}
13+
14+
const unwatch = await mount.driver.watch((event: WatchEvent, key: string) => {
15+
const fullKey = key.startsWith(mountKey) ? key : `${mountKey}${key}`
16+
onChange(event, normalizeKey(fullKey))
17+
})
18+
return unwatch ?? (() => {})
19+
}

packages/devtools/src/server-rpc/storage.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { StorageMounts } from 'nitropack'
22
import type { Storage, StorageValue } from 'unstorage'
33
import type { NuxtDevtoolsServerContext, ServerFunctions } from '../types'
4+
import { watchStorageMount } from './storage-watch'
45

56
const IGNORE_STORAGE_MOUNTS = ['root', 'build', 'src', 'cache']
67
function shouldIgnoreStorageKey(key: string) {
@@ -15,32 +16,44 @@ export function setupStorageRPC({
1516
const storageMounts: StorageMounts = {}
1617

1718
let storage: Storage | undefined
19+
let unwatchStorageMounts: Array<() => Promise<void> | void> = []
1820

1921
nuxt.hook('nitro:init', (nitro) => {
2022
storage = nitro.storage
2123

22-
nuxt.hook('ready', () => {
23-
storage!.watch((event, key) => {
24-
if (shouldIgnoreStorageKey(key))
25-
return
26-
rpc.broadcast.callHook.asEvent('storage:key:update', key, event)
27-
})
28-
})
29-
3024
// Taken from https://github.com/unjs/nitro/blob/d83f2b65165d7ba996e7ef129ea99ff5b551dccc/src/storage.ts#L7-L10
3125
// Waiting for https://github.com/unjs/unstorage/issues/53
3226
const mounts = {
3327
...nitro.options.storage,
3428
...nitro.options.devStorage,
3529
}
3630

31+
for (const key of Object.keys(storageMounts))
32+
delete storageMounts[key]
33+
3734
for (const name of Object.keys(mounts)) {
3835
if (shouldIgnoreStorageKey(name))
3936
continue
4037
storageMounts[name] = mounts[name]!
4138
}
4239
})
4340

41+
nuxt.hook('ready', async () => {
42+
const activeStorage = storage
43+
if (!activeStorage)
44+
return
45+
await Promise.all(unwatchStorageMounts.map(unwatch => unwatch()))
46+
unwatchStorageMounts = await Promise.all(Object.keys(storageMounts).map(mountName =>
47+
watchStorageMount(activeStorage, mountName, (event, key) => {
48+
rpc.broadcast.callHook.asEvent('storage:key:update', key, event)
49+
})))
50+
})
51+
52+
nuxt.hook('close', async () => {
53+
await Promise.all(unwatchStorageMounts.map(unwatch => unwatch()))
54+
unwatchStorageMounts = []
55+
})
56+
4457
return {
4558
async getStorageMounts() {
4659
return storageMounts

0 commit comments

Comments
 (0)