-
Notifications
You must be signed in to change notification settings - Fork 4
/
Watch.js
125 lines (105 loc) · 4.26 KB
/
Watch.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import { dirname } from 'path'
import { watch } from 'fs'
import { throttle } from 'source/common/function'
import { createHub } from 'source/common/module/Event'
import { statAsync } from './function'
import { nearestExistPath } from './Path'
// single node only: a file, or one level of directory
// will throttle event
// listener should check the file for actual file change
// - can watch a non-exist file
// - watch will resume when deleted file is recreated
// TODO: upper directory renaming will not trigger change (still the same stat for this node & upper node)
// TODO: so renaming then replace the path will not trigger change
const CHANGE_PATH = 'rename' // const CHANGE_CONTENT = 'change'
const getNull = () => null
const createFileWatcher = ({
wait = 250,
// set true to keep process from exiting, like `timeout.ref()`
// once set, should call clear for process to unref and exit
persistent = false
}) => {
const hub = createHub()
let watcherPath // the nearest existing path, may be upper path
let watcherPathStat
let watcher
let watcherUpper
let targetPath
let prevTargetStat
const emitThrottled = throttle(async () => {
// path change: create/delete(also for rename since this watches single node)
// content change: path-change/file-content/directory-file-list
const targetStat = await statAsync(targetPath).catch(getNull)
const isPathChange = Boolean(prevTargetStat) !== Boolean(targetStat)
if (!targetStat && targetPath === watcherPath) await setupWatch() // renamed, not the target any more
__DEV__ && !isPathChange && !targetStat && console.log('emitThrottled dropped', isPathChange, Boolean(prevTargetStat), Boolean(targetStat))
if (!isPathChange && !targetStat) return
__DEV__ && console.log('emitThrottled send', isPathChange, Boolean(prevTargetStat), Boolean(targetStat))
const changeState = { targetPath, isPathChange, targetStat }
prevTargetStat = targetStat
hub.send(changeState)
}, wait)
const onErrorEvent = async (...args) => {
__DEV__ && console.log('[onErrorEvent]', args)
await setupWatch()
onChangeEvent(CHANGE_PATH)
}
const onChangeEvent = (changeType, path) => {
__DEV__ && console.log('[onChangeEvent]', changeType, path)
emitThrottled(changeType, path)
}
const clearWatch = () => {
watcher && watcher.close()
watcherUpper && watcherUpper.close()
watcherPath = null
watcherPathStat = null
watcher = null
watcherUpper = null
}
const setupWatch = async () => {
const targetStat = await statAsync(targetPath).catch(getNull)
if (targetStat) {
clearWatch()
__DEV__ && console.log('[Watch] targetPath visible', targetPath, targetStat.isDirectory())
watcherPath = targetPath
watcherPathStat = targetStat
watcher = watch(targetPath, { persistent, recursive: false })
watcher.addListener('error', onErrorEvent)
watcher.addListener('change', onChangeEvent)
// TODO: directly watch directory will miss rename, upper will receive content change, but upper will not receive add/delete change
if (targetStat.isDirectory()) {
watcherUpper = watch(dirname(targetPath))
watcherUpper.addListener('error', getNull) // not care this error
watcherUpper.addListener('change', onChangeEvent)
}
} else {
__DEV__ && console.log('[Watch] targetPath invisible')
const nearestPath = await nearestExistPath(targetPath)
if (nearestPath === watcherPath) return // same nearest path
clearWatch()
__DEV__ && console.log(`[Watch] change nearestPath: ${nearestPath}`)
watcherPath = nearestPath
watcherPathStat = targetStat
watcher = watch(nearestPath, { persistent, recursive: false })
watcher.addListener('error', onErrorEvent)
watcher.addListener('change', onErrorEvent)
}
}
return {
clear: () => {
clearWatch()
hub.clear()
prevTargetStat = null
},
setup: async (path) => {
clearWatch()
targetPath = path
__DEV__ && console.log('[setup]', path)
await setupWatch()
prevTargetStat = watcherPath === targetPath ? watcherPathStat : null
},
subscribe: (listener) => hub.subscribe(listener),
unsubscribe: (listener) => hub.unsubscribe(listener)
}
}
export { createFileWatcher }