forked from atom/atom
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathnative-watcher-registry.js
442 lines (387 loc) · 17.3 KB
/
native-watcher-registry.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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
const path = require('path')
// Private: re-join the segments split from an absolute path to form another absolute path.
function absolute (...parts) {
const candidate = path.join(...parts)
return path.isAbsolute(candidate) ? candidate : path.join(path.sep, candidate)
}
// Private: Map userland filesystem watcher subscriptions efficiently to deliver filesystem change notifications to
// each watcher with the most efficient coverage of native watchers.
//
// * If two watchers subscribe to the same directory, use a single native watcher for each.
// * Re-use a native watcher watching a parent directory for a watcher on a child directory. If the parent directory
// watcher is removed, it will be split into child watchers.
// * If any child directories already being watched, stop and replace them with a watcher on the parent directory.
//
// Uses a trie whose structure mirrors the directory structure.
class RegistryTree {
// Private: Construct a tree with no native watchers.
//
// * `basePathSegments` the position of this tree's root relative to the filesystem's root as an {Array} of directory
// names.
// * `createNative` {Function} used to construct new native watchers. It should accept an absolute path as an argument
// and return a new {NativeWatcher}.
constructor (basePathSegments, createNative) {
this.basePathSegments = basePathSegments
this.root = new RegistryNode()
this.createNative = createNative
}
// Private: Identify the native watcher that should be used to produce events at a watched path, creating a new one
// if necessary.
//
// * `pathSegments` the path to watch represented as an {Array} of directory names relative to this {RegistryTree}'s
// root.
// * `attachToNative` {Function} invoked with the appropriate native watcher and the absolute path to its watch root.
add (pathSegments, attachToNative) {
const absolutePathSegments = this.basePathSegments.concat(pathSegments)
const absolutePath = absolute(...absolutePathSegments)
const attachToNew = (childPaths) => {
const native = this.createNative(absolutePath)
const leaf = new RegistryWatcherNode(native, absolutePathSegments, childPaths)
this.root = this.root.insert(pathSegments, leaf)
const sub = native.onWillStop(() => {
sub.dispose()
this.root = this.root.remove(pathSegments, this.createNative) || new RegistryNode()
})
attachToNative(native, absolutePath)
return native
}
this.root.lookup(pathSegments).when({
parent: (parent, remaining) => {
// An existing NativeWatcher is watching the same directory or a parent directory of the requested path.
// Attach this Watcher to it as a filtering watcher and record it as a dependent child path.
const native = parent.getNativeWatcher()
parent.addChildPath(remaining)
attachToNative(native, absolute(...parent.getAbsolutePathSegments()))
},
children: children => {
// One or more NativeWatchers exist on child directories of the requested path. Create a new native watcher
// on the parent directory, note the subscribed child paths, and cleanly stop the child native watchers.
const newNative = attachToNew(children.map(child => child.path))
for (let i = 0; i < children.length; i++) {
const childNode = children[i].node
const childNative = childNode.getNativeWatcher()
childNative.reattachTo(newNative, absolutePath)
childNative.dispose()
childNative.stop()
}
},
missing: () => attachToNew([])
})
}
// Private: Access the root node of the tree.
getRoot () {
return this.root
}
// Private: Return a {String} representation of this tree's structure for diagnostics and testing.
print () {
return this.root.print()
}
}
// Private: Non-leaf node in a {RegistryTree} used by the {NativeWatcherRegistry} to cover the allocated {Watcher}
// instances with the most efficient set of {NativeWatcher} instances possible. Each {RegistryNode} maps to a directory
// in the filesystem tree.
class RegistryNode {
// Private: Construct a new, empty node representing a node with no watchers.
constructor () {
this.children = {}
}
// Private: Recursively discover any existing watchers corresponding to a path.
//
// * `pathSegments` filesystem path of a new {Watcher} already split into an Array of directory names.
//
// Returns: A {ParentResult} if the exact requested directory or a parent directory is being watched, a
// {ChildrenResult} if one or more child paths are being watched, or a {MissingResult} if no relevant watchers
// exist.
lookup (pathSegments) {
if (pathSegments.length === 0) {
return new ChildrenResult(this.leaves([]))
}
const child = this.children[pathSegments[0]]
if (child === undefined) {
return new MissingResult(this)
}
return child.lookup(pathSegments.slice(1))
}
// Private: Insert a new {RegistryWatcherNode} into the tree, creating new intermediate {RegistryNode} instances as
// needed. Any existing children of the watched directory are removed.
//
// * `pathSegments` filesystem path of the new {Watcher}, already split into an Array of directory names.
// * `leaf` initialized {RegistryWatcherNode} to insert
//
// Returns: The root of a new tree with the {RegistryWatcherNode} inserted at the correct location. Callers should
// replace their node references with the returned value.
insert (pathSegments, leaf) {
if (pathSegments.length === 0) {
return leaf
}
const pathKey = pathSegments[0]
let child = this.children[pathKey]
if (child === undefined) {
child = new RegistryNode()
}
this.children[pathKey] = child.insert(pathSegments.slice(1), leaf)
return this
}
// Private: Remove a {RegistryWatcherNode} by its exact watched directory.
//
// * `pathSegments` absolute pre-split filesystem path of the node to remove.
// * `createSplitNative` callback to be invoked with each child path segment {Array} if the {RegistryWatcherNode}
// is split into child watchers rather than removed outright. See {RegistryWatcherNode.remove}.
//
// Returns: The root of a new tree with the {RegistryWatcherNode} removed. Callers should replace their node
// references with the returned value.
remove (pathSegments, createSplitNative) {
if (pathSegments.length === 0) {
// Attempt to remove a path with child watchers. Do nothing.
return this
}
const pathKey = pathSegments[0]
const child = this.children[pathKey]
if (child === undefined) {
// Attempt to remove a path that isn't watched. Do nothing.
return this
}
// Recurse
const newChild = child.remove(pathSegments.slice(1), createSplitNative)
if (newChild === null) {
delete this.children[pathKey]
} else {
this.children[pathKey] = newChild
}
// Remove this node if all of its children have been removed
return Object.keys(this.children).length === 0 ? null : this
}
// Private: Discover all {RegistryWatcherNode} instances beneath this tree node and the child paths
// that they are watching.
//
// * `prefix` {Array} of intermediate path segments to prepend to the resulting child paths.
//
// Returns: A possibly empty {Array} of `{node, path}` objects describing {RegistryWatcherNode}
// instances beneath this node.
leaves (prefix) {
const results = []
for (const p of Object.keys(this.children)) {
results.push(...this.children[p].leaves(prefix.concat([p])))
}
return results
}
// Private: Return a {String} representation of this subtree for diagnostics and testing.
print (indent = 0) {
let spaces = ''
for (let i = 0; i < indent; i++) {
spaces += ' '
}
let result = ''
for (const p of Object.keys(this.children)) {
result += `${spaces}${p}\n${this.children[p].print(indent + 2)}`
}
return result
}
}
// Private: Leaf node within a {NativeWatcherRegistry} tree. Represents a directory that is covered by a
// {NativeWatcher}.
class RegistryWatcherNode {
// Private: Allocate a new node to track a {NativeWatcher}.
//
// * `nativeWatcher` An existing {NativeWatcher} instance.
// * `absolutePathSegments` The absolute path to this {NativeWatcher}'s directory as an {Array} of
// path segments.
// * `childPaths` {Array} of child directories that are currently the responsibility of this
// {NativeWatcher}, if any. Directories are represented as arrays of the path segments between this
// node's directory and the watched child path.
constructor (nativeWatcher, absolutePathSegments, childPaths) {
this.nativeWatcher = nativeWatcher
this.absolutePathSegments = absolutePathSegments
// Store child paths as joined strings so they work as Set members.
this.childPaths = new Set()
for (let i = 0; i < childPaths.length; i++) {
this.childPaths.add(path.join(...childPaths[i]))
}
}
// Private: Assume responsibility for a new child path. If this node is removed, it will instead
// split into a subtree with a new {RegistryWatcherNode} for each child path.
//
// * `childPathSegments` the {Array} of path segments between this node's directory and the watched
// child directory.
addChildPath (childPathSegments) {
this.childPaths.add(path.join(...childPathSegments))
}
// Private: Stop assuming responsibility for a previously assigned child path. If this node is
// removed, the named child path will no longer be allocated a {RegistryWatcherNode}.
//
// * `childPathSegments` the {Array} of path segments between this node's directory and the no longer
// watched child directory.
removeChildPath (childPathSegments) {
this.childPaths.delete(path.join(...childPathSegments))
}
// Private: Accessor for the {NativeWatcher}.
getNativeWatcher () {
return this.nativeWatcher
}
// Private: Return the absolute path watched by this {NativeWatcher} as an {Array} of directory names.
getAbsolutePathSegments () {
return this.absolutePathSegments
}
// Private: Identify how this watcher relates to a request to watch a directory tree.
//
// * `pathSegments` filesystem path of a new {Watcher} already split into an Array of directory names.
//
// Returns: A {ParentResult} referencing this node.
lookup (pathSegments) {
return new ParentResult(this, pathSegments)
}
// Private: Remove this leaf node if the watcher's exact path matches. If this node is covering additional
// {Watcher} instances on child paths, it will be split into a subtree.
//
// * `pathSegments` filesystem path of the node to remove.
// * `createSplitNative` callback invoked with each {Array} of absolute child path segments to create a native
// watcher on a subtree of this node.
//
// Returns: If `pathSegments` match this watcher's path exactly, returns `null` if this node has no `childPaths`
// or a new {RegistryNode} on a newly allocated subtree if it did. If `pathSegments` does not match the watcher's
// path, it's an attempt to remove a subnode that doesn't exist, so the remove call has no effect and returns
// `this` unaltered.
remove (pathSegments, createSplitNative) {
if (pathSegments.length !== 0) {
return this
} else if (this.childPaths.size > 0) {
let newSubTree = new RegistryTree(this.absolutePathSegments, createSplitNative)
for (const childPath of this.childPaths) {
const childPathSegments = childPath.split(path.sep)
newSubTree.add(childPathSegments, (native, attachmentPath) => {
this.nativeWatcher.reattachTo(native, attachmentPath)
})
}
return newSubTree.getRoot()
} else {
return null
}
}
// Private: Discover this {RegistryWatcherNode} instance.
//
// * `prefix` {Array} of intermediate path segments to prepend to the resulting child paths.
//
// Returns: An {Array} containing a `{node, path}` object describing this node.
leaves (prefix) {
return [{node: this, path: prefix}]
}
// Private: Return a {String} representation of this watcher for diagnostics and testing. Indicates the number of
// child paths that this node's {NativeWatcher} is responsible for.
print (indent = 0) {
let result = ''
for (let i = 0; i < indent; i++) {
result += ' '
}
result += '[watcher'
if (this.childPaths.size > 0) {
result += ` +${this.childPaths.size}`
}
result += ']\n'
return result
}
}
// Private: A {RegistryNode} traversal result that's returned when neither a directory, its children, nor its parents
// are present in the tree.
class MissingResult {
// Private: Instantiate a new {MissingResult}.
//
// * `lastParent` the final successfully traversed {RegistryNode}.
constructor (lastParent) {
this.lastParent = lastParent
}
// Private: Dispatch within a map of callback actions.
//
// * `actions` {Object} containing a `missing` key that maps to a callback to be invoked when no results were returned
// by {RegistryNode.lookup}. The callback will be called with the last parent node that was encountered during the
// traversal.
//
// Returns: the result of the `actions` callback.
when (actions) {
return actions.missing(this.lastParent)
}
}
// Private: A {RegistryNode.lookup} traversal result that's returned when a parent or an exact match of the requested
// directory is being watched by an existing {RegistryWatcherNode}.
class ParentResult {
// Private: Instantiate a new {ParentResult}.
//
// * `parent` the {RegistryWatcherNode} that was discovered.
// * `remainingPathSegments` an {Array} of the directories that lie between the leaf node's watched directory and
// the requested directory. This will be empty for exact matches.
constructor (parent, remainingPathSegments) {
this.parent = parent
this.remainingPathSegments = remainingPathSegments
}
// Private: Dispatch within a map of callback actions.
//
// * `actions` {Object} containing a `parent` key that maps to a callback to be invoked when a parent of a requested
// requested directory is returned by a {RegistryNode.lookup} call. The callback will be called with the
// {RegistryWatcherNode} instance and an {Array} of the {String} path segments that separate the parent node
// and the requested directory.
//
// Returns: the result of the `actions` callback.
when (actions) {
return actions.parent(this.parent, this.remainingPathSegments)
}
}
// Private: A {RegistryNode.lookup} traversal result that's returned when one or more children of the requested
// directory are already being watched.
class ChildrenResult {
// Private: Instantiate a new {ChildrenResult}.
//
// * `children` {Array} of the {RegistryWatcherNode} instances that were discovered.
constructor (children) {
this.children = children
}
// Private: Dispatch within a map of callback actions.
//
// * `actions` {Object} containing a `children` key that maps to a callback to be invoked when a parent of a requested
// requested directory is returned by a {RegistryNode.lookup} call. The callback will be called with the
// {RegistryWatcherNode} instance.
//
// Returns: the result of the `actions` callback.
when (actions) {
return actions.children(this.children)
}
}
// Private: Track the directories being monitored by native filesystem watchers. Minimize the number of native watchers
// allocated to receive events for a desired set of directories by:
//
// 1. Subscribing to the same underlying {NativeWatcher} when watching the same directory multiple times.
// 2. Subscribing to an existing {NativeWatcher} on a parent of a desired directory.
// 3. Replacing multiple {NativeWatcher} instances on child directories with a single new {NativeWatcher} on the
// parent.
class NativeWatcherRegistry {
// Private: Instantiate an empty registry.
//
// * `createNative` {Function} that will be called with a normalized filesystem path to create a new native
// filesystem watcher.
constructor (createNative) {
this.tree = new RegistryTree([], createNative)
}
// Private: Attach a watcher to a directory, assigning it a {NativeWatcher}. If a suitable {NativeWatcher} already
// exists, it will be attached to the new {Watcher} with an appropriate subpath configuration. Otherwise, the
// `createWatcher` callback will be invoked to create a new {NativeWatcher}, which will be registered in the tree
// and attached to the watcher.
//
// If any pre-existing child watchers are removed as a result of this operation, {NativeWatcher.onWillReattach} will
// be broadcast on each with the new parent watcher as an event payload to give child watchers a chance to attach to
// the new watcher.
//
// * `watcher` an unattached {Watcher}.
async attach (watcher) {
const normalizedDirectory = await watcher.getNormalizedPathPromise()
const pathSegments = normalizedDirectory.split(path.sep).filter(segment => segment.length > 0)
this.tree.add(pathSegments, (native, nativePath) => {
watcher.attachToNative(native, nativePath)
})
}
// Private: Generate a visual representation of the currently active watchers managed by this
// registry.
//
// Returns a {String} showing the tree structure.
print () {
return this.tree.print()
}
}
module.exports = {NativeWatcherRegistry}