Skip to content

Commit f65bfc4

Browse files
committed
Recycle the JavaScript engine pool if any of the loaded files are changed
1 parent ff957fc commit f65bfc4

File tree

2 files changed

+127
-8
lines changed

2 files changed

+127
-8
lines changed

src/React.Tests/Core/JavaScriptEngineFactoryTest.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public class JavaScriptEngineFactoryTest
2020
private JavaScriptEngineFactory CreateFactory()
2121
{
2222
var config = new Mock<IReactSiteConfiguration>();
23+
var fileSystem = new Mock<IFileSystem>();
2324
var registration = new JavaScriptEngineFactory.Registration
2425
{
2526
Factory = () =>
@@ -30,7 +31,7 @@ private JavaScriptEngineFactory CreateFactory()
3031
},
3132
Priority = 1
3233
};
33-
return new JavaScriptEngineFactory(new[] { registration }, config.Object);
34+
return new JavaScriptEngineFactory(new[] { registration }, config.Object, fileSystem.Object);
3435
}
3536

3637
[Test]

src/React/JavaScriptEngineFactory.cs

Lines changed: 125 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Concurrent;
33
using System.Collections.Generic;
44
using System.Diagnostics;
5+
using System.IO;
56
using System.Linq;
67
using System.Threading;
78
using JavaScriptEngineSwitcher.Core;
@@ -20,6 +21,10 @@ public class JavaScriptEngineFactory : IDisposable, IJavaScriptEngineFactory
2021
/// </summary>
2122
protected readonly IReactSiteConfiguration _config;
2223
/// <summary>
24+
/// File system wrapper
25+
/// </summary>
26+
protected readonly IFileSystem _fileSystem;
27+
/// <summary>
2328
/// Function used to create new JavaScript engine instances.
2429
/// </summary>
2530
protected readonly Func<IJsEngine> _factory;
@@ -31,25 +36,84 @@ protected readonly ConcurrentDictionary<int, IJsEngine> _engines
3136
/// <summary>
3237
/// Pool of JavaScript engines to use
3338
/// </summary>
34-
protected readonly IJsPool _pool;
39+
protected IJsPool _pool;
40+
/// <summary>
41+
/// Used to recycle the JavaScript engine pool when relevant JavaScript files are modified.
42+
/// </summary>
43+
protected readonly FileSystemWatcher _watcher;
44+
/// <summary>
45+
/// Names of all the files that are loaded into the JavaScript engine. If any of these
46+
/// files are changed, the engines should be recycled
47+
/// </summary>
48+
protected readonly ISet<string> _watchedFiles;
49+
/// <summary>
50+
/// Timer for debouncing pool recycling
51+
/// </summary>
52+
protected readonly Timer _resetPoolTimer;
53+
/// <summary>
54+
/// Whether this class has been disposed.
55+
/// </summary>
56+
protected bool _disposed;
57+
58+
/// <summary>
59+
/// Time period to debounce file system changed events, in milliseconds.
60+
/// </summary>
61+
protected const int DEBOUNCE_TIMEOUT = 25;
3562

3663
/// <summary>
3764
/// Initializes a new instance of the <see cref="JavaScriptEngineFactory"/> class.
3865
/// </summary>
39-
public JavaScriptEngineFactory(IEnumerable<Registration> availableFactories, IReactSiteConfiguration config)
66+
public JavaScriptEngineFactory(
67+
IEnumerable<Registration> availableFactories,
68+
IReactSiteConfiguration config,
69+
IFileSystem fileSystem
70+
)
4071
{
4172
_config = config;
73+
_fileSystem = fileSystem;
4274
_factory = GetFactory(availableFactories);
4375
if (_config.ReuseJavaScriptEngines)
4476
{
45-
_pool = new JsPool(new JsPoolConfig
77+
_pool = CreatePool();
78+
_resetPoolTimer = new Timer(OnResetPoolTimer);
79+
_watchedFiles = new HashSet<string>(_config.Scripts.Select(
80+
fileName => _fileSystem.MapPath(fileName).ToLowerInvariant()
81+
));
82+
try
4683
{
47-
EngineFactory = _factory,
48-
Initializer = InitialiseEngine,
49-
});
84+
// Attempt to initialise a FileSystemWatcher so we can recycle the JavaScript
85+
// engine pool when files are changed.
86+
_watcher = new FileSystemWatcher
87+
{
88+
Path = _fileSystem.MapPath("~"),
89+
IncludeSubdirectories = true,
90+
EnableRaisingEvents = true,
91+
};
92+
_watcher.Changed += OnFileChanged;
93+
_watcher.Created += OnFileChanged;
94+
_watcher.Deleted += OnFileChanged;
95+
_watcher.Renamed += OnFileChanged;
96+
}
97+
catch (Exception ex)
98+
{
99+
// Can't use FileSystemWatcher (eg. not running in Full Trust)
100+
Trace.WriteLine("Unable to initialise FileSystemWatcher: " + ex.Message);
101+
}
50102
}
51103
}
52104

