Skip to content

Commit

Permalink
Merge pull request #32 from xledger/browser-api-test-coverage
Browse files Browse the repository at this point in the history
Browser API test coverage
  • Loading branch information
oconnor0 authored Feb 3, 2025
2 parents e53a1e1 + 0880e99 commit 17e5ebd
Show file tree
Hide file tree
Showing 8 changed files with 667 additions and 223 deletions.
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

0 comments on commit 17e5ebd

Please sign in to comment.