@ms-cloudpack/file-watcher 0.4.19 → 0.4.20

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.
Files changed (39) hide show
  1. package/README.md +34 -0
  2. package/lib/DebouncedWatcher.d.ts +50 -0
  3. package/lib/DebouncedWatcher.d.ts.map +1 -0
  4. package/lib/DebouncedWatcher.js +108 -0
  5. package/lib/DebouncedWatcher.js.map +1 -0
  6. package/lib/ForkWatcher.d.ts +5 -1
  7. package/lib/ForkWatcher.d.ts.map +1 -1
  8. package/lib/ForkWatcher.js +7 -2
  9. package/lib/ForkWatcher.js.map +1 -1
  10. package/lib/MockWatcher.d.ts +26 -0
  11. package/lib/MockWatcher.d.ts.map +1 -0
  12. package/lib/MockWatcher.js +53 -0
  13. package/lib/MockWatcher.js.map +1 -0
  14. package/lib/SharedRootWatcher.d.ts +32 -0
  15. package/lib/SharedRootWatcher.d.ts.map +1 -0
  16. package/lib/SharedRootWatcher.js +166 -0
  17. package/lib/SharedRootWatcher.js.map +1 -0
  18. package/lib/applyPolicies.d.ts +15 -0
  19. package/lib/applyPolicies.d.ts.map +1 -0
  20. package/lib/applyPolicies.js +29 -0
  21. package/lib/applyPolicies.js.map +1 -0
  22. package/lib/createWatcher.d.ts +35 -2
  23. package/lib/createWatcher.d.ts.map +1 -1
  24. package/lib/createWatcher.js +31 -10
  25. package/lib/createWatcher.js.map +1 -1
  26. package/lib/createWatcherInternal.d.ts +3 -0
  27. package/lib/createWatcherInternal.d.ts.map +1 -0
  28. package/lib/createWatcherInternal.js +6 -0
  29. package/lib/createWatcherInternal.js.map +1 -0
  30. package/lib/index.d.ts +2 -1
  31. package/lib/index.d.ts.map +1 -1
  32. package/lib/index.js.map +1 -1
  33. package/lib/types/WatcherPolicy.d.ts +23 -0
  34. package/lib/types/WatcherPolicy.d.ts.map +1 -0
  35. package/lib/types/WatcherPolicy.js +2 -0
  36. package/lib/types/WatcherPolicy.js.map +1 -0
  37. package/lib/watcherForkWrapper.js +19 -4
  38. package/lib/watcherForkWrapper.js.map +1 -1
  39. package/package.json +7 -4
package/README.md CHANGED
@@ -29,6 +29,40 @@ await watcher.dispose();
29
29
  ### `createWatcher` options
30
30
 
31
31
  - `type` (`'default' | 'fork'`, optional): By default, this will create a watcher in the same thread. Use `type: 'fork'` to create the watcher in a forked process.
32
+ - `backend` (`'default' | 'parcel'`, optional): Choose the file watching backend.
33
+ - `'default'`: Uses chokidar (default)
34
+ - `'parcel'`: Uses @parcel/watcher for better performance
35
+ - `policies` (optional): Enable performance optimizations
36
+ - `'shared-root'`: Multiplexes N package watches into 1 root watch (for monorepos with 100+ packages)
37
+ - `'debounce'`: Batches rapid file changes into single notifications
38
+
39
+ ### Shared Root Watcher for Large Monorepos
40
+
41
+ For optimal performance when watching many packages (100+), use `SharedRootWatcher` - an orchestrator that wraps any watcher:
42
+
43
+ ```ts
44
+ import { SharedRootWatcher, createWatcher } from '@ms-cloudpack/file-watcher';
45
+
46
+ // Create underlying watcher (can be any backend)
47
+ const underlyingWatcher = createWatcher({ backend: 'default' });
48
+
49
+ // Wrap it with SharedRootWatcher orchestrator
50
+ const watcher = new SharedRootWatcher(underlyingWatcher, '/path/to/monorepo');
51
+
52
+ // Watch multiple packages efficiently (single OS watcher)
53
+ await watcher.watch({ path: '/path/to/monorepo/packages/pkg1' }, onPkg1Change);
54
+ await watcher.watch({ path: '/path/to/monorepo/packages/pkg2' }, onPkg2Change);
55
+ // ... 1000+ packages
56
+ ```
57
+
58
+ This uses a single file system watcher at the root instead of creating one per package.
59
+
60
+ **With Parcel backend for maximum performance:**
61
+
62
+ ```ts
63
+ const underlyingWatcher = createWatcher({ backend: 'parcel' });
64
+ const watcher = new SharedRootWatcher(underlyingWatcher, '/path/to/monorepo');
65
+ ```
32
66
 
33
67
  ### `watcher.watch` options
34
68
 
