@teambit/watcher 1.0.831 → 1.0.833

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -2,5 +2,7 @@ import { WatcherAspect } from './watcher.aspect';
2
2
  export type { WatchOptions } from './watcher';
3
3
  export { CheckTypes } from './check-types';
4
4
  export type { WatcherMain } from './watcher.main.runtime';
5
+ export { WatcherDaemon, WatcherClient, getOrCreateWatcherConnection } from './watcher-daemon';
6
+ export type { WatcherEvent, WatcherError, WatcherHeartbeat, WatcherReady, WatcherMessage } from './watcher-daemon';
5
7
  export default WatcherAspect;
6
8
  export { WatcherAspect };
package/dist/index.js CHANGED
@@ -15,7 +15,25 @@ Object.defineProperty(exports, "WatcherAspect", {
15
15
  return _watcher().WatcherAspect;
16
16
  }
17
17
  });
18
+ Object.defineProperty(exports, "WatcherClient", {
19
+ enumerable: true,
20
+ get: function () {
21
+ return _watcherDaemon().WatcherClient;
22
+ }
23
+ });
24
+ Object.defineProperty(exports, "WatcherDaemon", {
25
+ enumerable: true,
26
+ get: function () {
27
+ return _watcherDaemon().WatcherDaemon;
28
+ }
29
+ });
18
30
  exports.default = void 0;
