Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/AngleSharp.Js.Tests/InteractionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,30 @@ public async Task RunScriptSnippetDirectlyGetSimpleValueFromCalculation()
Assert.AreEqual(3.0, result);
}

[Test]
public async Task RunSameScriptSourceInSeveralDocumentsKeepsStateSeparate()
{
var html = "<!doctype html><span id=test>Test</span>";
var config = Configuration.Default.WithJs();
var source = "(function () { window.counter = (window.counter || 0) + 1; return window.counter; })()";
var first = await BrowsingContext.New(config).OpenAsync(m => m.Content(html));
var second = await BrowsingContext.New(config).OpenAsync(m => m.Content(html));

Assert.AreEqual(1.0, first.ExecuteScript(source));
Assert.AreEqual(1.0, second.ExecuteScript(source));
Assert.AreEqual(2.0, first.ExecuteScript(source));
Assert.AreEqual(2.0, second.ExecuteScript(source));
}

[Test]
public async Task RunScriptSnippetWithSyntaxErrorThrows()
{
var html = "<!doctype html><span id=test>Test</span>";
var config = Configuration.Default.WithJs();
var document = await BrowsingContext.New(config).OpenAsync(m => m.Content(html));
Assert.Throws<JavaScriptException>(() => document.ExecuteScript("function ("));
}

[Test]
public async Task RunScriptAtPressingLink_Issue47()
{
Expand Down
54 changes: 54 additions & 0 deletions src/AngleSharp.Js/Cache/ScriptCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
namespace AngleSharp.Js.Cache
{
using Acornima.Ast;
using Jint;
using Jint.Runtime;
using System;
using System.Collections.Concurrent;

/// <summary>
/// Caches the parsed and analyzed form of scripts. A prepared script is
/// documented by Jint as reusable and thread-safe, so the same one can serve
/// every document that runs the same source - a page loading a library ends up
/// parsing it once instead of once per document.
/// </summary>
static class ScriptCache
{
// There is no bound on how many distinct scripts a process may see, so the
// cache does not grow without end. The scripts worth keeping are the ones
// seen first: libraries are referenced early and by many documents.
private const Int32 Capacity = 32;

private static readonly ConcurrentDictionary<String, Prepared<Script>> _scripts =
new ConcurrentDictionary<String, Prepared<Script>>(StringComparer.Ordinal);

/// <summary>
/// Gets the prepared form of the given source. An invalid result means the
/// source could not be prepared and should be handed to the engine as text,
/// so that the engine reports the syntax error itself.
/// </summary>
public static Prepared<Script> GetOrCreate(String source)
{
if (_scripts.TryGetValue(source, out var prepared))
{
return prepared;
}

try
{
prepared = Engine.PrepareScript(source);
}
catch (ScriptPreparationException)
{
return default;
}

if (_scripts.Count < Capacity)
{
_scripts.TryAdd(source, prepared);
}

return prepared;
}
}
}
8 changes: 6 additions & 2 deletions src/AngleSharp.Js/EngineInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ namespace AngleSharp.Js
{
using AngleSharp.Dom;
using AngleSharp.Io;
using AngleSharp.Js.Cache;
using AngleSharp.Text;
using Jint;
using Jint.Native;
Expand Down Expand Up @@ -84,7 +85,7 @@ public EngineInstance(IWindow window, IDictionary<String, Object> assignments, I

public ObjectInstance GetDomPrototype(Type type) => _prototypes.GetOrCreate(type, CreatePrototype);

public JsValue RunScript(String source, String type, String sourceUrl, JsValue context)
public JsValue RunScript(String source, String type, String sourceUrl)
{
if (string.IsNullOrEmpty(type))
{
Expand All @@ -95,7 +96,10 @@ public JsValue RunScript(String source, String type, String sourceUrl, JsValue c
{
if (MimeTypeNames.IsJavaScript(type))
{
return _engine.Evaluate(source);
var prepared = ScriptCache.GetOrCreate(source);
// An invalid result means the source did not parse; hand it to the
// engine as text so the syntax error is reported as usual.
return prepared.IsValid ? _engine.Evaluate(prepared) : _engine.Evaluate(source);
}
else if (type.Isi("importmap"))
{
Expand Down
6 changes: 0 additions & 6 deletions src/AngleSharp.Js/Extensions/EngineExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,12 +209,6 @@ public static void AddInstance(this EngineInstance engine, ObjectInstance obj, T
apply.Invoke(engine, obj);
}

public static JsValue RunScript(this EngineInstance engine, String source, String type, String sourceUrl) =>
engine.RunScript(source, type, sourceUrl, engine.Window);

public static JsValue RunScript(this EngineInstance engine, String source, String type, String sourceUrl, INode context) =>
engine.RunScript(source, type, sourceUrl, context.ToJsValue(engine));

public static JsValue Call(this EngineInstance instance, MethodInfo method, JsValue thisObject, JsValue[] arguments)
{
if (method != null)
Expand Down
Loading