-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathBaristaModuleRecord.cs
319 lines (280 loc) · 14 KB
/
BaristaModuleRecord.cs
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
namespace BaristaLabs.BaristaCore
{
using BaristaLabs.BaristaCore.JavaScript;
using BaristaLabs.BaristaCore.ModuleLoaders;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
/// <summary>
/// Represents a JavaScript Module
/// </summary>
public sealed class BaristaModuleRecord : BaristaObject<JavaScriptModuleRecord>
{
private readonly string m_name;
private readonly JavaScriptValueSafeHandle m_moduleSpecifier;
private readonly BaristaContext m_context;
private readonly IBaristaModuleRecordFactory m_moduleRecordFactory;
private readonly IBaristaModuleLoader m_moduleLoader;
private readonly GCHandle m_fetchImportedModuleCallbackHandle = default(GCHandle);
private readonly GCHandle m_notifyCallbackHandle = default(GCHandle);
private readonly Dictionary<string, BaristaModuleRecord> m_importedModules = new Dictionary<string, BaristaModuleRecord>();
private readonly BaristaModuleRecord m_parentModule;
private readonly GCHandle m_beforeCollectCallbackDelegateHandle;
public BaristaModuleRecord(string name, JavaScriptValueSafeHandle moduleSpecifier, BaristaModuleRecord parentModule, IJavaScriptEngine engine, BaristaContext context, IBaristaModuleRecordFactory moduleRecordFactory, IBaristaModuleLoader moduleLoader, JavaScriptModuleRecord moduleRecord)
: base(engine, moduleRecord)
{
m_name = name ?? throw new ArgumentNullException(nameof(name));
m_moduleSpecifier = moduleSpecifier ?? throw new ArgumentNullException(nameof(moduleSpecifier));
m_parentModule = parentModule;
m_context = context ?? throw new ArgumentNullException(nameof(context));
m_moduleRecordFactory = moduleRecordFactory ?? throw new ArgumentNullException(nameof(moduleRecordFactory));
//Module loader is not required, but if not specified, imports will fail.
m_moduleLoader = moduleLoader;
//Associate functions that will handle module loading
if (m_parentModule == null)
{
//Set the fetch module callback for the module.
m_fetchImportedModuleCallbackHandle = InitFetchImportedModuleCallback(Handle);
//Set the notify callback for the module.
m_notifyCallbackHandle = InitNotifyModuleReadyCallback(Handle);
}
//Set the event that will be called prior to the engine collecting the context.
JavaScriptObjectBeforeCollectCallback beforeCollectCallback = (IntPtr handle, IntPtr callbackState) =>
{
OnBeforeCollect(handle, callbackState);
};
m_beforeCollectCallbackDelegateHandle = GCHandle.Alloc(beforeCollectCallback);
Engine.JsSetObjectBeforeCollectCallback(moduleRecord, IntPtr.Zero, beforeCollectCallback);
}
private GCHandle InitFetchImportedModuleCallback(JavaScriptModuleRecord moduleRecord)
{
JavaScriptFetchImportedModuleCallback fetchImportedModule = (IntPtr referencingModule, IntPtr specifier, out IntPtr dependentModule) =>
{
try
{
return FetchImportedModule(new JavaScriptModuleRecord(referencingModule), new JavaScriptValueSafeHandle(specifier), out dependentModule);
}
catch (Exception ex)
{
if (Engine.JsHasException() == false)
{
Engine.JsSetException(Context.CreateError(ex.Message).Handle);
}
dependentModule = referencingModule;
return true;
}
};
var handle = GCHandle.Alloc(fetchImportedModule);
IntPtr fetchCallbackPtr = Marshal.GetFunctionPointerForDelegate(handle.Target);
Engine.JsSetModuleHostInfo(moduleRecord, JavaScriptModuleHostInfoKind.FetchImportedModuleCallback, fetchCallbackPtr);
Engine.JsSetModuleHostInfo(moduleRecord, JavaScriptModuleHostInfoKind.FetchImportedModuleFromScriptCallback, fetchCallbackPtr);
return handle;
}
private GCHandle InitNotifyModuleReadyCallback(JavaScriptModuleRecord moduleRecord)
{
JavaScriptNotifyModuleReadyCallback moduleNotifyCallback = (IntPtr referencingModule, IntPtr exceptionVar) =>
{
if (exceptionVar != IntPtr.Zero)
{
if (!Engine.JsHasException())
{
Engine.JsSetException(new JavaScriptValueSafeHandle(exceptionVar));
}
return true;
}
IsReady = true;
return false;
};
var handle = GCHandle.Alloc(moduleNotifyCallback);
IntPtr notifyCallbackPtr = Marshal.GetFunctionPointerForDelegate(handle.Target);
Engine.JsSetModuleHostInfo(moduleRecord, JavaScriptModuleHostInfoKind.NotifyModuleReadyCallback, notifyCallbackPtr);
return handle;
}
#region Properties
/// <summary>
/// Gets a value that indicates if the module's notify ready callback has been called.
/// </summary>
public bool IsReady
{
get;
private set;
}
/// <summary>
/// Gets the name of the module.
/// </summary>
public string Name
{
get { return m_name; }
}
/// <summary>
/// Gets the handle of the module specifier.
/// </summary>
public JavaScriptValueSafeHandle Specifier
{
get { return m_moduleSpecifier; }
}
private BaristaContext Context
{
get { return m_context; }
}
#endregion
public JsError ParseModuleSource(string script)
{
var scriptBuffer = Encoding.UTF8.GetBytes(script);
var parseResultHandle = Engine.JsParseModuleSource(Handle, JavaScriptSourceContext.GetNextSourceContext(), scriptBuffer, (uint)scriptBuffer.Length, JavaScriptParseModuleSourceFlags.DataIsUTF8);
return Context.CreateValue<JsError>(parseResultHandle);
}
private bool FetchImportedModule(JavaScriptModuleRecord jsReferencingModuleRecord, JavaScriptValueSafeHandle specifier, out IntPtr dependentModule)
{
var moduleName = Context.CreateValue(specifier).ToString();
var referencingModuleRecord = m_moduleRecordFactory.GetBaristaModuleRecord(jsReferencingModuleRecord);
//If the current module name is equal to the fetching module name, return this value.
if (Name == moduleName || referencingModuleRecord != null && referencingModuleRecord.Name == moduleName)
{
//Top-level self-referencing module. Reference itself.
dependentModule = jsReferencingModuleRecord.DangerousGetHandle();
return false;
}
else if (m_importedModules.ContainsKey(moduleName))
{
//The module has already been imported, return the existing JavaScriptModuleRecord
dependentModule = m_importedModules[moduleName].Handle.DangerousGetHandle();
return false;
}
else if (m_moduleLoader != null)
{
Task<IBaristaModule> moduleLoaderTask = null;
try
{
moduleLoaderTask = m_moduleLoader.GetModule(moduleName);
}
catch (Exception ex)
{
var error = Context.CreateError($"An error occurred while attempting to load a module named {moduleName}: {ex.Message}");
Engine.JsSetException(error.Handle);
dependentModule = jsReferencingModuleRecord.DangerousGetHandle();
return true;
}
if (moduleLoaderTask != null)
{
IBaristaModule module;
try
{
module = moduleLoaderTask.GetAwaiter().GetResult();
}
catch (Exception ex)
{
var error = Context.CreateError($"An error occurred while attempting to load a module named {moduleName}: {ex.Message}");
Engine.JsSetException(error.Handle);
dependentModule = jsReferencingModuleRecord.DangerousGetHandle();
return true;
}
if (module != null)
{
var newModuleRecord = m_moduleRecordFactory.CreateBaristaModuleRecord(Context, specifier, this, false);
m_importedModules.Add(moduleName, newModuleRecord);
dependentModule = newModuleRecord.Handle.DangerousGetHandle();
switch (module)
{
//For the built-in Script Module type, parse the string returned by ExportDefault and install it as a module.
case IBaristaScriptModule scriptModule:
var script = scriptModule.ExportDefault(Context, newModuleRecord) as JsString;
if (script == null)
{
var error = Context.CreateError($"The module {moduleName} implements IBaristaScriptModule and is expected to return a string based module that exports a default value.");
Engine.JsSetException(error.Handle);
return true;
}
newModuleRecord.ParseModuleSource(script.ToString());
return false;
//Otherwise, install the module.
default:
var result = InstallModule(newModuleRecord, referencingModuleRecord, module, specifier);
return result;
}
}
}
}
dependentModule = jsReferencingModuleRecord.DangerousGetHandle();
return true;
}
private bool InstallModule(BaristaModuleRecord newModuleRecord, BaristaModuleRecord referencingModuleRecord, IBaristaModule module, JavaScriptValueSafeHandle specifier)
{
try
{
var moduleValue = module.ExportDefault(Context, referencingModuleRecord);
return CreateSingleValueModule(newModuleRecord, specifier, moduleValue);
}
catch (Exception ex)
{
var error = Context.CreateError($"An error occurred while obtaining the default export of the native module named {newModuleRecord.Name}: {ex.Message}");
Engine.JsSetException(error.Handle);
return true;
}
}
/// <summary>
/// Creates a module that returns the specified value.
/// </summary>
/// <param name="valueSafeHandle"></param>
/// <param name="referencingModuleRecord"></param>
/// <param name="specifierHandle"></param>
/// <param name="dependentModuleRecord"></param>
/// <returns></returns>
private bool CreateSingleValueModule(BaristaModuleRecord moduleRecord, JavaScriptValueSafeHandle specifier, JsValue defaultExportedValue)
{
var globalId = Guid.NewGuid();
var exportSymbol = Context.Symbol.For($"$DEFAULTEXPORT_{globalId.ToString()}");
var exposeNativeValueScript = $@"
const defaultExport = global[Symbol.for('$DEFAULTEXPORT_{globalId.ToString()}')];
export default defaultExport;
";
Context.Object.DefineProperty(Context.GlobalObject, exportSymbol, new JsPropertyDescriptor() { Configurable = false, Enumerable = false, Writable = false, Value = defaultExportedValue });
moduleRecord.ParseModuleSource(exposeNativeValueScript);
return false;
}
#region IDisposable Support
protected override void Dispose(bool disposing)
{
if (!IsDisposed)
{
if (disposing)
{
// free managed resources
foreach(var importedModule in m_importedModules.Values)
{
importedModule.Dispose();
}
}
// free unmanaged resources (unmanaged objects)
if (m_parentModule == null)
{
try
{
Engine.JsSetModuleHostInfo(Handle, JavaScriptModuleHostInfoKind.FetchImportedModuleFromScriptCallback, IntPtr.Zero);
Engine.JsSetModuleHostInfo(Handle, JavaScriptModuleHostInfoKind.FetchImportedModuleCallback, IntPtr.Zero);
Engine.JsSetModuleHostInfo(Handle, JavaScriptModuleHostInfoKind.NotifyModuleReadyCallback, IntPtr.Zero);
}
catch
{
//Do Nothing...
}
}
//Unset the before collect callback.
Engine.JsSetObjectBeforeCollectCallback(Handle, IntPtr.Zero, null);
if (m_fetchImportedModuleCallbackHandle != default(GCHandle) && m_fetchImportedModuleCallbackHandle.IsAllocated)
{
m_fetchImportedModuleCallbackHandle.Free();
}
if (m_notifyCallbackHandle != default(GCHandle) && m_notifyCallbackHandle.IsAllocated)
{
m_notifyCallbackHandle.Free();
}
m_beforeCollectCallbackDelegateHandle.Free();
}
base.Dispose(disposing);
}
#endregion
}
}