31
+ Object.defineProperty(exports, "getOrCreateWatcherConnection", {
32
+ enumerable: true,
33
+ get: function () {
34
+ return _watcherDaemon().getOrCreateWatcherConnection;
35
+ }
36
+ });
19
37
  function _watcher() {
20
38
  const data = require("./watcher.aspect");
21
39
  _watcher = function () {
@@ -30,6 +48,13 @@ function _checkTypes() {
30
48
  };
31
49
  return data;
32
50
  }
51
+ function _watcherDaemon() {
52
+ const data = require("./watcher-daemon");
53
+ _watcherDaemon = function () {
54
+ return data;
55
+ };
56
+ return data;
57
+ }
33
58
  var _default = exports.default = _watcher().WatcherAspect;
34
59
 
35
60
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"names":["_watcher","data","require","_checkTypes","_default","exports","default","WatcherAspect"],"sources":["index.ts"],"sourcesContent":["import { WatcherAspect } from './watcher.aspect';\n\nexport type { WatchOptions } from './watcher';\nexport { CheckTypes } from './check-types';\nexport type { WatcherMain } from './watcher.main.runtime';\nexport default WatcherAspect;\nexport { WatcherAspect };\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA,SAAAA,SAAA;EAAA,MAAAC,IAAA,GAAAC,OAAA;EAAAF,QAAA,YAAAA,CAAA;IAAA,OAAAC,IAAA;EAAA;EAAA,OAAAA,IAAA;AAAA;AAGA,SAAAE,YAAA;EAAA,MAAAF,IAAA,GAAAC,OAAA;EAAAC,WAAA,YAAAA,CAAA;IAAA,OAAAF,IAAA;EAAA;EAAA,OAAAA,IAAA;AAAA;AAA2C,IAAAG,QAAA,GAAAC,OAAA,CAAAC,OAAA,GAE5BC,wBAAa","ignoreList":[]}
1
+ {"version":3,"names":["_watcher","data","require","_checkTypes","_watcherDaemon","_default","exports","default","WatcherAspect"],"sources":["index.ts"],"sourcesContent":["import { WatcherAspect } from './watcher.aspect';\n\nexport type { WatchOptions } from './watcher';\nexport { CheckTypes } from './check-types';\nexport type { WatcherMain } from './watcher.main.runtime';\nexport { WatcherDaemon, WatcherClient, getOrCreateWatcherConnection } from './watcher-daemon';\nexport type { WatcherEvent, WatcherError, WatcherHeartbeat, WatcherReady, WatcherMessage } from './watcher-daemon';\nexport default WatcherAspect;\nexport { WatcherAspect };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAAA,SAAA;EAAA,MAAAC,IAAA,GAAAC,OAAA;EAAAF,QAAA,YAAAA,CAAA;IAAA,OAAAC,IAAA;EAAA;EAAA,OAAAA,IAAA;AAAA;AAGA,SAAAE,YAAA;EAAA,MAAAF,IAAA,GAAAC,OAAA;EAAAC,WAAA,YAAAA,CAAA;IAAA,OAAAF,IAAA;EAAA;EAAA,OAAAA,IAAA;AAAA;AAEA,SAAAG,eAAA;EAAA,MAAAH,IAAA,GAAAC,OAAA;EAAAE,cAAA,YAAAA,CAAA;IAAA,OAAAH,IAAA;EAAA;EAAA,OAAAA,IAAA;AAAA;AAA8F,IAAAI,QAAA,GAAAC,OAAA,CAAAC,OAAA,GAE/EC,wBAAa","ignoreList":[]}
@@ -0,0 +1,156 @@
1
+ import type { Event } from '@parcel/watcher';
2
+ import type { Logger } from '@teambit/logger';
3
+ export type WatcherEvent = {
4
+ type: 'events';
5
+ events: Event[];
6
+ };
7
+ export type WatcherError = {
8
+ type: 'error';
9
+ message: string;
10
+ isDropError?: boolean;
11
+ };
12
+ export type WatcherHeartbeat = {
13
+ type: 'heartbeat';
14
+ timestamp: number;
15
+ };
16
+ export type WatcherReady = {
17
+ type: 'ready';
18
+ };
19
+ export type WatcherMessage = WatcherEvent | WatcherError | WatcherHeartbeat | WatcherReady;
20
+ /**
21
+ * WatcherDaemon is the server-side of the shared watcher infrastructure.
22
+ * It runs a Unix domain socket server and broadcasts file system events to all connected clients.
23
+ *
24
+ * Only ONE daemon can run per workspace. The daemon is responsible for:
25
+ * 1. Subscribing to Parcel Watcher for file system events
26
+ * 2. Broadcasting events to all connected clients
27
+ * 3. Sending heartbeats to clients so they know the daemon is alive
28
+ * 4. Cleaning up resources on shutdown
29
+ */
30
+ export declare class WatcherDaemon {
31
+ private scopePath;
32
+ private logger;
33
+ private server;
34
+ private clients;
35
+ private heartbeatInterval;
36
+ private isShuttingDown;
37
+ constructor(scopePath: string, logger: Logger);
38
+ get socketPath(): string;
39
+ get lockPath(): string;
40
+ /**
41
+ * Check if a daemon is already running for this workspace
42
+ */
43
+ static isRunning(scopePath: string, logger?: Logger): Promise<boolean>;
44
+ /**
45
+ * Try to connect to an existing daemon socket
46
+ */
47
+ private static tryConnect;
48
+ /**
49
+ * Start the daemon server
50
+ */
51
+ start(): Promise<void>;
52
+ /**
53
+ * Handle a new client connection
54
+ */
55
+ private handleConnection;
56
+ /**
57
+ * Broadcast events to all connected clients
58
+ */
59
+ broadcast(message: WatcherMessage): void;
60
+ /**
61
+ * Send message to a specific client
62
+ */
63
+ private sendToClient;
64
+ /**
65
+ * Broadcast file system events from Parcel watcher
66
+ */
67
+ broadcastEvents(events: Event[]): void;
68
+ /**
69
+ * Broadcast an error to all clients
70
+ */
71
+ broadcastError(message: string, isDropError?: boolean): void;
72
+ /**
73
+ * Start sending heartbeats to clients
74
+ */
75
+ private startHeartbeat;
76
+ /**
77
+ * Get the number of connected clients
78
+ */
79
+ get clientCount(): number;
80
+ /**
81
+ * Stop the daemon and cleanup
82
+ */
83
+ stop(): Promise<void>;
84
+ }
85
+ /**
86
+ * WatcherClient connects to an existing WatcherDaemon to receive file system events.
87
+ *
88
+ * Usage:
89
+ * ```typescript
90
+ * const client = new WatcherClient(scopePath, logger);
91
+ * await client.connect();
92
+ * client.onEvents((events) => { ... });
93
+ * client.onError((err) => { ... });
94
+ * ```
95
+ */
96
+ export declare class WatcherClient {
97
+ private scopePath;
98
+ private logger;
99
+ private socket;
100
+ private eventsHandler;
101
+ private errorHandler;
102
+ private readyHandler;
103
+ private disconnectHandler;
104
+ private buffer;
105
+ private isConnected;
106
+ constructor(scopePath: string, logger: Logger);
107
+ get socketPath(): string;
108
+ /**
109
+ * Connect to the daemon
110
+ */
111
+ connect(): Promise<void>;
112
+ /**
113
+ * Handle incoming data from the daemon
114
+ */
115
+ private handleData;
116
+ /**
117
+ * Handle a parsed message from the daemon
118
+ */
119
+ private handleMessage;
120
+ /**
121
+ * Register handler for file system events
122
+ */
123
+ onEvents(handler: (events: Event[]) => void): void;
124
+ /**
125
+ * Register handler for errors
126
+ */
127
+ onError(handler: (error: WatcherError) => void): void;
128
+ /**
129
+ * Register handler for ready signal
130
+ */
131
+ onReady(handler: () => void): void;
132
+ /**
133
+ * Register handler for disconnection
134
+ */
135
+ onDisconnect(handler: () => void): void;
136
+ /**
137
+ * Check if connected to daemon
138
+ */
139
+ get connected(): boolean;
140
+ /**
141
+ * Disconnect from the daemon
142
+ */
143
+ disconnect(): void;
144
+ }
145
+ /**
146
+ * Get or create a watcher connection for the given workspace.
147
+ * Returns either a daemon (if we're the first) or a client (if daemon exists).
148
+ *
149
+ * Uses retry with random jitter to handle race conditions when multiple processes
150
+ * try to become the daemon simultaneously.
151
+ */
152
+ export declare function getOrCreateWatcherConnection(scopePath: string, logger: Logger, retryCount?: number): Promise<{
153
+ isDaemon: boolean;
154
+ daemon?: WatcherDaemon;
155
+ client?: WatcherClient;
156
+ }>;
@@ -0,0 +1,498 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.WatcherDaemon = exports.WatcherClient = void 0;
7
+ exports.getOrCreateWatcherConnection = getOrCreateWatcherConnection;
8
+ function _net() {
9
+ const data = _interopRequireDefault(require("net"));
10
+ _net = function () {
11
+ return data;
12
+ };
13
+ return data;
14
+ }
15
+ function _fsExtra() {
16
+ const data = _interopRequireDefault(require("fs-extra"));
17
+ _fsExtra = function () {
18
+ return data;
19
+ };
20
+ return data;
21
+ }
22
+ function _path() {
23
+ const data = _interopRequireDefault(require("path"));
24
+ _path = function () {
25
+ return data;
26
+ };
27
+ return data;
28
+ }
29
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
30
+ function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
31
+ function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
32
+ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
33
+ const SOCKET_FILENAME = 'watcher.sock';
34
+ const LOCK_FILENAME = 'watcher.lock';
35
+ const HEARTBEAT_INTERVAL_MS = 5000;
36
+ const CONNECTION_TIMEOUT_MS = 3000;
37
+ /**
38
+ * WatcherDaemon is the server-side of the shared watcher infrastructure.
39
+ * It runs a Unix domain socket server and broadcasts file system events to all connected clients.
40
+ *
41
+ * Only ONE daemon can run per workspace. The daemon is responsible for:
42
+ * 1. Subscribing to Parcel Watcher for file system events
43
+ * 2. Broadcasting events to all connected clients
44
+ * 3. Sending heartbeats to clients so they know the daemon is alive
45
+ * 4. Cleaning up resources on shutdown
46
+ */
47
+ class WatcherDaemon {
48
+ constructor(scopePath, logger) {
49
+ this.scopePath = scopePath;
50
+ this.logger = logger;
51
+ _defineProperty(this, "server", null);
52
+ _defineProperty(this, "clients", new Set());
53
+ _defineProperty(this, "heartbeatInterval", null);
54
+ _defineProperty(this, "isShuttingDown", false);
55
+ }
56
+ get socketPath() {
57
+ return _path().default.join(this.scopePath, SOCKET_FILENAME);
58
+ }
59
+ get lockPath() {
60
+ return _path().default.join(this.scopePath, LOCK_FILENAME);
61
+ }
62
+
63
+ /**
64
+ * Check if a daemon is already running for this workspace
65
+ */
66
+ static async isRunning(scopePath, logger) {
67
+ const lockPath = _path().default.join(scopePath, LOCK_FILENAME);
68
+ const socketPath = _path().default.join(scopePath, SOCKET_FILENAME);
69
+
70
+ // Check if lock file exists
71
+ if (!(await _fsExtra().default.pathExists(lockPath))) {
72
+ return false;
73
+ }
74
+
75
+ // Check if the PID in lock file is still alive
76
+ try {
77
+ const lockContent = await _fsExtra().default.readFile(lockPath, 'utf8');
78
+ let pid;
79
+ try {
80
+ ({
81
+ pid
82
+ } = JSON.parse(lockContent));
83
+ } catch (parseErr) {
84
+ // Malformed JSON in lock file - treat as stale and clean up
85
+ logger?.debug(`Malformed lock file, cleaning up: ${parseErr.message}`);
86
+ await _fsExtra().default.remove(lockPath);
87
+ await _fsExtra().default.remove(socketPath);
88
+ return false;
89
+ }
90
+
91
+ // Check if process is running
92
+ try {
93
+ process.kill(pid, 0); // Signal 0 doesn't kill, just checks if process exists
94
+ } catch {
95
+ // Process doesn't exist, clean up stale lock
96
+ await _fsExtra().default.remove(lockPath);
97
+ await _fsExtra().default.remove(socketPath);
98
+ return false;
99
+ }
100
+
101
+ // Process exists, try to connect to verify it's actually the daemon
102
+ const canConnect = await WatcherDaemon.tryConnect(socketPath);
103
+ if (!canConnect) {
104
+ // Lock file exists but socket doesn't respond - stale lock
105
+ await _fsExtra().default.remove(lockPath);
106
+ await _fsExtra().default.remove(socketPath);
107
+ return false;
108
+ }
109
+ return true;
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Try to connect to an existing daemon socket
117
+ */
118
+ static tryConnect(socketPath) {
119
+ return new Promise(resolve => {
120
+ const socket = _net().default.createConnection(socketPath);
121
+ const timeout = setTimeout(() => {
122
+ socket.destroy();
123
+ resolve(false);
124
+ }, CONNECTION_TIMEOUT_MS);
125
+ socket.on('connect', () => {
126
+ clearTimeout(timeout);
127
+ socket.destroy();
128
+ resolve(true);
129
+ });
130
+ socket.on('error', () => {
131
+ clearTimeout(timeout);
132
+ socket.destroy();
133
+ resolve(false);
134
+ });
135
+ });
136
+ }
137
+
138
+ /**
139
+ * Start the daemon server
140
+ */
141
+ async start() {
142
+ // Remove any stale socket file
143
+ await _fsExtra().default.remove(this.socketPath);
144
+
145
+ // Create lock file with our PID
146
+ await _fsExtra().default.outputFile(this.lockPath, JSON.stringify({
147
+ pid: process.pid,
148
+ startTime: Date.now()
149
+ }));
150
+
151
+ // Create Unix domain socket server
152
+ this.server = _net().default.createServer(socket => this.handleConnection(socket));
153
+ return new Promise((resolve, reject) => {
154
+ if (!this.server) {
155
+ reject(new Error('Server not initialized'));
156
+ return;
157
+ }
158
+ this.server.on('error', err => {
159
+ this.logger.error(`Watcher daemon server error: ${err.message}`);
160
+ reject(err);
161
+ });
162
+ this.server.listen(this.socketPath, () => {
163
+ this.logger.debug(`Watcher daemon started on ${this.socketPath}`);
164
+ this.startHeartbeat();
165
+ resolve();
166
+ });
167
+ });
168
+ }
169
+
170
+ /**
171
+ * Handle a new client connection
172
+ */
173
+ handleConnection(socket) {
174
+ this.clients.add(socket);
175
+ this.logger.debug(`Watcher daemon: client connected (${this.clients.size} total)`);
176
+
177
+ // Send ready message to new client
178
+ this.sendToClient(socket, {
179
+ type: 'ready'
180
+ });
181
+ socket.on('close', () => {
182
+ this.clients.delete(socket);
183
+ this.logger.debug(`Watcher daemon: client disconnected (${this.clients.size} total)`);
184
+ });
185
+ socket.on('error', err => {
186
+ this.logger.debug(`Watcher daemon: client error - ${err.message}`);
187
+ this.clients.delete(socket);
188
+ });
189
+ }
190
+
191
+ /**
192
+ * Broadcast events to all connected clients
193
+ */
194
+ broadcast(message) {
195
+ const data = JSON.stringify(message) + '\n';
196
+ for (const client of this.clients) {
197
+ try {
198
+ client.write(data);
199
+ } catch (err) {
200
+ this.logger.debug(`Failed to send to client: ${err.message}`);
201
+ this.clients.delete(client);
202
+ }
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Send message to a specific client
208
+ */
209
+ sendToClient(socket, message) {
210
+ try {
211
+ socket.write(JSON.stringify(message) + '\n');
212
+ } catch (err) {
213
+ this.logger.debug(`Failed to send to client: ${err.message}`);
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Broadcast file system events from Parcel watcher
219
+ */
220
+ broadcastEvents(events) {
221
+ this.broadcast({
222
+ type: 'events',
223
+ events
224
+ });
225
+ }
226
+
227
+ /**
228
+ * Broadcast an error to all clients
229
+ */
230
+ broadcastError(message, isDropError = false) {
231
+ this.broadcast({
232
+ type: 'error',
233
+ message,
234
+ isDropError
235
+ });
236
+ }
237
+
238
+ /**
239
+ * Start sending heartbeats to clients
240
+ */
241
+ startHeartbeat() {
242
+ this.heartbeatInterval = setInterval(() => {
243
+ this.broadcast({
244
+ type: 'heartbeat',
245
+ timestamp: Date.now()
246
+ });
247
+ }, HEARTBEAT_INTERVAL_MS);
248
+ }
249
+
250
+ /**
251
+ * Get the number of connected clients
252
+ */
253
+ get clientCount() {
254
+ return this.clients.size;
255
+ }
256
+
257
+ /**
258
+ * Stop the daemon and cleanup
259
+ */
260
+ async stop() {
261
+ if (this.isShuttingDown) {
262
+ return;
263
+ }
264
+ this.isShuttingDown = true;
265
+ this.logger.debug('Watcher daemon stopping...');
266
+ if (this.heartbeatInterval) {
267
+ clearInterval(this.heartbeatInterval);
268
+ this.heartbeatInterval = null;
269
+ }
270
+
271
+ // Close all client connections
272
+ for (const client of this.clients) {
273
+ client.destroy();
274
+ }
275
+ this.clients.clear();
276
+
277
+ // Close server
278
+ if (this.server) {
279
+ await new Promise(resolve => {
280
+ this.server.close(() => resolve());
281
+ });
282
+ this.server = null;
283
+ }
284
+
285
+ // Remove lock and socket files
286
+ await _fsExtra().default.remove(this.lockPath);
287
+ await _fsExtra().default.remove(this.socketPath);
288
+ this.logger.debug('Watcher daemon stopped');
289
+ }
290
+ }
291
+
292
+ /**
293
+ * WatcherClient connects to an existing WatcherDaemon to receive file system events.
294
+ *
295
+ * Usage:
296
+ * ```typescript
297
+ * const client = new WatcherClient(scopePath, logger);
298
+ * await client.connect();
299
+ * client.onEvents((events) => { ... });
300
+ * client.onError((err) => { ... });
301
+ * ```
302
+ */
303
+ exports.WatcherDaemon = WatcherDaemon;
304
+ class WatcherClient {
305
+ constructor(scopePath, logger) {
306
+ this.scopePath = scopePath;
307
+ this.logger = logger;
308
+ _defineProperty(this, "socket", null);
309
+ _defineProperty(this, "eventsHandler", null);
310
+ _defineProperty(this, "errorHandler", null);
311
+ _defineProperty(this, "readyHandler", null);
312
+ _defineProperty(this, "disconnectHandler", null);
313
+ _defineProperty(this, "buffer", '');
314
+ _defineProperty(this, "isConnected", false);
315
+ }
316
+ get socketPath() {
317
+ return _path().default.join(this.scopePath, SOCKET_FILENAME);
318
+ }
319
+
320
+ /**
321
+ * Connect to the daemon
322
+ */
323
+ connect() {
324
+ return new Promise((resolve, reject) => {
325
+ this.socket = _net().default.createConnection(this.socketPath);
326
+ const timeout = setTimeout(() => {
327
+ this.socket?.destroy();
328
+ reject(new Error('Connection timeout'));
329
+ }, CONNECTION_TIMEOUT_MS);
330
+ this.socket.on('connect', () => {
331
+ clearTimeout(timeout);
332
+ this.isConnected = true;
333
+ this.logger.debug('Watcher client connected to daemon');
334
+ resolve();
335
+ });
336
+ this.socket.on('data', data => {
337
+ this.handleData(data);
338
+ });
339
+ this.socket.on('close', () => {
340
+ this.isConnected = false;
341
+ this.logger.debug('Watcher client disconnected from daemon');
342
+ this.disconnectHandler?.();
343
+ });
344
+ this.socket.on('error', err => {
345
+ clearTimeout(timeout);
346
+ this.socket?.destroy();
347
+ this.isConnected = false;
348
+ this.logger.debug(`Watcher client error: ${err.message}`);
349
+ reject(err);
350
+ });
351
+ });
352
+ }
353
+
354
+ /**
355
+ * Handle incoming data from the daemon
356
+ */
357
+ handleData(data) {
358
+ this.buffer += data.toString('utf8');
359
+
360
+ // Process complete messages (newline-delimited JSON)
361
+ const lines = this.buffer.split('\n');
362
+ this.buffer = lines.pop() || ''; // Keep incomplete line in buffer
363
+
364
+ for (const line of lines) {
365
+ if (!line.trim()) continue;
366
+ try {
367
+ const message = JSON.parse(line);
368
+ this.handleMessage(message);
369
+ } catch (err) {
370
+ this.logger.debug(`Failed to parse message: ${err.message}`);
371
+ }
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Handle a parsed message from the daemon
377
+ */
378
+ handleMessage(message) {
379
+ switch (message.type) {
380
+ case 'events':
381
+ this.eventsHandler?.(message.events);
382
+ break;
383
+ case 'error':
384
+ this.errorHandler?.(message);
385
+ break;
386
+ case 'ready':
387
+ this.readyHandler?.();
388
+ break;
389
+ case 'heartbeat':
390
+ // Heartbeats serve to keep the TCP connection alive and allow the OS to detect
391
+ // a dead daemon faster. The client doesn't need to actively monitor them since
392
+ // the socket 'close' event will fire when the daemon dies.
393
+ break;
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Register handler for file system events
399
+ */
400
+ onEvents(handler) {
401
+ this.eventsHandler = handler;
402
+ }
403
+
404
+ /**
405
+ * Register handler for errors
406
+ */
407
+ onError(handler) {
408
+ this.errorHandler = handler;
409
+ }
410
+
411
+ /**
412
+ * Register handler for ready signal
413
+ */
414
+ onReady(handler) {
415
+ this.readyHandler = handler;
416
+ }
417
+
418
+ /**
419
+ * Register handler for disconnection
420
+ */
421
+ onDisconnect(handler) {
422
+ this.disconnectHandler = handler;
423
+ }
424
+
425
+ /**
426
+ * Check if connected to daemon
427
+ */
428
+ get connected() {
429
+ return this.isConnected;
430
+ }
431
+
432
+ /**
433
+ * Disconnect from the daemon
434
+ */
435
+ disconnect() {
436
+ if (this.socket) {
437
+ this.socket.destroy();
438
+ this.socket = null;
439
+ }
440
+ this.isConnected = false;
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Get or create a watcher connection for the given workspace.
446
+ * Returns either a daemon (if we're the first) or a client (if daemon exists).
447
+ *
448
+ * Uses retry with random jitter to handle race conditions when multiple processes
449
+ * try to become the daemon simultaneously.
450
+ */
451
+ exports.WatcherClient = WatcherClient;
452
+ async function getOrCreateWatcherConnection(scopePath, logger, retryCount = 0) {
453
+ const MAX_RETRIES = 3;
454
+
455
+ // Check if daemon is already running
456
+ const isRunning = await WatcherDaemon.isRunning(scopePath, logger);
457
+ if (isRunning) {
458
+ // Connect as client
459
+ const client = new WatcherClient(scopePath, logger);
460
+ try {
461
+ await client.connect();
462
+ return {
463
+ isDaemon: false,
464
+ client
465
+ };
466
+ } catch (err) {
467
+ // Connection failed - daemon might have just died, retry
468
+ if (retryCount < MAX_RETRIES) {
469
+ // Add random jitter (100-500ms) to reduce thundering herd
470
+ const jitter = 100 + Math.random() * 400;
471
+ await new Promise(resolve => setTimeout(resolve, jitter));
472
+ return getOrCreateWatcherConnection(scopePath, logger, retryCount + 1);
473
+ }
474
+ throw err;
475
+ }
476
+ }
477
+
478
+ // Try to become the daemon
479
+ const daemon = new WatcherDaemon(scopePath, logger);
480
+ try {
481
+ await daemon.start();
482
+ return {
483
+ isDaemon: true,
484
+ daemon
485
+ };
486
+ } catch (err) {
487
+ // Failed to start daemon - another process might have beaten us, retry as client
488
+ if (retryCount < MAX_RETRIES) {
489
+ // Add random jitter to reduce thundering herd
490
+ const jitter = 100 + Math.random() * 400;
491
+ await new Promise(resolve => setTimeout(resolve, jitter));
492
+ return getOrCreateWatcherConnection(scopePath, logger, retryCount + 1);
493
+ }
494
+ throw err;
495
+ }
496
+ }
497
+
498
+ //# sourceMappingURL=watcher-daemon.js.map