Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Browser API test coverage #32

Merged
merged 7 commits into from
Feb 3, 2025
1 change: 1 addition & 0 deletions Trell.Engine/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

[assembly: ComVisible(false)]
[assembly: InternalsVisibleTo("Trell")]
[assembly: InternalsVisibleTo("Trell.Test")]

// The following GUID is for the ID of the typelib if this project is exposed to COM.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System.Collections.Concurrent;
using Trell.IPC.Server;

namespace Trell.Collections;
namespace Trell.Engine.Collections;
/// <summary>
/// A bounded, concurrent object pool that preinitializes some number of objects.
/// </summary>
Expand Down
220 changes: 8 additions & 212 deletions Trell.Engine/RuntimeApis/BrowserApi.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.ClearScript.JavaScript;
using System.Net.Http;
using System.Reflection;
using System.Text.Json;
using Trell.Engine.ClearScriptWrappers;
using Trell.Engine.Extensibility.Interfaces;
Expand All @@ -16,225 +17,20 @@ public BrowserApi(ITrellLogger logger) {

public PluginDotNetObject DotNetObject { get; }

public string JsScript => """
async function fetch(url, options) {
const response = await dotNetBrowserApi.Fetch(url, options);
const blob = async () => new Blob([await response.content()], {type: response.headers['Content-Type'] ?? ''});
return {
ok: response.ok,
status: response.status,
statusText: response.statusText,
text: () => blob().then(x => x.text()),
json: () => blob().then(x => x.text()).then(x => JSON.parse(x)),
blob: blob,
headers: new Headers(response.headers)
};
}

class TextEncoder {
#encoding;

constructor(encoding) {
if (encoding !== undefined && encoding !== 'utf-8') {
throw new Error('unsupported encoding');
}

this.#encoding = encoding ?? 'utf-8';
}

get encoding() {
return this.#encoding;
}

encode(string) {
return new Uint8Array(dotNetBrowserApi.TextEncode(string, this.#encoding));
}

encodeInto(string, uint8Array) {
throw new Error('not implemented yet');
}
}

const BLOB_IMPL_SYM = Symbol('blobImpl');

class BasicBlobImpl {
#parts; #cache;

constructor(blobParts) {
this.#parts = blobParts;
}

async arrayBuffer() {
if (this.#cache !== undefined) {
return this.#cache;
}

const encodedParts = []
let encodedSize = 0;

for (const part of this.#parts) {
if (typeof part === 'string') {
const buf = dotNetBrowserApi.TextEncode(part, 'utf-8');
encodedParts.push(buf);
encodedSize += buf.byteLength;
} else if (part instanceof Blob) {
const buf = await part.arrayBuffer();
encodedParts.push(buf);
encodedSize += buf.byteLength;
} else if (part instanceof ArrayBuffer) {
encodedParts.push(part);
encodedSize += part.byteLength;
} else if (part?.buffer instanceof ArrayBuffer) {
encodedParts.push(part.buffer);
encodedSize += part.buffer.byteLength;
} else {
throw new Error(`invalid value for blob part: ${part}`);
}
}

const bytes = new Uint8Array(encodedSize);
let i = 0;
for (const part of encodedParts) {
for (const v of new Uint8Array(part)) {
bytes[i++] = v;
}
}

this.#cache = bytes.buffer;
this.#parts = undefined;
return bytes.buffer;
}
}

class SliceBlobImpl {
#source; #start; #end; #cache;

constructor(source, start, end) {
this.#source = source;
this.#start = start;
this.#end = end;
}

async arrayBuffer() {
if (this.#cache !== undefined) {
return this.#cache;
}

const buf = await this.#source.arrayBuffer();
this.#cache = buf.slice(this.#start, this.#end);
this.#source = undefined;
return this.#cache;
}
}

class Blob {
#impl; #type;

constructor(parts, options) {
this.#impl = options[BLOB_IMPL_SYM] ?? new BasicBlobImpl(parts);
this.#type = options?.type ?? '';
}

arrayBuffer() {
return this.#impl.arrayBuffer();
}

slice(start, end, type) {
return new Blob(null, {[BLOB_IMPL_SYM]: new SliceBlobImpl(this, start, end), type: type ?? this.#type});
}

stream() {
throw new Error('not implemted yet');
}

text() {
return this.#impl.arrayBuffer().then(x => dotNetBrowserApi.TextDecode(x, 'utf-8'));
}

get type() {
return this.#type;
}
}

class File extends Blob {
#filename;

constructor(parts, filename, options) {
super(parts, options);
this.#filename = filename;
}

get name() {
return this.#filename;
}
}

function Headers(dotNetHeaders) {
this.headers = {};
for (let k of dotNetHeaders.Keys || []) {
this.headers[k] = dotNetHeaders.Item(k);
}
this.get = function(key) {
return this.headers[key];
}
}

console = (function() {
const LOG_MESSAGE_LENGTH_LIMIT = 4000; // If changing, also change in C# below.
let getLogMessage = (args) => {
let parts = [];
let len = 0;
for (let i = 0; i < args.length && len < LOG_MESSAGE_LENGTH_LIMIT; i += 1) {
let arg = args[i];
if (typeof arg === 'string' || arg instanceof String) {
parts.push(arg);
} else if (arg?.hostException) {
parts.push(arg.toString());
} else if (arg === undefined) {
parts.push("undefined");
} else if (arg === null) {
parts.push("null");
} else if (arg instanceof Error) {
parts.push(arg.stack);
} else {
let s = arg?.toString();
parts.push(s);
}
let sepLen = i > 0 ? 1 : 0;
len += parts[parts.length - 1].length + sepLen;
}
let msg = parts.join(' ');
if (msg.length > LOG_MESSAGE_LENGTH_LIMIT) {
msg = msg.substring(0, LOG_MESSAGE_LENGTH_LIMIT) + '…';
}
return msg;
};
return {
log: (...args) => {
let s = getLogMessage(args);
dotNetBrowserApi.LogInformation(s);
},
warn: (...args) => {
let s = getLogMessage(args);
dotNetBrowserApi.LogWarning(s);
},
error: (...args) => {
let s = getLogMessage(args);
dotNetBrowserApi.LogError(s);
},
status: (text, options) => {
dotNetBrowserApi.LogStatus(text, options ?? {});
},
}
})();
""";
public string JsScript => CachedJsScript.Value;
static readonly Lazy<string> CachedJsScript = new(() =>
new StreamReader(
Assembly.GetExecutingAssembly().GetManifestResourceStream("Trell.Engine.RuntimeApis.ExposeBrowserApi.js")!
).ReadToEnd()
);

public IReadOnlyList<string> TopLevelJsNamesExposed { get; } = new[] {
"fetch",
"Headers",
"console",
"Blob",
"TextEncoder",
"TextDecoder",
};
}

Expand Down
Loading