3.5 KB115 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import type {Logger} from './logger';
9
10import path from 'node:path';
11// Import from the generated files
12import {
13 EdenFSNotificationsClient,
14 type EdenFSSubscription,
15 type SubscriptionCallback,
16 type SubscriptionOptions,
17} from './__generated__/node-edenfs-notifications-client/index.js';
18
19export type EdenFSSubscriptionManager = {
20 path: string;
21 name: string;
22 subscriptionCount: number;
23 internalSubscription: EdenFSSubscription;
24};
25
26/**
27 * Provides directory watching functionality using EdenFS notify API.
28 */
29export class EdenFSNotifications {
30 private client: EdenFSNotificationsClient;
31 private subscriptions: Map<string, EdenFSSubscriptionManager> = new Map();
32
33 constructor(
34 private logger: Logger,
35 private mountPoint: string,
36 ) {
37 this.client = new EdenFSNotificationsClient({
38 mountPoint: this.mountPoint,
39 timeout: 30000,
40 edenBinaryPath: process.env.EDEN_PATH ? process.env.EDEN_PATH : 'eden',
41 });
42 }
43
44 /**
45 * Watch a directory recursively for changes using EdenFS notifications.
46 */
47 public async watchDirectoryRecursive(
48 localDirectoryPath: string,
49 rawSubscriptionName: string,
50 subscriptionOptions: SubscriptionOptions,
51 callback: SubscriptionCallback,
52 ): Promise<EdenFSSubscription> {
53 // Subscriptions should be unique by name and by folder
54 const subscriptionName = this.fixupName(localDirectoryPath, rawSubscriptionName);
55 const existingSubscription = this.getSubscription(subscriptionName);
56
57 if (existingSubscription) {
58 existingSubscription.subscriptionCount++;
59 return existingSubscription.internalSubscription;
60 }
61
62 const internalSubscription = this.client.subscribe(subscriptionOptions, callback);
63
64 const subscription: EdenFSSubscriptionManager = {
65 path: localDirectoryPath,
66 name: subscriptionName,
67 subscriptionCount: 1,
68 internalSubscription,
69 };
70
71 this.setSubscription(subscriptionName, subscription);
72 await internalSubscription.start();
73 this.logger.log(`edenfs subscription started: ${subscriptionName}`);
74 return internalSubscription;
75 }
76
77 private getSubscription(entryPath: string): EdenFSSubscriptionManager | undefined {
78 return this.subscriptions.get(path.normalize(entryPath));
79 }
80
81 private setSubscription(entryPath: string, subscription: EdenFSSubscriptionManager): void {
82 const key = path.normalize(entryPath);
83 this.subscriptions.set(key, subscription);
84 }
85
86 private deleteSubscription(entryPath: string): void {
87 const key = path.normalize(entryPath);
88 this.subscriptions.delete(key);
89 }
90
91 private fixupName(path: string, name: string): string {
92 const refinedPath = path.replace(/\\/g, '-').replace(/\//g, '-');
93 return `${refinedPath}-${name}`;
94 }
95
96 /**
97 * Remove a subscription (unwatch)
98 */
99 public unwatch(path: string, name: string) {
100 const subscriptionName = this.fixupName(path, name);
101 const subscription = this.getSubscription(subscriptionName);
102
103 if (subscription == null) {
104 this.logger.error(`No watcher entity found with path [${path}] name [${name}]`);
105 return;
106 }
107
108 if (--subscription.subscriptionCount === 0) {
109 subscription.internalSubscription.stop();
110 this.deleteSubscription(subscriptionName);
111 this.logger.log(`edenfs subscription destroyed: ${subscriptionName}`);
112 }
113 }
114}
115