@@ -0,0 +1,50 @@
1
+ import type { UnwatchCallback, WatchChangeCallback, Watcher, WatchOptions } from './types/Watcher.js';
2
+ /**
3
+ * Decorator that adds debouncing/batching to any Watcher implementation.
4
+ *
5
+ * Uses a hybrid flush strategy:
6
+ * - First change: immediate flush (0ms latency)
7
+ * - Subsequent changes within debounce window: batched together
8
+ * - After debounce window expires: returns to immediate flush mode
9
+ *
10
+ * This provides zero-latency for single file saves while efficiently batching
11
+ * bulk operations (git checkout, npm install, etc.).
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * // Debounce any watcher
16
+ * const watcher = new DebouncedWatcher(new DefaultWatcher(), { debounceMs: 10 });
17
+ *
18
+ * // Compose with SharedRootWatcher
19
+ * const shared = new SharedRootWatcher(new DefaultWatcher(), '/monorepo');
20
+ * const debounced = new DebouncedWatcher(shared, { debounceMs: 10 });
21
+ * ```
22
+ */
23
+ export declare class DebouncedWatcher implements Watcher {
24
+ private _watcher;
25
+ private _debounceMs;
26
+ private _subscribers;
27
+ /**
28
+ * @param watcher - The watcher to wrap with debouncing
29
+ * @param options - Configuration options
30
+ * @param options.debounceMs - Debounce delay in milliseconds (default: 10ms)
31
+ */
32
+ constructor(watcher: Watcher, options?: {
33
+ debounceMs?: number;
34
+ });
35
+ watch(options: WatchOptions, onChange: WatchChangeCallback): Promise<UnwatchCallback>;
36
+ unwatch(id: string): Promise<void>;
37
+ dispose(): Promise<void>;
38
+ /**
39
+ * Add changed paths to pending notifications for a subscriber.
40
+ * Implements hybrid flush strategy:
41
+ * - Idle state (no timer): flush immediately, start timer
42
+ * - Debouncing state (timer active): accumulate, reset timer
43
+ */
44
+ private _addPending;
45
+ /**
46
+ * Flush pending notifications for a subscriber and clear its state.
47
+ */
48
+ private _flushSubscriber;
49
+ }
50
+ //# sourceMappingURL=DebouncedWatcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DebouncedWatcher.d.ts","sourceRoot":"","sources":["../src/DebouncedWatcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,mBAAmB,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAWtG;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,gBAAiB,YAAW,OAAO;IAC9C,OAAO,CAAC,QAAQ,CAAU;IAC1B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAAsC;IAE1D;;;;OAIG;gBACS,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE;IAKzD,KAAK,CAAC,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,mBAAmB,GAAG,OAAO,CAAC,eAAe,CAAC;IAmBrF,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMlC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAS9B;;;;;OAKG;IACH,OAAO,CAAC,WAAW;IA2BnB;;OAEG;IACH,OAAO,CAAC,gBAAgB;CAYzB"}
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Decorator that adds debouncing/batching to any Watcher implementation.
3
+ *
4
+ * Uses a hybrid flush strategy:
5
+ * - First change: immediate flush (0ms latency)
6
+ * - Subsequent changes within debounce window: batched together
7
+ * - After debounce window expires: returns to immediate flush mode
8
+ *
9
+ * This provides zero-latency for single file saves while efficiently batching
10
+ * bulk operations (git checkout, npm install, etc.).
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * // Debounce any watcher
15
+ * const watcher = new DebouncedWatcher(new DefaultWatcher(), { debounceMs: 10 });
16
+ *
17
+ * // Compose with SharedRootWatcher
18
+ * const shared = new SharedRootWatcher(new DefaultWatcher(), '/monorepo');
19
+ * const debounced = new DebouncedWatcher(shared, { debounceMs: 10 });
20
+ * ```
21
+ */
22
+ export class DebouncedWatcher {
23
+ _watcher;
24
+ _debounceMs;
25
+ _subscribers = new Map();
26
+ /**
27
+ * @param watcher - The watcher to wrap with debouncing
28
+ * @param options - Configuration options
29
+ * @param options.debounceMs - Debounce delay in milliseconds (default: 10ms)
30
+ */
31
+ constructor(watcher, options) {
32
+ this._watcher = watcher;
33
+ this._debounceMs = options?.debounceMs ?? 10;
34
+ }
35
+ async watch(options, onChange) {
36
+ const { id = options.path } = options;
37
+ // Wrap the user's callback with debouncing logic
38
+ const debouncedCallback = (changedPaths) => {
39
+ this._addPending(id, changedPaths, onChange);
40
+ };
41
+ // Delegate to wrapped watcher
42
+ const unwatch = await this._watcher.watch(options, debouncedCallback);
43
+ // Return unwatch callback that flushes pending and cleans up
44
+ return async () => {
45
+ this._flushSubscriber(id);
46
+ this._subscribers.delete(id);
47
+ await unwatch();
48
+ };
49
+ }
50
+ async unwatch(id) {
51
+ this._flushSubscriber(id);
52
+ this._subscribers.delete(id);
53
+ await this._watcher.unwatch(id);
54
+ }
55
+ async dispose() {
56
+ // Flush all pending notifications before disposing
57
+ for (const id of this._subscribers.keys()) {
58
+ this._flushSubscriber(id);
59
+ }
60
+ this._subscribers.clear();
61
+ await this._watcher.dispose();
62
+ }
63
+ /**
64
+ * Add changed paths to pending notifications for a subscriber.
65
+ * Implements hybrid flush strategy:
66
+ * - Idle state (no timer): flush immediately, start timer
67
+ * - Debouncing state (timer active): accumulate, reset timer
68
+ */
69
+ _addPending(id, changedPaths, callback) {
70
+ let state = this._subscribers.get(id);
71
+ if (!state) {
72
+ state = { callback, pendingPaths: new Set() };
73
+ this._subscribers.set(id, state);
74
+ }
75
+ // Hybrid flush strategy
76
+ if (!state.flushTimer && state.pendingPaths.size === 0) {
77
+ // Idle state - flush immediately (zero latency)
78
+ callback(changedPaths);
79
+ // Start timer to enable debouncing for subsequent rapid changes
80
+ state.flushTimer = setTimeout(() => {
81
+ state.flushTimer = undefined;
82
+ }, this._debounceMs);
83
+ }
84
+ else {
85
+ // Debouncing state - accumulate paths and reset timer
86
+ changedPaths.forEach((p) => state.pendingPaths.add(p));
87
+ clearTimeout(state.flushTimer);
88
+ state.flushTimer = setTimeout(() => {
89
+ this._flushSubscriber(id);
90
+ }, this._debounceMs);
91
+ }
92
+ }
93
+ /**
94
+ * Flush pending notifications for a subscriber and clear its state.
95
+ */
96
+ _flushSubscriber(id) {
97
+ const state = this._subscribers.get(id);
98
+ if (!state)
99
+ return;
100
+ clearTimeout(state.flushTimer);
101
+ state.flushTimer = undefined;
102
+ if (state.pendingPaths.size > 0) {
103
+ state.callback(Array.from(state.pendingPaths));
104
+ state.pendingPaths.clear();
105
+ }
106
+ }
107
+ }
108
+ //# sourceMappingURL=DebouncedWatcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DebouncedWatcher.js","sourceRoot":"","sources":["../src/DebouncedWatcher.ts"],"names":[],"mappings":"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","sourcesContent":["import type { UnwatchCallback, WatchChangeCallback, Watcher, WatchOptions } from './types/Watcher.js';\n\ninterface SubscriberState {\n /** Original callback provided by the user */\n callback: WatchChangeCallback;\n /** Paths accumulated during debouncing */\n pendingPaths: Set<string>;\n /** Timer for debouncing subsequent changes */\n flushTimer?: NodeJS.Timeout;\n}\n\n/**\n * Decorator that adds debouncing/batching to any Watcher implementation.\n *\n * Uses a hybrid flush strategy:\n * - First change: immediate flush (0ms latency)\n * - Subsequent changes within debounce window: batched together\n * - After debounce window expires: returns to immediate flush mode\n *\n * This provides zero-latency for single file saves while efficiently batching\n * bulk operations (git checkout, npm install, etc.).\n *\n * @example\n * ```typescript\n * // Debounce any watcher\n * const watcher = new DebouncedWatcher(new DefaultWatcher(), { debounceMs: 10 });\n *\n * // Compose with SharedRootWatcher\n * const shared = new SharedRootWatcher(new DefaultWatcher(), '/monorepo');\n * const debounced = new DebouncedWatcher(shared, { debounceMs: 10 });\n * ```\n */\nexport class DebouncedWatcher implements Watcher {\n private _watcher: Watcher;\n private _debounceMs: number;\n private _subscribers = new Map<string, SubscriberState>();\n\n /**\n * @param watcher - The watcher to wrap with debouncing\n * @param options - Configuration options\n * @param options.debounceMs - Debounce delay in milliseconds (default: 10ms)\n */\n constructor(watcher: Watcher, options?: { debounceMs?: number }) {\n this._watcher = watcher;\n this._debounceMs = options?.debounceMs ?? 10;\n }\n\n async watch(options: WatchOptions, onChange: WatchChangeCallback): Promise<UnwatchCallback> {\n const { id = options.path } = options;\n\n // Wrap the user's callback with debouncing logic\n const debouncedCallback = (changedPaths: string[]) => {\n this._addPending(id, changedPaths, onChange);\n };\n\n // Delegate to wrapped watcher\n const unwatch = await this._watcher.watch(options, debouncedCallback);\n\n // Return unwatch callback that flushes pending and cleans up\n return async () => {\n this._flushSubscriber(id);\n this._subscribers.delete(id);\n await unwatch();\n };\n }\n\n async unwatch(id: string): Promise<void> {\n this._flushSubscriber(id);\n this._subscribers.delete(id);\n await this._watcher.unwatch(id);\n }\n\n async dispose(): Promise<void> {\n // Flush all pending notifications before disposing\n for (const id of this._subscribers.keys()) {\n this._flushSubscriber(id);\n }\n this._subscribers.clear();\n await this._watcher.dispose();\n }\n\n /**\n * Add changed paths to pending notifications for a subscriber.\n * Implements hybrid flush strategy:\n * - Idle state (no timer): flush immediately, start timer\n * - Debouncing state (timer active): accumulate, reset timer\n */\n private _addPending(id: string, changedPaths: string[], callback: WatchChangeCallback): void {\n let state = this._subscribers.get(id);\n if (!state) {\n state = { callback, pendingPaths: new Set() };\n this._subscribers.set(id, state);\n }\n\n // Hybrid flush strategy\n if (!state.flushTimer && state.pendingPaths.size === 0) {\n // Idle state - flush immediately (zero latency)\n callback(changedPaths);\n\n // Start timer to enable debouncing for subsequent rapid changes\n state.flushTimer = setTimeout(() => {\n state.flushTimer = undefined;\n }, this._debounceMs);\n } else {\n // Debouncing state - accumulate paths and reset timer\n changedPaths.forEach((p) => state.pendingPaths.add(p));\n\n clearTimeout(state.flushTimer);\n state.flushTimer = setTimeout(() => {\n this._flushSubscriber(id);\n }, this._debounceMs);\n }\n }\n\n /**\n * Flush pending notifications for a subscriber and clear its state.\n */\n private _flushSubscriber(id: string): void {\n const state = this._subscribers.get(id);\n if (!state) return;\n\n clearTimeout(state.flushTimer);\n state.flushTimer = undefined;\n\n if (state.pendingPaths.size > 0) {\n state.callback(Array.from(state.pendingPaths));\n state.pendingPaths.clear();\n }\n }\n}\n"]}
@@ -1,11 +1,15 @@
1
1
  import type { Backend, UnwatchCallback, WatchOptions } from './types/Watcher.js';
2
+ import type { WatcherPolicies } from './types/WatcherPolicy.js';
2
3
  import { BaseWatcher } from './BaseWatcher.js';
3
4
  /**
4
5
  * Creates a watcher in a fork which can watch a package and notify the client when it changes.
6
+ *
7
+ * Policies (shared-root, debounce) are applied inside the child process for better performance
8
+ * and reduced IPC message overhead.
5
9
  */