105+
/// <summary>
106+
/// Creates a new JavaScript engine pool.
107+
/// </summary>
108+
protected virtual IJsPool CreatePool()
109+
{
110+
return new JsPool(new JsPoolConfig
111+
{
112+
EngineFactory = _factory,
113+
Initializer = InitialiseEngine,
114+
});
115+
}
116+
53117
/// <summary>
54118
/// Loads standard React and JSXTransformer scripts into the engine.
55119
/// </summary>
@@ -74,6 +138,7 @@ protected virtual void InitialiseEngine(IJsEngine engine)
74138
/// <returns>The JavaScript engine</returns>
75139
public virtual IJsEngine GetEngineForCurrentThread()
76140
{
141+
EnsureNotDisposed();
77142
return _engines.GetOrAdd(Thread.CurrentThread.ManagedThreadId, id =>
78143
{
79144
var engine = _factory();
@@ -103,6 +168,7 @@ public virtual void DisposeEngineForCurrentThread()
103168
/// <returns>The JavaScript engine</returns>
104169
public virtual IJsEngine GetEngine()
105170
{
171+
EnsureNotDisposed();
106172
return _pool.GetEngine();
107173
}
108174

@@ -112,7 +178,12 @@ public virtual IJsEngine GetEngine()
112178
/// <param name="engine">Engine to return</param>
113179
public virtual void ReturnEngineToPool(IJsEngine engine)
114180
{
115-
_pool.ReturnEngineToPool(engine);
181+
// This could be called from ReactEnvironment.Dispose if that class is disposed after
182+
// this class. Let's just ignore this if it's disposed.
183+
if (!_disposed)
184+
{
185+
_pool.ReturnEngineToPool(engine);
186+
}
116187
}
117188

118189
/// <summary>
@@ -161,13 +232,60 @@ private static Func<IJsEngine> GetFactory(IEnumerable<Registration> availableFac
161232
/// </summary>
162233
public virtual void Dispose()
163234
{
235+
_disposed = true;
164236
foreach (var engine in _engines)
165237
{
166238
if (engine.Value != null)
167239
{
168240
engine.Value.Dispose();
169241
}
170242
}
243+
if (_pool != null)
244+
{
245+
_pool.Dispose();
246+
_pool = null;
247+
}
248+
}
249+
250+
/// <summary>
251+
/// Handles events fired when any files are changed.
252+
/// </summary>
253+
/// <param name="sender">The sender</param>
254+
/// <param name="args">The <see cref="FileSystemEventArgs"/> instance containing the event data</param>
255+
protected virtual void OnFileChanged(object sender, FileSystemEventArgs args)
256+
{
257+
if (_watchedFiles.Contains(args.FullPath.ToLowerInvariant()))
258+
{
259+
// Use a timer so multiple changes only result in a single reset.
260+
_resetPoolTimer.Change(DEBOUNCE_TIMEOUT, Timeout.Infinite);
261+
}
262+
}
263+
264+
/// <summary>
265+
/// Called when any of the watched files have changed. Recycles the JavaScript engine pool
266+
/// so the files are all reloaded.
267+
/// </summary>
268+
/// <param name="state">Unused</param>
269+
protected virtual void OnResetPoolTimer(object state)
270+
{
271+
// Create the new pool before disposing the old pool so that _pool is never null.
272+
var oldPool = _pool;
273+
_pool = CreatePool();
274+
if (oldPool != null)
275+
{
276+
oldPool.Dispose();
277+
}
278+
}
279+
280+
/// <summary>
281+
/// Ensures that this object has not been disposed.
282+
/// </summary>
283+
public void EnsureNotDisposed()
284+
{
285+
if (_disposed)
286+
{
287+
throw new ObjectDisposedException(GetType().Name);
288+
}
171289
}
172290

173291
/// <summary>

0 commit comments

Comments
 (0)