-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathFEngine.cs
More file actions
590 lines (564 loc) · 31.7 KB
/
FEngine.cs
File metadata and controls
590 lines (564 loc) · 31.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
/*
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Author: Michael J. Froelich
*/
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using VroomJs;
namespace FAP
{
/// <summary>
/// Engine class for rendering JSX files to HTML and for transforming JSX files to a more standardised script
/// Please try not to instanstiate this class too much. Debug code within here for those working closely with the Js Engine
/// </summary>
public class FEngine
{
/// <summary>
/// Will either load a script from this path if a path is stored
/// or it will search around the binary for a script with this in its filename
/// Defaults are in this order: Babel, requirejs (but will not flag an error if missing)
/// </summary>
public List<string> BabelScriptPaths { get; set; } = new List<string>(new string[] { "babel" });
/// <summary>
/// Will either load a script from this path if a path is stored or it will search around the binary for a script with this in its filename
/// These scripts will not update on runtime but will update all instances when accessed
/// Defaults are in this order: React, ReactDOMServer, requirejs (but will not flag an error if missing)
/// </summary>
public List<string> ReactScriptPaths { get; set; } = new List<string>(new string[] { "react", "react-dom-server" });
/// <summary>
/// JsPool of instances used to transform script, consume by using(var engine = BabelPool.GetContext()) {
/// Contains Babel
/// Common functions are engine.Execute and engine.Set/engine.Get variables
/// </summary>
public static JsPool BabelPool;
/// <summary>
/// JsPool of instances used to render HTML, consume by using(var engine = ReactPool.GetContext()) {
/// Contains React, React-DOM-Server and anything included as a "mock" script
/// Common functions are engine.Execute and engine.Set/engine.Get variables
/// </summary>
public static JsPool ReactPool;
public List<string> BlankScriptPaths { get; set; } = new List<string>();
/// <summary>
/// Originally intended to be a sandbox for debugging and unit testing, but has become a deliberately blank engine intended for the ScriptedPage class
/// </summary>
public static JsPool BlankPool;
private string blankscript;
private string blankScriptGet() => blankscript ?? String.Empty;
private string babelscript;
private string babelScriptGet() => babelscript ?? String.Empty; //Used to dynamically get the complete scripts
private string reactscript;
private string reactScriptGet() => reactscript ?? String.Empty;
public static string JsFolder { get; set; } = Directory.GetCurrentDirectory().ToString();
/// <summary>
/// Whatever you give me, know that the purpose of this entire class is to run babel.js
/// A word of caution to the lazy, someone who expects you to forget will begin to expect
/// Consequences.
/// </summary>
/// <param name="babelScriptPaths">Scripts run between loading Babel and performing a Transform function from Babel</param>
/// <param name="reactScriptPaths">Scripts run before performing a Render function from React, finished component scripts etc</param>
/// <param name="blankScriptPaths">Unused to keep memory down, but would have enabled a free sandbox for scripts</param>
public FEngine(List<string> babelScriptPaths = null, List<string> reactScriptPaths = null, List<string> blankScriptPaths = null)
{
string requireisnice = null;// ReactivePage.Search("require.js");
string commonisalsonice = null;// ReactivePage.Search("common.js");
RenderCache = new Dictionary<int, string>();
if (!AssemblyLoader._isLoaded && IsWindows) {
AssemblyLoader.EnsureLoaded(); //The three lines of code too difficult for the officialised system
}
if (reactScriptPaths != null) {
ReactScriptPaths = reactScriptPaths;
if (JsFolder == Directory.GetCurrentDirectory().ToString() && File.Exists(reactScriptPaths.FirstOrDefault())) {
JsFolder = Path.GetDirectoryName(reactScriptPaths[0]); //Get this away from what's usually the executing binary as soon as possible
}
}
else {
for (int i = 0; i < ReactScriptPaths.Count; i++) {
string p;
if ((p = ReactivePage.Search(ReactScriptPaths[i])) == null) {
Console.Error.WriteLine("22: Cannot find essential script " + ReactScriptPaths[i] + "\nEither set the JS folder from ReactivePage.JsFolder or run the ReactivePage.DownloadScripts function.");
}
else
ReactScriptPaths[i] = p;
}
}
if ((requireisnice = ReactivePage.Search("require")) != null && !ReactScriptPaths.Contains("require")) {
ReactScriptPaths.Insert(0, requireisnice); //by default, it's nice to allow require and import like lines, if you're into that and have it around
}
if ((commonisalsonice = ReactivePage.Search("common")) != null && !ReactScriptPaths.Contains("common")) {
ReactScriptPaths.Insert(0, commonisalsonice); //by default, it's nice to allow require and import like lines, if you're into that and have it around
}
if (babelScriptPaths != null)
BabelScriptPaths = babelScriptPaths;
else {
for (int i = 0; i < BabelScriptPaths.Count; i++) {
if ((BabelScriptPaths[i] = ReactivePage.Search(BabelScriptPaths[i])) == null) {
Console.Error.WriteLine("23: Cannot find babel.js this means any script requiring transformation included will throw errors");
}
}
}
if (!string.IsNullOrEmpty(requireisnice) && !ReactScriptPaths.Contains("require.js")) {
BabelScriptPaths.Insert(0, requireisnice);
}
if (!string.IsNullOrEmpty(commonisalsonice) && !ReactScriptPaths.Contains("common.js")) {
BabelScriptPaths.Insert(0, commonisalsonice);
}
if (!string.IsNullOrEmpty(requireisnice) && !BlankScriptPaths.Contains("require.js")) {
BlankScriptPaths.Insert(0, requireisnice);
}
if (!string.IsNullOrEmpty(commonisalsonice) && !BlankScriptPaths.Contains("common.js")) {
BlankScriptPaths.Insert(0, commonisalsonice);
}
if (blankScriptPaths != null)
BlankScriptPaths = blankScriptPaths;
else
for (int i = 0; i < BlankScriptPaths.Count; i++)
BlankScriptPaths[i] = ReactivePage.Search(BlankScriptPaths[i]);
IncludeScript(BlankScriptPaths, false, Machine.Blank);
if (BlankPool == null)
BlankPool = new JsPool(babelScriptGet);
IncludeScript(ReactScriptPaths, false, Machine.React);
IncludeScript(BabelScriptPaths, false, Machine.Babel);
}
/// <summary>
/// Enum used for specifying which machine to add scripts to, adding scripts to the React machine
/// enables validation for scripts passed into that machine whereas adding scripts to the Babel
/// machine enables validation for scripts being transformed. It all depends where you're getting
/// invalidation errors, at program startup or upon connecting with a browser.
/// </summary>
public enum Machine
{
/// <summary>
/// The react render engine, these scripts are run when a user connects
/// </summary>
React,
/// <summary>
/// The babel transformation engine, these scripts are run when loaded so only plain JS is used
/// </summary>
Babel,
/// <summary>
/// Originally meant as a sandbox, now used for Vue and ScriptedPage classes to execute code segments
/// </summary>
Blank
}
/// <summary>
/// Includes or executes these scripts on the startup of either Js Context: the render method or babel transform
/// </summary>
/// <param name="Pathname">Pathname/Path to the script to include</param>
/// <param name="UseRenderMachine">Whether or not to transform the code before passing it into the context</param>
/// <param name="machine">Either FAP.Machine.Render or FAP.Machine.Babel or FAP.Machine.Blank</param>
/// <returns></returns>
public bool IncludeScript(string Pathname, bool UseRenderMachine = false, Machine machine = Machine.React)
{
return IncludeScript(new[] { Pathname }, UseRenderMachine, machine);
}
/// <summary></summary>
/// <param name="Pathname">Pathname/Path to the script to include</param>
/// <param name="UseRenderMachine">Whether or not to transform the code before passing it into the context</param>
/// <param name="machine">Either FAP.Machine.Render or FAP.Machine.Babel or FAP.Machine.Blank</param>
/// <returns></returns>
public bool IncludeScript(IEnumerable<string> Pathname, bool UseRenderMachine = false, Machine machine = Machine.React)
{
List<string> scriptsToWork = null;
List<string> paths = new List<string>();
var inputcopy = Pathname.ToArray(); //IEnumerables aren't nice to work with
foreach (string s in inputcopy)
paths.Add(ReactivePage.Search(s)); //ensures paths
bool HasList = true;
switch (machine) {
case Machine.React:
scriptsToWork = ReactScriptPaths;
break;
case Machine.Babel:
scriptsToWork = BabelScriptPaths;
break;
case Machine.Blank:
scriptsToWork = BlankScriptPaths;
break;
default:
throw new Exception("Not supported");
}
string scripttowork = string.Empty;
foreach (string s in inputcopy) //removes any unensured paths
if (scriptsToWork.Contains(s))
scriptsToWork.Remove(s);
scriptsToWork.AddRange(paths);
paths.ForEach(s => HasList &= File.Exists(s)); //One last very quick check..
if (HasList) {
scripttowork = concatScripts(paths, UseRenderMachine);
switch (machine) {
case Machine.React:
reactscript += scripttowork;
ReactPool = new JsPool(reactScriptGet);
break;
case Machine.Blank:
blankscript += scripttowork;
BlankPool = new JsPool(blankScriptGet);
break;
case Machine.Babel:
babelscript += scripttowork;
BabelPool = new JsPool(babelScriptGet);
break;
}
}
else
throw new Exception("Engine include script error, non existent paths in the script path list of: \n" + JsonConvert.SerializeObject(paths));
return true;
}
private string concatScripts(List<string> scripts, bool please)
{
StringBuilder sb = new StringBuilder();
foreach (string scripttocompile in scripts) {
if (scripttocompile.EndsWith(".jsx") || please)
sb.Append(TransformCode(File.ReadAllText(scripttocompile))).Append("\n;\n");
else
sb.Append(File.ReadAllText(scripttocompile)).Append("\n;\n");
}
return sb.ToString();
}
/// <summary>
/// ID used on the first div element and passed by to the ReactDOM function, default is rootComponent
/// I have only a hazy idea why you'd want to change it
/// </summary>
public string RootComponentId { get; set; } = "rootComponent";
private string GetReactRenderScript(string ComponentName, string props) =>
string.Format("ReactDOMServer.renderToString(React.createElement({0},{1}));", ComponentName, props);
private string GetHtmlRenderScript(string ComponentName, string props) =>
string.Format("ReactDOMServer.renderToStaticMarkup(React.createElement({0},{1}));", ComponentName, props);
/// <summary>
/// Provides one line of script as a string used for attaching the ReactJS engine client side to the RootComponentId
/// </summary>
/// <returns>The java script</returns>
/// <param name="ComponentName">Component name</param>
/// <param name="props">Properties serialised into a string</param>
public string RenderJavaScript(string ComponentName, object props) =>
RenderJavaScript(ComponentName, JsonConvert.SerializeObject(props));
/// <summary>
/// Provides one line of script as a string used for attaching the ReactJS engine client side to the RootComponentId
/// </summary>
/// <returns>The java script</returns>
/// <param name="ComponentName">Component name</param>
/// <param name="props">Properties serialised into a string</param>
public string RenderJavaScript(string ComponentName, string props) =>
string.Format("ReactDOM.render(React.createElement({0},{1}), document.getElementById('{2}'));", ComponentName, props, RootComponentId);
/// <summary>
/// Renders the html.
/// </summary>
/// <returns>The html</returns>
/// <param name="ComponentName">Component name</param>
/// <param name="props">Properties serialised into a string</param>
/// <param name="InputScripts">Input scripts</param>
public string RenderHtml(string ComponentName, string props, IEnumerable<string> InputScripts = null) => RenderHtml(ComponentName, props, false, InputScripts);
/// <summary>
/// Renders the html.
/// </summary>
/// <returns>The html</returns>
/// <param name="ComponentName">Component name</param>
/// <param name="props">Properties as a serialisable object</param>
/// <param name="InputScripts">Input scripts</param>
public string RenderHtml(string ComponentName, object props, IEnumerable<string> InputScripts = null) => RenderHtml(ComponentName, props, false, InputScripts);
/// <param name="HtmlOnly">If true, react information shall not be included</param>
/// <returns></returns>
public string RenderHtml(string ComponentName, object props, bool HtmlOnly = false, IEnumerable<string> InputScripts = null)
{
string sprops = JsonConvert.SerializeObject(props);
return RenderHtml(ComponentName, sprops, HtmlOnly, InputScripts);
}
/// <summary>
/// Renders the html.
/// </summary>
/// <returns>The html</returns>
/// <param name="ComponentName">Component name</param>
/// <param name="props">Properties</param>
/// <param name="HtmlOnly">If set to <c>true</c> html only</param>
/// <param name="InputScripts">Input scripts</param>
public string RenderHtml(string ComponentName, string props, bool HtmlOnly = false, IEnumerable<string> InputScripts = null)
{
ReactivePage.Component dangerousidea;
if (ReactivePage.Component.ComponentRegistry.TryGetValue(ComponentName.ToLower(), out dangerousidea)) {
return RenderHtml(ComponentName, props, HtmlOnly, dangerousidea, InputScripts);
}
return RenderHtml(ComponentName, props, HtmlOnly, null, InputScripts);
}
internal string RenderHtml(string ComponentName, string props, bool HtmlOnly = false, ReactivePage.Component components = null, IEnumerable<string> InputScripts = null)
{
StringBuilder renderBuild = new StringBuilder();
string scriptnameforvroom = "Anonymous";
if (components != null && components.ComponentScriptPathinfo.Count > 0) {
foreach (ReactivePage.Script s in components.ComponentScriptPathinfo.Values) {
if (string.IsNullOrEmpty(s.RenderedComponentScript))
renderBuild.AppendLine(s.ComponentScript);
else
renderBuild.AppendLine(s.RenderedComponentScript);
}
scriptnameforvroom = Path.GetFileName(components.ComponentScriptPathinfo.Last().Value.ScriptPath); //Assumedly, the last script would contain the renderable component
}
if (InputScripts != null) {
foreach (string s in InputScripts)
renderBuild.AppendLine(s);
}
if (HtmlOnly)
renderBuild.Append(GetHtmlRenderScript(ComponentName, props));
else
renderBuild.Append(GetReactRenderScript(ComponentName, props));
string toRender = renderBuild.ToString();
string toReturn;
int hash = toRender.GetHashCode() + props.GetHashCode(); //It's still faster to hash both these and check if something with this script and props has come than to run javascript
if (!RenderCache.TryGetValue(hash, out toReturn)) {
if (!File.Exists(ReactScriptPaths[0]))
throw new Exception("Render HTML exception, cannot find important react scripts");
using (var pool = ReactPool.GetContext()) {
var instance = pool.Instance;
var output = instance.Execute(toRender, scriptnameforvroom);
string html = output as string;
toReturn = string.Format("<div id='{0}'>{1}</div>", RootComponentId, html);
}
RenderCache.Add(hash, toReturn);
}
return toReturn;
}
Dictionary<int, string> RenderCache; //haxxy cache to prevent someone spamming from creating contexes
/// <summary>
/// Minimises the output gained from performing the Transform functions. Default is false.
/// </summary>
public bool MinimiseBabelOutput { get; set; } = true;
public List<object> BabelPresets { get; set; } = new List<object> {
"stage-2",//Saves 10ms against other stages when benchmarking debug, as of writing
"es2015",
"react"/*,
new {
modules = "commonjs" //this isn't necessary, but making it a List<object> opens up these options
}*/
};
/// <summary>
/// Currently unsure how these are used, assumedly including these here and as "mock scripts" from reactive page enables middleware?
/// </summary>
public List<string> BabelPlugins { get; set; } = new List<string>();
/// <summary>
/// Parser options sent to Babylon, what's really transforming code. Set this by making it equal to an anonymous object, such as:
/// ParserOptions = new { allowImportExportEverywhere = true, allowReturnOutsideFunction = true };
/// Which are the defaults, since you'd rather more code than less transforming. Alert me if plugins or presets cease working,
/// it would be this line of code here. Set to null for the Babel/Babylon's true default.
/// </summary>
public object ParserOptions { get; set; } = new { allowImportExportEverywhere = true, allowReturnOutsideFunction = true };
private const string RenderOutputVariable = "_FAP_Render_Output";
private const string RenderInputVariable = "_FAP_Render_Input";
private const string ParserOptionsConst = ", parserOpts: ";
private readonly string TransformCodeScriptDebug = RenderOutputVariable + " = Babel.transform(" + RenderInputVariable + ", { retainLines: true, presets: ";
private readonly string TransformCodeFileDebug = RenderOutputVariable + " = Babel.transformFile(" + RenderInputVariable + ", { retainLines: true, presets: ";
private readonly string TransformCodeScript = RenderOutputVariable + " = Babel.transform(" + RenderInputVariable + ", {minified: true, comments: false, presets: ";
private readonly string TransformCodeFile = RenderOutputVariable + " = Babel.transformFile(" + RenderInputVariable + ", {minified: true, comments: false, presets: ";
private readonly string TransformCodeTrailer = "}).code;";
/// <summary>
/// Calls the internal Babel transform function with Pathname passed in as the first parameter
/// </summary>
/// <param name="Pathname"></param>
/// <returns>Null if failure</returns>
public string TransformFile(string Pathname, string ScriptName = "Anonymous")
{
string toret = null;
if (!File.Exists(BabelScriptPaths[0]))
throw new Exception("Transform code exception, cannot find important babel script");
try {
if (ScriptName == "Anonymous")
ScriptName = Path.GetFileName(Pathname);
using (var instance = BabelPool.GetContext()) {
string plugins = string.Empty;
if (BabelPlugins.Count > 0)
plugins = ", plugins: " + JsonConvert.SerializeObject(BabelPlugins);
instance.Instance.SetVariable(RenderInputVariable, Pathname);
if (!MinimiseBabelOutput)
instance.Instance.Execute(
TransformCodeFileDebug + JsonConvert.SerializeObject(BabelPresets) + plugins +
(ParserOptions != null ? ParserOptionsConst + JsonConvert.SerializeObject(ParserOptions) : string.Empty) +
TransformCodeTrailer, ScriptName);
else
instance.Instance.Execute(
TransformCodeFile + JsonConvert.SerializeObject(BabelPresets) + plugins +
(ParserOptions != null ? ParserOptionsConst + JsonConvert.SerializeObject(ParserOptions) : string.Empty) +
TransformCodeTrailer, ScriptName);
toret = instance.Instance.GetVariable(RenderOutputVariable) as string;
}
}
catch (Exception e) {
Console.Error.WriteLine("20: Babel Transformation Error\n" + e.Message);
}
return toret;
}
public string TransformCode(string code, string ScriptName = "Anonymous")
{//scriptnameforvroom = Path.GetFileName(components.ComponentScriptPathinfo.FirstOrDefault().Value.ScriptPath);
string toret = null;
if (!File.Exists(BabelScriptPaths[0]))
throw new Exception("Transform code exception, cannot find important babel script");
try {
using (var instance = BabelPool.GetContext()) {
string plugins = string.Empty;
if (BabelPlugins.Count > 0)
plugins = ", plugins: " + JsonConvert.SerializeObject(BabelPlugins);
instance.Instance.SetVariable(RenderInputVariable, code);
if (!MinimiseBabelOutput)
instance.Instance.Execute(
TransformCodeScriptDebug + JsonConvert.SerializeObject(BabelPresets) + plugins +
(ParserOptions != null ? ParserOptionsConst + JsonConvert.SerializeObject(ParserOptions) : string.Empty) +
TransformCodeTrailer, ScriptName);
else
instance.Instance.Execute(
TransformCodeScript + JsonConvert.SerializeObject(BabelPresets) + plugins +
(ParserOptions != null ? ParserOptionsConst + JsonConvert.SerializeObject(ParserOptions) : string.Empty) +
TransformCodeTrailer, ScriptName);
toret = instance.Instance.GetVariable(RenderOutputVariable) as string;
}
if (toret.StartsWith("'use strict';"))
toret = toret.Substring("'use strict';".Length);
}
catch (Exception e) {
Console.Error.WriteLine("20: Babel Transformation Error\n" + e.Message);
}
return toret;
}
static bool IsWindows => (Environment.OSVersion.Platform.ToString().StartsWith("W"));
internal string PageBuilder(string ChildHTML, ReactivePage.Component cvars)
{
var output = new StringBuilder(ReactivePage.Component.OPENINGHEADER).Append(cvars.Title).Append(cvars.Metadata).Append(cvars.Style).Append(ReactivePage.Component.CLOSINGHEADER + ChildHTML);
int i = cvars.InternalReact ? 2 : 0; //Horrible hack all because I want more options available via changing boolean properties
for (; i < cvars.Scripts.Count; i++) {
if (!string.IsNullOrEmpty(cvars.Scripts[i]))
output.Append(cvars.Scripts[i]);
}
bool hasBabel = !string.IsNullOrEmpty(cvars.Scripts[2]);
foreach (ReactivePage.Script s in cvars.ComponentScriptPathinfo.Values) {
if (hasBabel && s.isJSX) {
output.Append(ReactivePage.Component.BabelType).Append("\n");
}
else {
output.Append(ReactivePage.Component.RegularType).Append("\n");
}
if (s.isJSX && !hasBabel) {
if (string.IsNullOrEmpty(s.RenderedComponentScript))
s.RenderedComponentScript = TransformCode(s.ComponentScript);//ReactEnvironment.Current.Babel.Transform(s.ComponentScript);
output.Append(s.RenderedComponentScript);
}
else {
output.Append(s.ComponentScript);
}
output.Append("\n\t\t</script>");
}
output.Append(ReactivePage.Component.FOOTER);
return string.Empty;
}
}
public class Poolable : IDisposable
{
public ConcurrentQueue<Poolable> parent;
/// <summary>
/// Lifetime of any poolable objects in seconds.
/// A minute supposes short bursts of use, an hour (3600) supposes long periods of heavy use, 5 seconds for the memory conscious only
/// </summary>
public static int PoolLife { get; set; } = 60;
internal DateTime LastUsed;
public Poolable(JsContext obj, ConcurrentQueue<Poolable> Parent)
{
this.Instance = obj;
this.parent = Parent;
LastUsed = DateTime.UtcNow;
if (JsPool.UseMinimalMemory == true) {
System.Threading.Tasks.Task.Factory.StartNew(Conserve);
}
//parent.Pool.Enqueue(this);
}
internal async void Conserve()
{
while (parent != null) {
await System.Threading.Tasks.Task.Delay(PoolLife * 1000);
if (DateTime.UtcNow.Subtract(LastUsed).Seconds > PoolLife && parent.Count > JsPool.MinSize) {
Poolable throwaway;
parent.TryDequeue(out throwaway);
if (throwaway.Instance is IDisposable)
(throwaway.Instance as IDisposable).Dispose();
throwaway = null;
}
}
}
public JsContext Instance { get; internal set; }
public void Dispose()
{
if (Instance != null && parent.Count < JsPool.MaxSize)
parent.Enqueue(this);
}
}
public class JsPool
{
/// <summary>
/// The maximum number allowable within the queue
/// Default is 100, it's unlikely to ever get that far
/// </summary>
public static int MaxSize { get; set; } = 100; //As of writing, the entire system responds in 10ms which means a max 100 requests a second
/// <summary>
/// The number of JsContexts in the queue, one is default and good for one user at one time
/// Three is ideal for busier sites
/// </summary>
public static int MinSize { get; set; } = 1;
/// <summary>
/// If over the minimum JsContext size and if set to true, JsContexts will begin deleting themselves after PoolLife seconds.
/// </summary>
public static bool UseMinimalMemory = true;
/// <summary>
/// Freely accessible JsPool actual pool, a queue, for mischief
/// </summary>
public ConcurrentQueue<Poolable> Pool;
private Func<JsContext> Generator;
/// <summary>
/// Input is a function that returns a string, this allows the JsPool to dynamically use script strings on regeneration
/// </summary>
/// <param name="StringGenerator"></param>
public JsPool(Func<string> ScriptGenerator)
{
try {
Pool = new ConcurrentQueue<Poolable>();
Generator = () => {
string Script = ScriptGenerator();
var dasengine = new JsEngine(-1, -1);
var newcontext = dasengine.CreateContext();
if (!string.IsNullOrEmpty(Script)) {
newcontext.Execute(Script);
}
return newcontext;
};
}
catch(Exception e) {
throw new Exception("Library parse error review what libraries you use and do not use any that rely on a DOM, " + e.Message,e);
}
Load();
}
private void Load()
{
for (int i = 0; i < MinSize; i++)
Pool.Enqueue(new Poolable(Generator(), Pool));
}
/// <summary>
/// Don't use this. It's an extremely bad habit.
/// </summary>
public JsContext GetInstance => GetContext().Instance;
/// <summary>
/// Literally use this such as using(var Pool = JsPool.GetObject())
/// </summary>
/// <returns></returns>
public Poolable GetContext()
{
if (Pool != null && Pool.Count > 0) {
Poolable output;
if (Pool.TryDequeue(out output))
return output;
}
return new Poolable(Generator(), Pool);
}
}
}