6
10
  export declare class ForkWatcher extends BaseWatcher {
7
11
  private _child;
8
- constructor(backend?: Backend);
12
+ constructor(backend?: Backend, policies?: WatcherPolicies);
9
13
  protected _watch(options: Required<WatchOptions>): UnwatchCallback;
10
14
  protected _dispose(): Promise<void>;
11
15
  protected _isDisposed(): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"ForkWatcher.d.ts","sourceRoot":"","sources":["../src/ForkWatcher.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACjF,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAK/C;;GAEG;AACH,qBAAa,WAAY,SAAQ,WAAW;IAC1C,OAAO,CAAC,MAAM,CAA2B;gBAE7B,OAAO,GAAE,OAAmB;IAuBxC,SAAS,CAAC,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,eAAe;cAQlD,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAYzC,SAAS,CAAC,WAAW,IAAI,OAAO;IAIhC,6EAA6E;IAC7E,OAAO,CAAC,YAAY;CAOrB"}
1
+ {"version":3,"file":"ForkWatcher.d.ts","sourceRoot":"","sources":["../src/ForkWatcher.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACjF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAK/C;;;;;GAKG;AACH,qBAAa,WAAY,SAAQ,WAAW;IAC1C,OAAO,CAAC,MAAM,CAA2B;gBAE7B,OAAO,GAAE,OAAmB,EAAE,QAAQ,CAAC,EAAE,eAAe;IAyBpE,SAAS,CAAC,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,eAAe;cAQlD,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAYzC,SAAS,CAAC,WAAW,IAAI,OAAO;IAIhC,6EAA6E;IAC7E,OAAO,CAAC,YAAY;CAOrB"}
@@ -6,12 +6,17 @@ const currentDir = path.dirname(fileURLToPath(import.meta.url));
6
6
  const watcherForkWrapper = path.resolve(currentDir, 'watcherForkWrapper.js');
7
7
  /**
8
8
  * Creates a watcher in a fork which can watch a package and notify the client when it changes.
9
+ *
10
+ * Policies (shared-root, debounce) are applied inside the child process for better performance
11
+ * and reduced IPC message overhead.
9
12
  */
10
13
  export class ForkWatcher extends BaseWatcher {
11
14
  _child;
12
- constructor(backend = 'default') {
15
+ constructor(backend = 'default', policies) {
13
16
  super();
14
- this._child = fork(watcherForkWrapper, [backend]);
17
+ // Pass backend and serialized policies to child process
18
+ const args = [backend, JSON.stringify(policies || {})];
19
+ this._child = fork(watcherForkWrapper, args);
15
20
  this._child.on('message', (message) => {
16
21
  switch (message.type) {
17
22
  case 'change':
@@ -1 +1 @@
1
- {"version":3,"file":"ForkWatcher.js","sourceRoot":"","sources":["../src/ForkWatcher.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,IAAI,EAAqB,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAGpC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAChE,MAAM,kBAAkB,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,uBAAuB,CAAC,CAAC;AAE7E;;GAEG;AACH,MAAM,OAAO,WAAY,SAAQ,WAAW;IAClC,MAAM,CAA2B;IAEzC,YAAY,UAAmB,SAAS;QACtC,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,kBAAkB,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;QAElD,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,OAAqB,EAAE,EAAE;YAClD,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;gBACrB,KAAK,QAAQ;oBACX,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;wBACvB,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;oBAChD,CAAC;oBACD,IAAI,CAAC,kBAAkB,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;oBAC5D,MAAM;gBAER,KAAK,OAAO;oBACV,OAAO,CAAC,KAAK,CAAC,2CAA2C,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;oBAC1E,MAAM;gBAER;oBACE,OAAO,CAAC,IAAI,CAAC,yBAAyB,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YACrE,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAES,MAAM,CAAC,OAA+B;QAC9C,IAAI,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;QAE9C,OAAO,GAAG,EAAE;YACV,IAAI,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC;QACzD,CAAC,CAAC;IACJ,CAAC;IAES,KAAK,CAAC,QAAQ;QACtB,MAAM,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,EAAE;YAC5B,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;gBACjB,OAAO,CAAC,EAAE,CAAC;YACb,CAAC;YAED,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YAC3B,IAAI,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;YACxC,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;QAC1B,CAAC,CAAC,CAAC;IACL,CAAC;IAES,WAAW;QACnB,OAAO,CAAC,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC,WAAW,EAAE,CAAC;IAC7C,CAAC;IAED,6EAA6E;IACrE,YAAY,CAAC,OAAsB;QACzC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;YACnC,IAAI,KAAK,EAAE,CAAC;gBACV,OAAO,CAAC,KAAK,CAAC,0DAA0D,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;YAC3F,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;CACF","sourcesContent":["import path from 'path';\nimport { fork, type ChildProcess } from 'child_process';\nimport { fileURLToPath } from 'url';\nimport type { ChildMessage, ParentMessage } from './types/WatchMessages.js';\nimport type { Backend, UnwatchCallback, WatchOptions } from './types/Watcher.js';\nimport { BaseWatcher } from './BaseWatcher.js';\n\nconst currentDir = path.dirname(fileURLToPath(import.meta.url));\nconst watcherForkWrapper = path.resolve(currentDir, 'watcherForkWrapper.js');\n\n/**\n * Creates a watcher in a fork which can watch a package and notify the client when it changes.\n */\nexport class ForkWatcher extends BaseWatcher {\n private _child: ChildProcess | undefined;\n\n constructor(backend: Backend = 'default') {\n super();\n this._child = fork(watcherForkWrapper, [backend]);\n\n this._child.on('message', (message: ChildMessage) => {\n switch (message.type) {\n case 'change':\n if (this._isDisposed()) {\n throw new Error('Watcher has been disposed.');\n }\n this._onChangeCallbacks[message.id]?.(message.changedPaths);\n break;\n\n case 'error':\n console.error(`An error occurred while watching files: ${message.error}`);\n break;\n\n default:\n console.warn(`Unknown message type: ${JSON.stringify(message)}`);\n }\n });\n }\n\n protected _watch(options: Required<WatchOptions>): UnwatchCallback {\n this._sendMessage({ type: 'watch', options });\n\n return () => {\n this._sendMessage({ type: 'unwatch', id: options.id });\n };\n }\n\n protected async _dispose(): Promise<void> {\n await new Promise<void>((r) => {\n if (!this._child) {\n return r();\n }\n\n this._child.on('close', r);\n this._sendMessage({ type: 'shutdown' });\n this._child = undefined;\n });\n }\n\n protected _isDisposed(): boolean {\n return !this._child || super._isDisposed();\n }\n\n /** Typed wrapper for `this._child.send()`. No-op if `_child` is disposed. */\n private _sendMessage(message: ParentMessage): void {\n this._child?.send(message, (error) => {\n if (error) {\n console.error(`Failed to send message to file watcher process. Error: ${error.message}`);\n }\n });\n }\n}\n"]}
1
+ {"version":3,"file":"ForkWatcher.js","sourceRoot":"","sources":["../src/ForkWatcher.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,IAAI,EAAqB,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAIpC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAChE,MAAM,kBAAkB,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,uBAAuB,CAAC,CAAC;AAE7E;;;;;GAKG;AACH,MAAM,OAAO,WAAY,SAAQ,WAAW;IAClC,MAAM,CAA2B;IAEzC,YAAY,UAAmB,SAAS,EAAE,QAA0B;QAClE,KAAK,EAAE,CAAC;QACR,wDAAwD;QACxD,MAAM,IAAI,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,CAAC;QACvD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,kBAAkB,EAAE,IAAI,CAAC,CAAC;QAE7C,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,OAAqB,EAAE,EAAE;YAClD,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;gBACrB,KAAK,QAAQ;oBACX,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;wBACvB,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;oBAChD,CAAC;oBACD,IAAI,CAAC,kBAAkB,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;oBAC5D,MAAM;gBAER,KAAK,OAAO;oBACV,OAAO,CAAC,KAAK,CAAC,2CAA2C,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;oBAC1E,MAAM;gBAER;oBACE,OAAO,CAAC,IAAI,CAAC,yBAAyB,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YACrE,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAES,MAAM,CAAC,OAA+B;QAC9C,IAAI,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;QAE9C,OAAO,GAAG,EAAE;YACV,IAAI,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC;QACzD,CAAC,CAAC;IACJ,CAAC;IAES,KAAK,CAAC,QAAQ;QACtB,MAAM,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,EAAE;YAC5B,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;gBACjB,OAAO,CAAC,EAAE,CAAC;YACb,CAAC;YAED,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YAC3B,IAAI,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;YACxC,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;QAC1B,CAAC,CAAC,CAAC;IACL,CAAC;IAES,WAAW;QACnB,OAAO,CAAC,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC,WAAW,EAAE,CAAC;IAC7C,CAAC;IAED,6EAA6E;IACrE,YAAY,CAAC,OAAsB;QACzC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;YACnC,IAAI,KAAK,EAAE,CAAC;gBACV,OAAO,CAAC,KAAK,CAAC,0DAA0D,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;YAC3F,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC;CACF","sourcesContent":["import path from 'path';\nimport { fork, type ChildProcess } from 'child_process';\nimport { fileURLToPath } from 'url';\nimport type { ChildMessage, ParentMessage } from './types/WatchMessages.js';\nimport type { Backend, UnwatchCallback, WatchOptions } from './types/Watcher.js';\nimport type { WatcherPolicies } from './types/WatcherPolicy.js';\nimport { BaseWatcher } from './BaseWatcher.js';\n\nconst currentDir = path.dirname(fileURLToPath(import.meta.url));\nconst watcherForkWrapper = path.resolve(currentDir, 'watcherForkWrapper.js');\n\n/**\n * Creates a watcher in a fork which can watch a package and notify the client when it changes.\n *\n * Policies (shared-root, debounce) are applied inside the child process for better performance\n * and reduced IPC message overhead.\n */\nexport class ForkWatcher extends BaseWatcher {\n private _child: ChildProcess | undefined;\n\n constructor(backend: Backend = 'default', policies?: WatcherPolicies) {\n super();\n // Pass backend and serialized policies to child process\n const args = [backend, JSON.stringify(policies || {})];\n this._child = fork(watcherForkWrapper, args);\n\n this._child.on('message', (message: ChildMessage) => {\n switch (message.type) {\n case 'change':\n if (this._isDisposed()) {\n throw new Error('Watcher has been disposed.');\n }\n this._onChangeCallbacks[message.id]?.(message.changedPaths);\n break;\n\n case 'error':\n console.error(`An error occurred while watching files: ${message.error}`);\n break;\n\n default:\n console.warn(`Unknown message type: ${JSON.stringify(message)}`);\n }\n });\n }\n\n protected _watch(options: Required<WatchOptions>): UnwatchCallback {\n this._sendMessage({ type: 'watch', options });\n\n return () => {\n this._sendMessage({ type: 'unwatch', id: options.id });\n };\n }\n\n protected async _dispose(): Promise<void> {\n await new Promise<void>((r) => {\n if (!this._child) {\n return r();\n }\n\n this._child.on('close', r);\n this._sendMessage({ type: 'shutdown' });\n this._child = undefined;\n });\n }\n\n protected _isDisposed(): boolean {\n return !this._child || super._isDisposed();\n }\n\n /** Typed wrapper for `this._child.send()`. No-op if `_child` is disposed. */\n private _sendMessage(message: ParentMessage): void {\n this._child?.send(message, (error) => {\n if (error) {\n console.error(`Failed to send message to file watcher process. Error: ${error.message}`);\n }\n });\n }\n}\n"]}
@@ -0,0 +1,26 @@
1
+ import type { UnwatchCallback, WatchChangeCallback, Watcher, WatchOptions } from './types/Watcher.js';
2
+ /**
3
+ * Mock watcher for testing purposes.
4
+ * Allows manual triggering of file change events without actual file system watching.
5
+ */
6
+ export declare class MockWatcher implements Watcher {
7
+ private _watches;
8
+ private _isDisposed;
9
+ watch(options: WatchOptions, onChange: WatchChangeCallback): Promise<UnwatchCallback>;
10
+ unwatch(id: string): Promise<void>;
11
+ dispose(): Promise<void>;
12
+ /**
13
+ * Manually trigger a file change event for testing.
14
+ * This will call all registered onChange callbacks that match the given path.
15
+ */
16
+ triggerChange(changedPaths: string[]): void;
17
+ /**
18
+ * Get the number of active watches (for testing)
19
+ */
20
+ get watchCount(): number;
21
+ /**
22
+ * Check if a specific path is being watched (for testing)
23
+ */
24
+ isWatching(id: string): boolean;
25
+ }
26
+ //# sourceMappingURL=MockWatcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MockWatcher.d.ts","sourceRoot":"","sources":["../src/MockWatcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,mBAAmB,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEtG;;;GAGG;AACH,qBAAa,WAAY,YAAW,OAAO;IACzC,OAAO,CAAC,QAAQ,CAA0C;IAC1D,OAAO,CAAC,WAAW,CAAS;IAE5B,KAAK,CAAC,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,mBAAmB,GAAG,OAAO,CAAC,eAAe,CAAC;IAarF,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKlC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAMxB;;;OAGG;IACH,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,IAAI;IAW3C;;OAEG;IACH,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED;;OAEG;IACH,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;CAGhC"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Mock watcher for testing purposes.
3
+ * Allows manual triggering of file change events without actual file system watching.
4
+ */
5
+ export class MockWatcher {
6
+ _watches = new Map();
7
+ _isDisposed = false;
8
+ watch(options, onChange) {
9
+ if (this._isDisposed) {
10
+ throw new Error('MockWatcher has been disposed');
11
+ }
12
+ const { id = options.path } = options;
13
+ this._watches.set(id, onChange);
14
+ return Promise.resolve(() => {
15
+ this._watches.delete(id);
16
+ });
17
+ }
18
+ unwatch(id) {
19
+ this._watches.delete(id);
20
+ return Promise.resolve();
21
+ }
22
+ dispose() {
23
+ this._isDisposed = true;
24
+ this._watches.clear();
25
+ return Promise.resolve();
26
+ }
27
+ /**
28
+ * Manually trigger a file change event for testing.
29
+ * This will call all registered onChange callbacks that match the given path.
30
+ */
31
+ triggerChange(changedPaths) {
32
+ if (this._isDisposed) {
33
+ throw new Error('MockWatcher has been disposed');
34
+ }
35
+ // Call all watch callbacks (in real scenario, filtering would happen in underlying watcher)
36
+ for (const callback of this._watches.values()) {
37
+ callback(changedPaths);
38
+ }
39
+ }
40
+ /**
41
+ * Get the number of active watches (for testing)
42
+ */
43
+ get watchCount() {
44
+ return this._watches.size;
45
+ }
46
+ /**
47
+ * Check if a specific path is being watched (for testing)
48
+ */
49
+ isWatching(id) {
50
+ return this._watches.has(id);
51
+ }
52
+ }
53
+ //# sourceMappingURL=MockWatcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MockWatcher.js","sourceRoot":"","sources":["../src/MockWatcher.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,OAAO,WAAW;IACd,QAAQ,GAAG,IAAI,GAAG,EAA+B,CAAC;IAClD,WAAW,GAAG,KAAK,CAAC;IAE5B,KAAK,CAAC,OAAqB,EAAE,QAA6B;QACxD,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;QACnD,CAAC;QAED,MAAM,EAAE,EAAE,GAAG,OAAO,CAAC,IAAI,EAAE,GAAG,OAAO,CAAC;QACtC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;QAEhC,OAAO,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE;YAC1B,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC3B,CAAC,CAAC,CAAC;IACL,CAAC;IAED,OAAO,CAAC,EAAU;QAChB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACzB,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC3B,CAAC;IAED,OAAO;QACL,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;QACxB,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QACtB,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC3B,CAAC;IAED;;;OAGG;IACH,aAAa,CAAC,YAAsB;QAClC,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;QACnD,CAAC;QAED,4FAA4F;QAC5F,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;YAC9C,QAAQ,CAAC,YAAY,CAAC,CAAC;QACzB,CAAC;IACH,CAAC;IAED;;OAEG;IACH,IAAI,UAAU;QACZ,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;IAC5B,CAAC;IAED;;OAEG;IACH,UAAU,CAAC,EAAU;QACnB,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC/B,CAAC;CACF","sourcesContent":["import type { UnwatchCallback, WatchChangeCallback, Watcher, WatchOptions } from './types/Watcher.js';\n\n/**\n * Mock watcher for testing purposes.\n * Allows manual triggering of file change events without actual file system watching.\n */\nexport class MockWatcher implements Watcher {\n private _watches = new Map<string, WatchChangeCallback>();\n private _isDisposed = false;\n\n watch(options: WatchOptions, onChange: WatchChangeCallback): Promise<UnwatchCallback> {\n if (this._isDisposed) {\n throw new Error('MockWatcher has been disposed');\n }\n\n const { id = options.path } = options;\n this._watches.set(id, onChange);\n\n return Promise.resolve(() => {\n this._watches.delete(id);\n });\n }\n\n unwatch(id: string): Promise<void> {\n this._watches.delete(id);\n return Promise.resolve();\n }\n\n dispose(): Promise<void> {\n this._isDisposed = true;\n this._watches.clear();\n return Promise.resolve();\n }\n\n /**\n * Manually trigger a file change event for testing.\n * This will call all registered onChange callbacks that match the given path.\n */\n triggerChange(changedPaths: string[]): void {\n if (this._isDisposed) {\n throw new Error('MockWatcher has been disposed');\n }\n\n // Call all watch callbacks (in real scenario, filtering would happen in underlying watcher)\n for (const callback of this._watches.values()) {\n callback(changedPaths);\n }\n }\n\n /**\n * Get the number of active watches (for testing)\n */\n get watchCount(): number {\n return this._watches.size;\n }\n\n /**\n * Check if a specific path is being watched (for testing)\n */\n isWatching(id: string): boolean {\n return this._watches.has(id);\n }\n}\n"]}
@@ -0,0 +1,32 @@
1
+ import type { UnwatchCallback, WatchChangeCallback, Watcher, WatchOptionsWithId } from './types/Watcher.js';
2
+ import { BaseWatcher } from './BaseWatcher.js';
3
+ /**
4
+ * Orchestrator that wraps an existing watcher to efficiently handle many watch paths (100+).
5
+ *
6
+ * Instead of creating N watchers for N packages, this creates a single watch at the root
7
+ * and routes events to subscribers based on path matching.
8
+ *
9
+ * This is a decorator pattern - it takes any Watcher implementation and multiplexes it.
10
+ */
11
+ export declare class SharedRootWatcher extends BaseWatcher {
12
+ private _watcher;
13
+ private _rootUnwatch?;
14
+ private _subscribers;
15
+ private _pathIndex;
16
+ private _rootPath;
17
+ private _isInitialized;
18
+ /**
19
+ * @param watcher - The underlying watcher to orchestrate (can be DefaultWatcher, ParcelWatcher, or ForkWatcher)
20
+ * @param rootPath - The root path to watch (typically the monorepo root)
21
+ */
22
+ constructor(watcher: Watcher, rootPath: string);
23
+ protected _watch(options: WatchOptionsWithId, onChange: WatchChangeCallback): Promise<UnwatchCallback>;
24
+ protected _dispose(): Promise<void>;
25
+ private _initializeRootWatcher;
26
+ private _findRelevantSubscribers;
27
+ private _handleFileChanges;
28
+ private _shouldNotifySubscriber;
29
+ /** Get number of subscribers (for testing/debugging) */
30
+ get subscriberCount(): number;
31
+ }
32
+ //# sourceMappingURL=SharedRootWatcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SharedRootWatcher.d.ts","sourceRoot":"","sources":["../src/SharedRootWatcher.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,mBAAmB,EAAE,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAC5G,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAqB/C;;;;;;;GAOG;AACH,qBAAa,iBAAkB,SAAQ,WAAW;IAChD,OAAO,CAAC,QAAQ,CAAU;IAC1B,OAAO,CAAC,YAAY,CAAC,CAAkB;IACvC,OAAO,CAAC,YAAY,CAAiC;IACrD,OAAO,CAAC,UAAU,CAAkC;IACpD,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,cAAc,CAAS;IAE/B;;;OAGG;gBACS,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM;cAM9B,MAAM,CAAC,OAAO,EAAE,kBAAkB,EAAE,QAAQ,EAAE,mBAAmB,GAAG,OAAO,CAAC,eAAe,CAAC;cAqE5F,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;YAS3B,sBAAsB;IAYpC,OAAO,CAAC,wBAAwB;IA4BhC,OAAO,CAAC,kBAAkB;IAiB1B,OAAO,CAAC,uBAAuB;IAuB/B,wDAAwD;IACxD,IAAW,eAAe,IAAI,MAAM,CAEnC;CACF"}
@@ -0,0 +1,166 @@
1
+ import path from 'path';
2
+ import picomatch from 'picomatch';
3
+ import { BaseWatcher } from './BaseWatcher.js';
4
+ import { sourceFilesGlobs } from '@ms-cloudpack/path-utilities';
5
+ import { slash } from '@ms-cloudpack/path-string-parsing';
6
+ import { separateGlobs } from './separateGlobs.js';
7
+ /**
8
+ * Orchestrator that wraps an existing watcher to efficiently handle many watch paths (100+).
9
+ *
10
+ * Instead of creating N watchers for N packages, this creates a single watch at the root
11
+ * and routes events to subscribers based on path matching.
12
+ *
13
+ * This is a decorator pattern - it takes any Watcher implementation and multiplexes it.
14
+ */
15
+ export class SharedRootWatcher extends BaseWatcher {
16
+ _watcher;
17
+ _rootUnwatch;
18
+ _subscribers = new Map();
19
+ _pathIndex = new Map(); // path → subscriber IDs for fast lookup
20
+ _rootPath;
21
+ _isInitialized = false;
22
+ /**
23
+ * @param watcher - The underlying watcher to orchestrate (can be DefaultWatcher, ParcelWatcher, or ForkWatcher)
24
+ * @param rootPath - The root path to watch (typically the monorepo root)
25
+ */
26
+ constructor(watcher, rootPath) {
27
+ super();
28
+ this._watcher = watcher;
29
+ this._rootPath = path.resolve(rootPath);
30
+ }
31
+ async _watch(options, onChange) {
32
+ const { path: watchPath, id, watchPaths = sourceFilesGlobs } = options;
33
+ const absolutePath = path.resolve(watchPath);
34
+ // Validate that the watch path is under the root
35
+ // Normalize for cross-platform comparison
36
+ if (!slash(absolutePath).startsWith(slash(this._rootPath))) {
37
+ throw new Error(`Cannot watch path "${absolutePath}" because it's not under the root "${this._rootPath}". ` +
38
+ `SharedRootWatcher requires all watched paths to be under the configured root.`);
39
+ }
40
+ // Initialize the root watcher lazily on first subscription
41
+ if (!this._isInitialized) {
42
+ await this._initializeRootWatcher();
43
+ this._isInitialized = true;
44
+ }
45
+ const { positiveGlobs, negativeGlobs } = separateGlobs(watchPaths);
46
+ // Create matchers for this subscriber
47
+ // Using picomatch for 10-50x better performance than minimatch
48
+ const matchers = {
49
+ positive: positiveGlobs.map((glob) => {
50
+ const absoluteGlob = slash(path.resolve(absolutePath, glob));
51
+ return picomatch(absoluteGlob, { dot: true, windows: true });
52
+ }),
53
+ negative: negativeGlobs.map((glob) => {
54
+ const absoluteGlob = slash(path.resolve(absolutePath, glob));
55
+ return picomatch(absoluteGlob, { dot: true, windows: true });
56
+ }),
57
+ };
58
+ // Add subscriber
59
+ this._subscribers.set(id, {
60
+ rootPath: absolutePath,
61
+ watchPaths,
62
+ callback: onChange,
63
+ matchers,
64
+ });
65
+ // Add to path index for fast lookup
66
+ const subscribersAtPath = this._pathIndex.get(absolutePath) || new Set();
67
+ subscribersAtPath.add(id);
68
+ this._pathIndex.set(absolutePath, subscribersAtPath);
69
+ return async () => {
70
+ this._subscribers.delete(id);
71
+ // Remove from path index
72
+ const indexedSubscribers = this._pathIndex.get(absolutePath);
73
+ if (indexedSubscribers) {
74
+ indexedSubscribers.delete(id);
75
+ // Clean up empty sets
76
+ if (indexedSubscribers.size === 0) {
77
+ this._pathIndex.delete(absolutePath);
78
+ }
79
+ }
80
+ // If no more subscribers, unwatch the root
81
+ if (this._subscribers.size === 0) {
82
+ await this._rootUnwatch?.();
83
+ this._rootUnwatch = undefined;
84
+ this._isInitialized = false;
85
+ }
86
+ };
87
+ }
88
+ async _dispose() {
89
+ await this._rootUnwatch?.();
90
+ this._rootUnwatch = undefined;
91
+ await this._watcher.dispose();
92
+ this._subscribers.clear();
93
+ this._pathIndex.clear();
94
+ this._isInitialized = false;
95
+ }
96
+ async _initializeRootWatcher() {
97
+ // Watch the entire root with the underlying watcher
98
+ // No filtering at this level - subscribers control their own filtering via watchPaths
99
+ this._rootUnwatch = await this._watcher.watch({
100
+ path: this._rootPath,
101
+ id: '__shared_root__',
102
+ }, (changedPaths) => this._handleFileChanges(changedPaths));
103
+ }
104
+ _findRelevantSubscribers(filePath) {
105
+ const relevantIds = new Set();
106
+ // Start from the file and walk up to the root, checking each parent directory
107
+ let currentPath = filePath;
108
+ const normalizedRoot = slash(this._rootPath);
109
+ while (slash(currentPath).startsWith(normalizedRoot)) {
110
+ const subscribersAtLevel = this._pathIndex.get(currentPath);
111
+ if (subscribersAtLevel) {
112
+ subscribersAtLevel.forEach((id) => relevantIds.add(id));
113
+ }
114
+ const parentPath = path.dirname(currentPath);
115
+ if (parentPath === currentPath)
116
+ break; // Reached filesystem root
117
+ currentPath = parentPath;
118
+ }
119
+ // Convert IDs to actual subscribers
120
+ const subscribers = [];
121
+ for (const id of relevantIds) {
122
+ const subscriber = this._subscribers.get(id);
123
+ if (subscriber) {
124
+ subscribers.push(subscriber);
125
+ }
126
+ }
127
+ return subscribers;
128
+ }
129
+ _handleFileChanges(changedPaths) {
130
+ for (const filePath of changedPaths) {
131
+ const absolutePath = path.resolve(filePath);
132
+ const normalizedPath = slash(absolutePath);
133
+ // Use path index to find only relevant subscribers (O(depth) instead of O(n))
134
+ const relevantSubscribers = this._findRelevantSubscribers(absolutePath);
135
+ for (const subscriber of relevantSubscribers) {
136
+ if (this._shouldNotifySubscriber(normalizedPath, subscriber)) {
137
+ // Call callback immediately - no batching at this level
138
+ subscriber.callback([absolutePath]);
139
+ }
140
+ }
141
+ }
142
+ }
143
+ _shouldNotifySubscriber(normalizedFilePath, subscriber) {
144
+ // Note: normalizedFilePath is already normalized to forward slashes
145
+ const normalizedRootPath = slash(subscriber.rootPath);
146
+ // 1. Check if file is under subscriber's root path
147
+ if (!normalizedFilePath.startsWith(normalizedRootPath)) {
148
+ return false;
149
+ }
150
+ // 2. Check negative globs (ignores) first - if any match, exclude
151
+ if (subscriber.matchers.negative.some((matcher) => matcher(normalizedFilePath))) {
152
+ return false;
153
+ }
154
+ // 3. If no positive globs specified, include everything (that's not ignored)
155
+ if (subscriber.matchers.positive.length === 0) {
156
+ return true;
157
+ }
158
+ // 4. Check if file matches any positive glob
159
+ return subscriber.matchers.positive.some((matcher) => matcher(normalizedFilePath));
160
+ }
161
+ /** Get number of subscribers (for testing/debugging) */
162
+ get subscriberCount() {
163
+ return this._subscribers.size;
164
+ }
165
+ }
166
+ //# sourceMappingURL=SharedRootWatcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SharedRootWatcher.js","sourceRoot":"","sources":["../src/SharedRootWatcher.ts"],"names":[],"mappings":"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","sourcesContent":["import path from 'path';\nimport picomatch from 'picomatch';\nimport type { UnwatchCallback, WatchChangeCallback, Watcher, WatchOptionsWithId } from './types/Watcher.js';\nimport { BaseWatcher } from './BaseWatcher.js';\nimport { sourceFilesGlobs } from '@ms-cloudpack/path-utilities';\nimport { slash } from '@ms-cloudpack/path-string-parsing';\nimport { separateGlobs } from './separateGlobs.js';\n\ntype PathMatcher = (filePath: string) => boolean;\n\ninterface Subscriber {\n /** Absolute path being watched */\n rootPath: string;\n /** Globs to match (positive and negative) */\n watchPaths: string[];\n /** Callback to invoke on changes */\n callback: WatchChangeCallback;\n /** Compiled matchers for performance */\n matchers: {\n positive: PathMatcher[];\n negative: PathMatcher[];\n };\n}\n\n/**\n * Orchestrator that wraps an existing watcher to efficiently handle many watch paths (100+).\n *\n * Instead of creating N watchers for N packages, this creates a single watch at the root\n * and routes events to subscribers based on path matching.\n *\n * This is a decorator pattern - it takes any Watcher implementation and multiplexes it.\n */\nexport class SharedRootWatcher extends BaseWatcher {\n private _watcher: Watcher;\n private _rootUnwatch?: UnwatchCallback;\n private _subscribers = new Map<string, Subscriber>();\n private _pathIndex = new Map<string, Set<string>>(); // path → subscriber IDs for fast lookup\n private _rootPath: string;\n private _isInitialized = false;\n\n /**\n * @param watcher - The underlying watcher to orchestrate (can be DefaultWatcher, ParcelWatcher, or ForkWatcher)\n * @param rootPath - The root path to watch (typically the monorepo root)\n */\n constructor(watcher: Watcher, rootPath: string) {\n super();\n this._watcher = watcher;\n this._rootPath = path.resolve(rootPath);\n }\n\n protected async _watch(options: WatchOptionsWithId, onChange: WatchChangeCallback): Promise<UnwatchCallback> {\n const { path: watchPath, id, watchPaths = sourceFilesGlobs } = options;\n const absolutePath = path.resolve(watchPath);\n\n // Validate that the watch path is under the root\n // Normalize for cross-platform comparison\n if (!slash(absolutePath).startsWith(slash(this._rootPath))) {\n throw new Error(\n `Cannot watch path \"${absolutePath}\" because it's not under the root \"${this._rootPath}\". ` +\n `SharedRootWatcher requires all watched paths to be under the configured root.`,\n );\n }\n\n // Initialize the root watcher lazily on first subscription\n if (!this._isInitialized) {\n await this._initializeRootWatcher();\n this._isInitialized = true;\n }\n\n const { positiveGlobs, negativeGlobs } = separateGlobs(watchPaths);\n\n // Create matchers for this subscriber\n // Using picomatch for 10-50x better performance than minimatch\n const matchers = {\n positive: positiveGlobs.map((glob) => {\n const absoluteGlob = slash(path.resolve(absolutePath, glob));\n return picomatch(absoluteGlob, { dot: true, windows: true });\n }),\n negative: negativeGlobs.map((glob) => {\n const absoluteGlob = slash(path.resolve(absolutePath, glob));\n return picomatch(absoluteGlob, { dot: true, windows: true });\n }),\n };\n\n // Add subscriber\n this._subscribers.set(id, {\n rootPath: absolutePath,\n watchPaths,\n callback: onChange,\n matchers,\n });\n\n // Add to path index for fast lookup\n const subscribersAtPath = this._pathIndex.get(absolutePath) || new Set<string>();\n subscribersAtPath.add(id);\n this._pathIndex.set(absolutePath, subscribersAtPath);\n\n return async () => {\n this._subscribers.delete(id);\n\n // Remove from path index\n const indexedSubscribers = this._pathIndex.get(absolutePath);\n if (indexedSubscribers) {\n indexedSubscribers.delete(id);\n // Clean up empty sets\n if (indexedSubscribers.size === 0) {\n this._pathIndex.delete(absolutePath);\n }\n }\n\n // If no more subscribers, unwatch the root\n if (this._subscribers.size === 0) {\n await this._rootUnwatch?.();\n this._rootUnwatch = undefined;\n this._isInitialized = false;\n }\n };\n }\n\n protected async _dispose(): Promise<void> {\n await this._rootUnwatch?.();\n this._rootUnwatch = undefined;\n await this._watcher.dispose();\n this._subscribers.clear();\n this._pathIndex.clear();\n this._isInitialized = false;\n }\n\n private async _initializeRootWatcher(): Promise<void> {\n // Watch the entire root with the underlying watcher\n // No filtering at this level - subscribers control their own filtering via watchPaths\n this._rootUnwatch = await this._watcher.watch(\n {\n path: this._rootPath,\n id: '__shared_root__',\n },\n (changedPaths) => this._handleFileChanges(changedPaths),\n );\n }\n\n private _findRelevantSubscribers(filePath: string): Subscriber[] {\n const relevantIds = new Set<string>();\n\n // Start from the file and walk up to the root, checking each parent directory\n let currentPath = filePath;\n const normalizedRoot = slash(this._rootPath);\n while (slash(currentPath).startsWith(normalizedRoot)) {\n const subscribersAtLevel = this._pathIndex.get(currentPath);\n if (subscribersAtLevel) {\n subscribersAtLevel.forEach((id) => relevantIds.add(id));\n }\n\n const parentPath = path.dirname(currentPath);\n if (parentPath === currentPath) break; // Reached filesystem root\n currentPath = parentPath;\n }\n\n // Convert IDs to actual subscribers\n const subscribers: Subscriber[] = [];\n for (const id of relevantIds) {\n const subscriber = this._subscribers.get(id);\n if (subscriber) {\n subscribers.push(subscriber);\n }\n }\n return subscribers;\n }\n\n private _handleFileChanges(changedPaths: string[]): void {\n for (const filePath of changedPaths) {\n const absolutePath = path.resolve(filePath);\n const normalizedPath = slash(absolutePath);\n\n // Use path index to find only relevant subscribers (O(depth) instead of O(n))\n const relevantSubscribers = this._findRelevantSubscribers(absolutePath);\n\n for (const subscriber of relevantSubscribers) {\n if (this._shouldNotifySubscriber(normalizedPath, subscriber)) {\n // Call callback immediately - no batching at this level\n subscriber.callback([absolutePath]);\n }\n }\n }\n }\n\n private _shouldNotifySubscriber(normalizedFilePath: string, subscriber: Subscriber): boolean {\n // Note: normalizedFilePath is already normalized to forward slashes\n const normalizedRootPath = slash(subscriber.rootPath);\n\n // 1. Check if file is under subscriber's root path\n if (!normalizedFilePath.startsWith(normalizedRootPath)) {\n return false;\n }\n\n // 2. Check negative globs (ignores) first - if any match, exclude\n if (subscriber.matchers.negative.some((matcher) => matcher(normalizedFilePath))) {\n return false;\n }\n\n // 3. If no positive globs specified, include everything (that's not ignored)\n if (subscriber.matchers.positive.length === 0) {\n return true;\n }\n\n // 4. Check if file matches any positive glob\n return subscriber.matchers.positive.some((matcher) => matcher(normalizedFilePath));\n }\n\n /** Get number of subscribers (for testing/debugging) */\n public get subscriberCount(): number {\n return this._subscribers.size;\n }\n}\n"]}
@@ -0,0 +1,15 @@
1
+ import type { Watcher } from './types/Watcher.js';
2
+ import type { WatcherPolicies } from './types/WatcherPolicy.js';
3
+ /**
4
+ * Apply watcher policies in the defined order.
5
+ *
6
+ * Policy application order:
7
+ * 1. shared-root - Multiplexes N watches into 1 root watch
8
+ * 2. debounce - Batches file change notifications
9
+ *
10
+ * @param watcher - Base watcher to wrap with policies
11
+ * @param policies - Policies to apply
12
+ * @returns Watcher wrapped with the specified policies
13
+ */
14
+ export declare function applyPolicies(watcher: Watcher, policies?: WatcherPolicies): Watcher;
15
+ //# sourceMappingURL=applyPolicies.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"applyPolicies.d.ts","sourceRoot":"","sources":["../src/applyPolicies.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAIhE;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,eAAe,GAAG,OAAO,CAkBnF"}
@@ -0,0 +1,29 @@
1
+ import { SharedRootWatcher } from './SharedRootWatcher.js';
2
+ import { DebouncedWatcher } from './DebouncedWatcher.js';
3
+ /**
4
+ * Apply watcher policies in the defined order.
5
+ *
6
+ * Policy application order:
7
+ * 1. shared-root - Multiplexes N watches into 1 root watch
8
+ * 2. debounce - Batches file change notifications
9
+ *
10
+ * @param watcher - Base watcher to wrap with policies
11
+ * @param policies - Policies to apply
12
+ * @returns Watcher wrapped with the specified policies
13
+ */
14
+ export function applyPolicies(watcher, policies) {
15
+ if (!policies) {
16
+ return watcher;
17
+ }
18
+ let result = watcher;
19
+ // Apply shared-root first (multiplexes N watches → 1)
20
+ if (policies['shared-root']) {
21
+ result = new SharedRootWatcher(result, policies['shared-root'].rootPath);
22
+ }
23
+ // Apply debounce second (batches events)
24
+ if (policies.debounce) {
25
+ result = new DebouncedWatcher(result, { debounceMs: policies.debounce.debounceMs });
26
+ }
27
+ return result;
28
+ }
29
+ //# sourceMappingURL=applyPolicies.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"applyPolicies.js","sourceRoot":"","sources":["../src/applyPolicies.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEzD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,aAAa,CAAC,OAAgB,EAAE,QAA0B;IACxE,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,IAAI,MAAM,GAAG,OAAO,CAAC;IAErB,sDAAsD;IACtD,IAAI,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;QAC5B,MAAM,GAAG,IAAI,iBAAiB,CAAC,MAAM,EAAE,QAAQ,CAAC,aAAa,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC3E,CAAC;IAED,yCAAyC;IACzC,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;QACtB,MAAM,GAAG,IAAI,gBAAgB,CAAC,MAAM,EAAE,EAAE,UAAU,EAAE,QAAQ,CAAC,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;IACtF,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["import type { Watcher } from './types/Watcher.js';\nimport type { WatcherPolicies } from './types/WatcherPolicy.js';\nimport { SharedRootWatcher } from './SharedRootWatcher.js';\nimport { DebouncedWatcher } from './DebouncedWatcher.js';\n\n/**\n * Apply watcher policies in the defined order.\n *\n * Policy application order:\n * 1. shared-root - Multiplexes N watches into 1 root watch\n * 2. debounce - Batches file change notifications\n *\n * @param watcher - Base watcher to wrap with policies\n * @param policies - Policies to apply\n * @returns Watcher wrapped with the specified policies\n */\nexport function applyPolicies(watcher: Watcher, policies?: WatcherPolicies): Watcher {\n if (!policies) {\n return watcher;\n }\n\n let result = watcher;\n\n // Apply shared-root first (multiplexes N watches → 1)\n if (policies['shared-root']) {\n result = new SharedRootWatcher(result, policies['shared-root'].rootPath);\n }\n\n // Apply debounce second (batches events)\n if (policies.debounce) {\n result = new DebouncedWatcher(result, { debounceMs: policies.debounce.debounceMs });\n }\n\n return result;\n}\n"]}
@@ -1,12 +1,37 @@
1
1
  import type { Backend, Watcher } from './types/Watcher.js';
2
+ import type { WatcherPolicies } from './types/WatcherPolicy.js';
2
3
  /**
3
- * Create a file watcher. By default, the watcher will run in the main thread.
4
- * Use `{ type: 'fork' }` to run the watcher in a forked process.
4
+ * Create a file watcher with optional policies.
5
+ *
6
+ * Policies are applied in a fixed order:
7
+ * 1. Base watcher (DefaultWatcher, ParcelWatcher, or ForkWatcher)
8
+ * 2. shared-root - Multiplexes many watches into one root watch
9
+ * 3. debounce - Batches file change notifications
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * // Simple watcher
14
+ * const watcher = createWatcher();
15
+ *
16
+ * // Monorepo with shared root and debouncing
17
+ * const watcher = createWatcher({
18
+ * type: 'fork',
19
+ * backend: 'parcel',
20
+ * policies: {
21
+ * 'shared-root': { rootPath: '/monorepo' },
22
+ * 'debounce': { debounceMs: 10 }
23
+ * }
24
+ * });
25
+ * ```
5
26
  */
6
27
  export declare function createWatcher(options?: {
7
28
  /**
8
29
  * By default, the watcher will run in the main thread.
9
30
  * Specify `'fork'` to run the watcher in a forked process.
31
+ *
32
+ * When using 'fork', policies are applied inside the child process
33
+ * for better performance and reduced IPC overhead.
34
+ *
10
35
  * @default 'default'
11
36
  */
12
37
  type?: 'default' | 'fork';
@@ -16,5 +41,13 @@ export declare function createWatcher(options?: {
16
41
  * @default 'default'
17
42
  */
18
43
  backend?: Backend;
44
+ /**
45
+ * Optional policies to enhance the watcher.
46
+ * Policies are applied in order: shared-root → debounce
47
+ *
48
+ * For 'fork' type: policies are applied in the child process.
49
+ * For 'default' type: policies are applied in the parent process.
50
+ */
51
+ policies?: WatcherPolicies;
19
52
  }): Watcher;
20
53
  //# sourceMappingURL=createWatcher.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"createWatcher.d.ts","sourceRoot":"","sources":["../src/createWatcher.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAE3D;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE;IACtC;;;;OAIG;IACH,IAAI,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC;IAC1B;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,GAAG,OAAO,CAYV"}
1
+ {"version":3,"file":"createWatcher.d.ts","sourceRoot":"","sources":["../src/createWatcher.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAEhE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE;IACtC;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,SAAS,GAAG,MAAM,CAAC;IAC1B;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B,GAAG,OAAO,CAaV"}
@@ -1,18 +1,39 @@
1
- import { DefaultWatcher } from './DefaultWatcher.js';
2
1
  import { ForkWatcher } from './ForkWatcher.js';
3
- import { ParcelWatcher } from './ParcelWatcher.js';
2
+ import { applyPolicies } from './applyPolicies.js';
3
+ import { createWatcherInternal } from './createWatcherInternal.js';
4
4
  /**
5
- * Create a file watcher. By default, the watcher will run in the main thread.
6
- * Use `{ type: 'fork' }` to run the watcher in a forked process.
5
+ * Create a file watcher with optional policies.
6
+ *
7
+ * Policies are applied in a fixed order:
8
+ * 1. Base watcher (DefaultWatcher, ParcelWatcher, or ForkWatcher)
9
+ * 2. shared-root - Multiplexes many watches into one root watch
10
+ * 3. debounce - Batches file change notifications
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * // Simple watcher
15
+ * const watcher = createWatcher();
16
+ *
17
+ * // Monorepo with shared root and debouncing
18
+ * const watcher = createWatcher({
19
+ * type: 'fork',
20
+ * backend: 'parcel',
21
+ * policies: {
22
+ * 'shared-root': { rootPath: '/monorepo' },
23
+ * 'debounce': { debounceMs: 10 }
24
+ * }
25
+ * });
26
+ * ```
7
27
  */
8
28
  export function createWatcher(options) {
9
- const { type = 'default', backend = 'default' } = options ?? {};
29
+ const { type = 'default', backend = 'default', policies } = options ?? {};
30
+ // Fork type: policies applied in child process
10
31
  if (type === 'fork') {
11
- return new ForkWatcher(backend);
32
+ return new ForkWatcher(backend, policies);
12
33
  }
13
- if (backend === 'parcel') {
14
- return new ParcelWatcher();
15
- }
16
- return new DefaultWatcher();
34
+ // Non-fork: create base watcher and apply policies in parent
35
+ const watcher = createWatcherInternal(backend);
36
+ // Apply policies in order
37
+ return applyPolicies(watcher, policies);
17
38
  }
18
39
  //# sourceMappingURL=createWatcher.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"createWatcher.js","sourceRoot":"","sources":["../src/createWatcher.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGnD;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,OAa7B;IACC,MAAM,EAAE,IAAI,GAAG,SAAS,EAAE,OAAO,GAAG,SAAS,EAAE,GAAG,OAAO,IAAI,EAAE,CAAC;IAEhE,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QACpB,OAAO,IAAI,WAAW,CAAC,OAAO,CAAC,CAAC;IAClC,CAAC;IAED,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;QACzB,OAAO,IAAI,aAAa,EAAE,CAAC;IAC7B,CAAC;IAED,OAAO,IAAI,cAAc,EAAE,CAAC;AAC9B,CAAC","sourcesContent":["import { DefaultWatcher } from './DefaultWatcher.js';\nimport { ForkWatcher } from './ForkWatcher.js';\nimport { ParcelWatcher } from './ParcelWatcher.js';\nimport type { Backend, Watcher } from './types/Watcher.js';\n\n/**\n * Create a file watcher. By default, the watcher will run in the main thread.\n * Use `{ type: 'fork' }` to run the watcher in a forked process.\n */\nexport function createWatcher(options?: {\n /**\n * By default, the watcher will run in the main thread.\n * Specify `'fork'` to run the watcher in a forked process.\n * @default 'default'\n */\n type?: 'default' | 'fork';\n /**\n * By default, the watcher will use chokidar.\n * Specify `'parcel'` to use parcel as the file watching backend.\n * @default 'default'\n */\n backend?: Backend;\n}): Watcher {\n const { type = 'default', backend = 'default' } = options ?? {};\n\n if (type === 'fork') {\n return new ForkWatcher(backend);\n }\n\n if (backend === 'parcel') {\n return new ParcelWatcher();\n }\n\n return new DefaultWatcher();\n}\n"]}
1
+ {"version":3,"file":"createWatcher.js","sourceRoot":"","sources":["../src/createWatcher.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAInE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,aAAa,CAAC,OAyB7B;IACC,MAAM,EAAE,IAAI,GAAG,SAAS,EAAE,OAAO,GAAG,SAAS,EAAE,QAAQ,EAAE,GAAG,OAAO,IAAI,EAAE,CAAC;IAE1E,+CAA+C;IAC/C,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QACpB,OAAO,IAAI,WAAW,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAC5C,CAAC;IAED,6DAA6D;IAC7D,MAAM,OAAO,GAAG,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAE/C,0BAA0B;IAC1B,OAAO,aAAa,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;AAC1C,CAAC","sourcesContent":["import { ForkWatcher } from './ForkWatcher.js';\nimport { applyPolicies } from './applyPolicies.js';\nimport { createWatcherInternal } from './createWatcherInternal.js';\nimport type { Backend, Watcher } from './types/Watcher.js';\nimport type { WatcherPolicies } from './types/WatcherPolicy.js';\n\n/**\n * Create a file watcher with optional policies.\n *\n * Policies are applied in a fixed order:\n * 1. Base watcher (DefaultWatcher, ParcelWatcher, or ForkWatcher)\n * 2. shared-root - Multiplexes many watches into one root watch\n * 3. debounce - Batches file change notifications\n *\n * @example\n * ```typescript\n * // Simple watcher\n * const watcher = createWatcher();\n *\n * // Monorepo with shared root and debouncing\n * const watcher = createWatcher({\n * type: 'fork',\n * backend: 'parcel',\n * policies: {\n * 'shared-root': { rootPath: '/monorepo' },\n * 'debounce': { debounceMs: 10 }\n * }\n * });\n * ```\n */\nexport function createWatcher(options?: {\n /**\n * By default, the watcher will run in the main thread.\n * Specify `'fork'` to run the watcher in a forked process.\n *\n * When using 'fork', policies are applied inside the child process\n * for better performance and reduced IPC overhead.\n *\n * @default 'default'\n */\n type?: 'default' | 'fork';\n /**\n * By default, the watcher will use chokidar.\n * Specify `'parcel'` to use parcel as the file watching backend.\n * @default 'default'\n */\n backend?: Backend;\n /**\n * Optional policies to enhance the watcher.\n * Policies are applied in order: shared-root → debounce\n *\n * For 'fork' type: policies are applied in the child process.\n * For 'default' type: policies are applied in the parent process.\n */\n policies?: WatcherPolicies;\n}): Watcher {\n const { type = 'default', backend = 'default', policies } = options ?? {};\n\n // Fork type: policies applied in child process\n if (type === 'fork') {\n return new ForkWatcher(backend, policies);\n }\n\n // Non-fork: create base watcher and apply policies in parent\n const watcher = createWatcherInternal(backend);\n\n // Apply policies in order\n return applyPolicies(watcher, policies);\n}\n"]}
@@ -0,0 +1,3 @@
1
+ import type { Backend, Watcher } from './types/Watcher.js';
2
+ export declare function createWatcherInternal(backend?: Backend): Watcher;
3
+ //# sourceMappingURL=createWatcherInternal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createWatcherInternal.d.ts","sourceRoot":"","sources":["../src/createWatcherInternal.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAE3D,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,OAAmB,GAAG,OAAO,CAE3E"}
@@ -0,0 +1,6 @@
1
+ import { DefaultWatcher } from './DefaultWatcher.js';
2
+ import { ParcelWatcher } from './ParcelWatcher.js';
3
+ export function createWatcherInternal(backend = 'default') {
4
+ return backend === 'parcel' ? new ParcelWatcher() : new DefaultWatcher();
5
+ }
6
+ //# sourceMappingURL=createWatcherInternal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createWatcherInternal.js","sourceRoot":"","sources":["../src/createWatcherInternal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGnD,MAAM,UAAU,qBAAqB,CAAC,UAAmB,SAAS;IAChE,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,aAAa,EAAE,CAAC,CAAC,CAAC,IAAI,cAAc,EAAE,CAAC;AAC3E,CAAC","sourcesContent":["import { DefaultWatcher } from './DefaultWatcher.js';\nimport { ParcelWatcher } from './ParcelWatcher.js';\nimport type { Backend, Watcher } from './types/Watcher.js';\n\nexport function createWatcherInternal(backend: Backend = 'default'): Watcher {\n return backend === 'parcel' ? new ParcelWatcher() : new DefaultWatcher();\n}\n"]}
package/lib/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
- export type { Watcher, WatchOptions } from './types/Watcher.js';
1
+ export type { Watcher, WatchOptions, Backend } from './types/Watcher.js';
2
+ export type { WatcherPolicies } from './types/WatcherPolicy.js';
2
3
  export { createWatcher } from './createWatcher.js';
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AACzE,YAAY,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC"}
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC","sourcesContent":["export type { Watcher, WatchOptions } from './types/Watcher.js';\nexport { createWatcher } from './createWatcher.js';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC","sourcesContent":["export type { Watcher, WatchOptions, Backend } from './types/Watcher.js';\nexport type { WatcherPolicies } from './types/WatcherPolicy.js';\nexport { createWatcher } from './createWatcher.js';\n"]}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Policies that can be applied to a watcher to add functionality.
3
+ * Each policy is optional and can only be specified once.
4
+ */
5
+ export interface WatcherPolicies {
6
+ /**
7
+ * Wraps the watcher with SharedRootWatcher to multiplex many watch paths
8
+ * into a single root watch. Recommended for monorepos with 100+ packages.
9
+ */
10
+ 'shared-root'?: {
11
+ /** Root path to watch (typically the monorepo root) */
12
+ rootPath: string;
13
+ };
14
+ /**
15
+ * Wraps the watcher with DebouncedWatcher to batch file change notifications.
16
+ * First change is immediate (0ms latency), subsequent rapid changes are batched.
17
+ */
18
+ debounce?: {
19
+ /** Debounce delay in milliseconds (default: 10ms) */
20
+ debounceMs?: number;
21
+ };
22
+ }
23
+ //# sourceMappingURL=WatcherPolicy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WatcherPolicy.d.ts","sourceRoot":"","sources":["../../src/types/WatcherPolicy.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,aAAa,CAAC,EAAE;QACd,uDAAuD;QACvD,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IAEF;;;OAGG;IACH,QAAQ,CAAC,EAAE;QACT,qDAAqD;QACrD,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;CACH"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=WatcherPolicy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WatcherPolicy.js","sourceRoot":"","sources":["../../src/types/WatcherPolicy.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Policies that can be applied to a watcher to add functionality.\n * Each policy is optional and can only be specified once.\n */\nexport interface WatcherPolicies {\n /**\n * Wraps the watcher with SharedRootWatcher to multiplex many watch paths\n * into a single root watch. Recommended for monorepos with 100+ packages.\n */\n 'shared-root'?: {\n /** Root path to watch (typically the monorepo root) */\n rootPath: string;\n };\n\n /**\n * Wraps the watcher with DebouncedWatcher to batch file change notifications.\n * First change is immediate (0ms latency), subsequent rapid changes are batched.\n */\n debounce?: {\n /** Debounce delay in milliseconds (default: 10ms) */\n debounceMs?: number;\n };\n}\n"]}
@@ -1,12 +1,27 @@
1
- import { DefaultWatcher } from './DefaultWatcher.js';
2
- import { ParcelWatcher } from './ParcelWatcher.js';
1
+ import { applyPolicies } from './applyPolicies.js';
2
+ import { createWatcherInternal } from './createWatcherInternal.js';
3
3
  /**
4
4
  * `ForkWatcher` runs this file to initialize the watcher in a child process.
5
+ *
6
+ * Policies (shared-root, debounce) are applied here in the child process to reduce
7
+ * IPC overhead and improve performance.
5
8
  */
6
9
  function initializeWatcher() {
7
- // Get backend from command line arguments
10
+ // Get backend and policies from command line arguments
8
11
  const backend = process.argv[2];
9
- const watcher = backend === 'parcel' ? new ParcelWatcher() : new DefaultWatcher();
12
+ const policiesJson = process.argv[3] || '{}';
13
+ let policies;
14
+ try {
15
+ policies = JSON.parse(policiesJson);
16
+ }
17
+ catch (error) {
18
+ console.error('Failed to parse policies in fork:', error);
19
+ policies = {};
20
+ }
21
+ // 1. Create base watcher
22
+ let watcher = createWatcherInternal(backend);
23
+ // 2. Apply policies in order (same as createWatcher)
24
+ watcher = applyPolicies(watcher, policies);
10
25
  process.on('message', (message) => {
11
26
  (async () => {
12
27
  switch (message.type) {
@@ -1 +1 @@
1
- {"version":3,"file":"watcherForkWrapper.js","sourceRoot":"","sources":["../src/watcherForkWrapper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAInD;;GAEG;AACH,SAAS,iBAAiB;IACxB,0CAA0C;IAC1C,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAwB,CAAC;IAEvD,MAAM,OAAO,GAAG,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,aAAa,EAAE,CAAC,CAAC,CAAC,IAAI,cAAc,EAAE,CAAC;IAElF,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,OAAsB,EAAE,EAAE;QAC/C,CAAC,KAAK,IAAI,EAAE;YACV,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;gBACrB,KAAK,OAAO;oBACV,MAAM,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,YAAY,EAAE,EAAE;wBACpD,WAAW,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,YAAY,EAAE,CAAC,CAAC;oBACxE,CAAC,CAAC,CAAC;oBACH,MAAM;gBAER,KAAK,SAAS;oBACZ,MAAM,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;oBAClC,MAAM;gBAER,KAAK,UAAU;oBACb,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;oBACxB,uGAAuG;oBACvG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACpB,CAAC;QACH,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;YACnB,IAAI,CAAC;gBACH,MAAM,YAAY,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC5E,WAAW,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC,CAAC;YACtD,CAAC;YAAC,OAAO,SAAS,EAAE,CAAC;gBACnB,OAAO,CAAC,KAAK,CAAC,+BAA+B,EAAE,SAAS,CAAC,CAAC;gBAC1D,OAAO,CAAC,KAAK,CAAC,iBAAiB,EAAE,KAAK,CAAC,CAAC;YAC1C,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED,yCAAyC;AACzC,SAAS,WAAW,CAAC,OAAqB;IACxC,OAAO,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,CAAC;AAC1B,CAAC;AAED,gDAAgD;AAChD,2EAA2E;AAC3E,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;AAE/B,iBAAiB,EAAE,CAAC","sourcesContent":["import { DefaultWatcher } from './DefaultWatcher.js';\nimport { ParcelWatcher } from './ParcelWatcher.js';\nimport type { Backend } from './types/Watcher.js';\nimport type { ChildMessage, ParentMessage } from './types/WatchMessages.js';\n\n/**\n * `ForkWatcher` runs this file to initialize the watcher in a child process.\n */\nfunction initializeWatcher() {\n // Get backend from command line arguments\n const backend = process.argv[2] as Backend | undefined;\n\n const watcher = backend === 'parcel' ? new ParcelWatcher() : new DefaultWatcher();\n\n process.on('message', (message: ParentMessage) => {\n (async () => {\n switch (message.type) {\n case 'watch':\n await watcher.watch(message.options, (changedPaths) => {\n sendMessage({ type: 'change', id: message.options.id, changedPaths });\n });\n break;\n\n case 'unwatch':\n await watcher.unwatch(message.id);\n break;\n\n case 'shutdown':\n await watcher.dispose();\n // eslint-disable-next-line no-restricted-properties -- rule isn't relevant for exiting a child process\n process.exit(0);\n }\n })().catch((error) => {\n try {\n const errorMessage = error instanceof Error ? error.message : String(error);\n sendMessage({ type: 'error', error: errorMessage });\n } catch (sendError) {\n console.error('Failed to send error message:', sendError);\n console.error('Original error:', error);\n }\n });\n });\n}\n\n/** Typed wrapper for `process.send()` */\nfunction sendMessage(message: ChildMessage) {\n process.send?.(message);\n}\n\n// Prevent the process from exiting immediately.\n// This is handled by the parent process instead with the shutdown message.\nprocess.on('SIGINT', () => {});\n\ninitializeWatcher();\n"]}
1
+ {"version":3,"file":"watcherForkWrapper.js","sourceRoot":"","sources":["../src/watcherForkWrapper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAKnE;;;;;GAKG;AACH,SAAS,iBAAiB;IACxB,uDAAuD;IACvD,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAwB,CAAC;IACvD,MAAM,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;IAE7C,IAAI,QAAyB,CAAC;IAC9B,IAAI,CAAC;QACH,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAoB,CAAC;IACzD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,mCAAmC,EAAE,KAAK,CAAC,CAAC;QAC1D,QAAQ,GAAG,EAAE,CAAC;IAChB,CAAC;IAED,yBAAyB;IACzB,IAAI,OAAO,GAAY,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAEtD,qDAAqD;IACrD,OAAO,GAAG,aAAa,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAE3C,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,OAAsB,EAAE,EAAE;QAC/C,CAAC,KAAK,IAAI,EAAE;YACV,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;gBACrB,KAAK,OAAO;oBACV,MAAM,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,YAAY,EAAE,EAAE;wBACpD,WAAW,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,YAAY,EAAE,CAAC,CAAC;oBACxE,CAAC,CAAC,CAAC;oBACH,MAAM;gBAER,KAAK,SAAS;oBACZ,MAAM,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;oBAClC,MAAM;gBAER,KAAK,UAAU;oBACb,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;oBACxB,uGAAuG;oBACvG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACpB,CAAC;QACH,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;YACnB,IAAI,CAAC;gBACH,MAAM,YAAY,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC5E,WAAW,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC,CAAC;YACtD,CAAC;YAAC,OAAO,SAAS,EAAE,CAAC;gBACnB,OAAO,CAAC,KAAK,CAAC,+BAA+B,EAAE,SAAS,CAAC,CAAC;gBAC1D,OAAO,CAAC,KAAK,CAAC,iBAAiB,EAAE,KAAK,CAAC,CAAC;YAC1C,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED,yCAAyC;AACzC,SAAS,WAAW,CAAC,OAAqB;IACxC,OAAO,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,CAAC;AAC1B,CAAC;AAED,gDAAgD;AAChD,2EAA2E;AAC3E,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;AAE/B,iBAAiB,EAAE,CAAC","sourcesContent":["import { applyPolicies } from './applyPolicies.js';\nimport { createWatcherInternal } from './createWatcherInternal.js';\nimport type { Backend, Watcher } from './types/Watcher.js';\nimport type { WatcherPolicies } from './types/WatcherPolicy.js';\nimport type { ChildMessage, ParentMessage } from './types/WatchMessages.js';\n\n/**\n * `ForkWatcher` runs this file to initialize the watcher in a child process.\n *\n * Policies (shared-root, debounce) are applied here in the child process to reduce\n * IPC overhead and improve performance.\n */\nfunction initializeWatcher() {\n // Get backend and policies from command line arguments\n const backend = process.argv[2] as Backend | undefined;\n const policiesJson = process.argv[3] || '{}';\n\n let policies: WatcherPolicies;\n try {\n policies = JSON.parse(policiesJson) as WatcherPolicies;\n } catch (error) {\n console.error('Failed to parse policies in fork:', error);\n policies = {};\n }\n\n // 1. Create base watcher\n let watcher: Watcher = createWatcherInternal(backend);\n\n // 2. Apply policies in order (same as createWatcher)\n watcher = applyPolicies(watcher, policies);\n\n process.on('message', (message: ParentMessage) => {\n (async () => {\n switch (message.type) {\n case 'watch':\n await watcher.watch(message.options, (changedPaths) => {\n sendMessage({ type: 'change', id: message.options.id, changedPaths });\n });\n break;\n\n case 'unwatch':\n await watcher.unwatch(message.id);\n break;\n\n case 'shutdown':\n await watcher.dispose();\n // eslint-disable-next-line no-restricted-properties -- rule isn't relevant for exiting a child process\n process.exit(0);\n }\n })().catch((error) => {\n try {\n const errorMessage = error instanceof Error ? error.message : String(error);\n sendMessage({ type: 'error', error: errorMessage });\n } catch (sendError) {\n console.error('Failed to send error message:', sendError);\n console.error('Original error:', error);\n }\n });\n });\n}\n\n/** Typed wrapper for `process.send()` */\nfunction sendMessage(message: ChildMessage) {\n process.send?.(message);\n}\n\n// Prevent the process from exiting immediately.\n// This is handled by the parent process instead with the shutdown message.\nprocess.on('SIGINT', () => {});\n\ninitializeWatcher();\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ms-cloudpack/file-watcher",
3
- "version": "0.4.19",
3
+ "version": "0.4.20",
4
4
  "description": "A file watcher abstraction for use with Cloudpack.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -15,15 +15,18 @@
15
15
  }
16
16
  },
17
17
  "dependencies": {
18
- "@ms-cloudpack/path-utilities": "^3.1.32",
18
+ "@ms-cloudpack/path-string-parsing": "^1.2.7",
19
+ "@ms-cloudpack/path-utilities": "^3.1.33",
19
20
  "@parcel/watcher": "^2.5.1",
20
- "chokidar": "^3.5.3"
21
+ "chokidar": "^3.5.3",
22
+ "picomatch": "^4.0.3"
21
23
  },
22
24
  "devDependencies": {
23
25
  "@ms-cloudpack/environment": "*",
24
26
  "@ms-cloudpack/eslint-plugin-internal": "^0.0.1",
25
27
  "@ms-cloudpack/scripts": "^0.0.1",
26
- "@ms-cloudpack/test-utilities": "^0.5.0"
28
+ "@ms-cloudpack/test-utilities": "^0.5.0",
29
+ "@types/picomatch": "^4"
27
30
  },
28
31
  "scripts": {
29
32
  "api": "cloudpack-scripts api",