@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 +2 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -1
- package/dist/watcher-daemon.d.ts +156 -0
- package/dist/watcher-daemon.js +498 -0
- package/dist/watcher-daemon.js.map +1 -0
- package/dist/watcher.d.ts +40 -0
- package/dist/watcher.js +233 -0
- package/dist/watcher.js.map +1 -1
- package/package.json +8 -8
- /package/dist/{preview-1764692648851.js → preview-1764708308174.js} +0 -0
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":"
|
|
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
|