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

Add navigator.userAgent to global scope #2903

Closed
guest271314 opened this issue Feb 2, 2025 · 13 comments
Closed

Add navigator.userAgent to global scope #2903

guest271314 opened this issue Feb 2, 2025 · 13 comments

Comments

@guest271314
Copy link

Feature suggestion

See

Deno and Bun execute AssemblyScript directly (after transforming or stripping types).

AssemblyScript process.stdin.read() is not the same as Node.js process.stdin.read(). String.UTF8 is not defined in Node.js or Deno.

The rational being able to use the same code that can be executed directly by JavaScript/TypeScript runtimes and compiled to WASM by asc.

Instead of using String.UTF8 as a condition

if(`${typeof String.UTF8}`) {
  process.stdout.write("AssemblyScript Version 0.27.32");
} else {
  process.stdout.write(navigator.userAgent);
}
@MaxGraey
Copy link
Member

MaxGraey commented Feb 2, 2025

userAgent is WebAPI and doesn't relate to JavaScript specifications, much less AssemblyScript. To work with WebAPI, use host bindings

@MaxGraey MaxGraey closed this as completed Feb 2, 2025
@guest271314
Copy link
Author

navigator.userAgent is useful. See https://github.com/wintercg/proposal-minimum-common-api, wintercg/proposal-minimum-common-api@94a44ea and the links I shared above. So this https://github.com/guest271314/NativeMessagingHosts/blob/main/nm_typescript.ts#L30-L69 can be done


import process from "node:process";
const runtime: string = navigator.userAgent;
const buffer: ArrayBuffer = new ArrayBuffer(0, { maxByteLength: 1024 ** 2 });
const view: DataView = new DataView(buffer);
const encoder: TextEncoder = new TextEncoder();

let readable: NodeJS.ReadStream & { fd: 0 } | ReadableStream<Uint8Array>,
  writable: WritableStream<Uint8Array>,
  exit: () => void = () => {};

if (runtime.startsWith("Deno")) {
  // @ts-ignore Deno
  ({ readable } = Deno.stdin);
  // @ts-ignore Deno
  ({ writable } = Deno.stdout);
  // @ts-ignore Deno
  ({ exit } = Deno);
}

if (runtime.startsWith("Node")) {
  readable = process.stdin;
  writable = new WritableStream({
    write(value) {
      process.stdout.write(value);
    },
  }, new CountQueuingStrategy({ highWaterMark: Infinity }));
  ({ exit } = process);
}

if (runtime.startsWith("Bun")) {
  // @ts-ignore Bun
  readable = Bun.file("/dev/stdin").stream();
  writable = new WritableStream<Uint8Array>({
    async write(value) {
      // @ts-ignore Bun
      await Bun.write(Bun.stdout, value);
    },
  }, new CountQueuingStrategy({ highWaterMark: Infinity }));
  ({ exit } = process);
}

Since AssemblyScript can be run directly with Bun and Deno

bun module.ts 4 5
/home/user/bin/bun5 of 23 (0-indexed, factorial 24) => [0,3,2,1]

deno  module.ts 4 5
deno5 of 23 (0-indexed, factorial 24) => [0,3,2,1]

I'm trying to switch on user agent so I can run the same code to compile to WASM and execute with a JavaScript/TypeScript runtime.

At this part of the code

if (process.argv.length > 1) {
  input = process.argv.at(-2);
  lex = process.argv.at(-1);
} else {
  let stdin = process.stdin;
  // @ts-ignore
  let buffer = bool > 0 ? new ArrayBuffer(64): new Uint8Array(new ArrayBuffer(64));
  // @ts-ignore
  let n: number = bool > 0 ? stdin.read(buffer) : readSync(stdin.fd, buffer);
  if (n > 0) {
    // @ts-ignore
    let data = bool > 0 ? String.UTF8.decode(buffer) : new TextDecoder().decode(buffer);
    input = data.slice(0, data.indexOf(" "));
    lex = data.slice(data.indexOf(" "), data.length);
  }
}

I'll see what you got going for host bindings.

navigator.userAgent is simple, consistent, and helpful. Thus a list item for WinterCG, implemented by Node.js, Deno, Bun, QuickJS NG, txiki.js.

@guest271314
Copy link
Author

My implementation so far

let runtime: string = process.argv.at(0);
let bool: i32 = runtime.includes(".wasm") ? 1 : 0;


if(bool > 0) {
   process.stdout.write("AssemblyScript Version 0.27.32");
} else {
  // @ts-ignore
  process.stdout.write(runtime); 
}

@guest271314
Copy link
Author

So, how would you switch in the above case between stdin.read(buffer) and readSync(stdin.fd, buffer) if you are either a) compiling to WASM with asc, or executing the AssemblyScript code directly with deno or bun using --bindings?

@CountBleck
Copy link
Member

AS isn't a runtime; maybe you can use isDefined(ASC_SHRINK_LEVEL) or something for detection.

@guest271314
Copy link
Author

I'll reserve my opinion on whether or not AssemblyScript is close enough to a runtime.

Tried that.

isDefined is not defined in Deno or Bun or Node.js.

As stated above both Deno and Bun execute AssemblyScript source code directly - after stripping types.

Now, read() from wasi_process.ts has a different signature from node:fs readSync(), which expects fd as first argument to the function.

@unmanaged
abstract class ReadableStream extends Stream {
  read(buffer: ArrayBuffer, offset: isize = 0): i32 {
    var end = <usize>buffer.byteLength;
    if (offset < 0 || <usize>offset > end) {
      throw new Error(E_INDEXOUTOFRANGE);
    }
    store<usize>(tempbuf, changetype<usize>(buffer) + offset);
    store<usize>(tempbuf, end - offset, sizeof<usize>());
    var err = fd_read(<u32>changetype<usize>(this), tempbuf, 1, tempbuf + 2 * sizeof<usize>());
    if (err) throw new Error(errnoToString(err));
    return <i32>load<isize>(tempbuf, 2 * sizeof<usize>());
  }
}

I've substituted String.fromCodePoint() for AssemblyScript-specific String.UTF8.decode()

So when the AssemblyScript is transformed/compiled to JavaScript using either Deno or Bun, e.,g.,

bun build module.ts

// module.ts
function array_nth_permutation(len, n) {
  let lex = n;
  let b = [];
  for (let x = 0;x < len; x++) {
    b.push(x);
  }
  const res = [];
  let i = 1;
  let f = 1;
  for (;i <= len; i++) {
    f *= i;
  }
  let fac = f;
  if (n >= 0 && n < f) {
    for (;len > 0; len--) {
      f /= len;
      i = (n - n % f) / f;
      res.push(b.splice(i, 1)[0]);
      n %= f;bun build module.ts

    }
    process.stdout.write(`${lex} of ${fac - 1} (0-indexed, factorial ${fac}) => [`);
    for (let z = 0;z < res.length; z++) {
      process.stdout.write(`${res.at(z)}`);
      if (z < res.length - 1) {
        process.stdout.write(",");
      }
    }
    process.stdout.write(`]
`);
    process.exit(0);
  } else {
    if (n === 0) {
      process.stdout.write(`${n} = 0`);
    }
    process.stdout.write(`${n} >= 0 && ${n} < ${f}: ${n >= 0 && n < f}`);
    process.exit(1);
  }
}
var input = "0";
var lex = "0";
var runtime = process.platform;
var bool = runtime === "wasm" ? 1 : 0;
if (bool > 0) {
} else {
}
var stdin = process.stdin;
var buffer = new ArrayBuffer(64);
var view = new DataView(buffer);
var n = stdin.read(buffer);
if (n > 0) {
  let data = "";
  for (let i = 0;i < n; i++) {
    data += String.fromCodePoint(view.getUint8(i));
  }
  input = data.slice(0, data.indexOf(" "));
  lex = data.slice(data.indexOf(" "), data.length);
} else {
  input = process.argv.at(-2);
  lex = process.argv.at(-1);
}
input = input.trim();
lex = lex.trim();
if (parseInt(input) < 2 || parseInt(lex) < 0) {
  process.stdout.write(`Expected n > 2, m >= 0, got ${input}, ${lex}`);
  process.exit(1);
}
array_nth_permutation(parseInt(input), parseInt(lex));
export {
  array_nth_permutation
};

there's this still left

var n = stdin.read(buffer);

which is incompatible with Deno or Bun.

I'm trying to figure out a way to write read() once, so that the same code can be compiled to WASM, compiled to JavaScript and executed in the same way.

Right now I'm modifying the resulting JavaScript by hand, e.g.,

deno install -f -c node_modules/assemblyscript/std/tsconfig.json module.ts

or

deno module.ts 4 5
5 of 23 (0-indexed, factorial 24) => [0,3,2,1]

then

cat  "$HOME/.cache/deno/gen/file$PWD/module.ts.js" > module.ts.js

then

let n = readSync(stdin.fd, buffer);

So I'm trying to figure out a way to DRY.

Either modifying read() in wasi_process.ts, or somehow importing node:fs readSync into the module.ts file and using that instead of read() in wasi_process.ts.

Make sense?

@MaxGraey
Copy link
Member

MaxGraey commented Feb 2, 2025

So, how would you switch in the above case between stdin.read(buffer) and readSync(stdin.fd, buffer) if you are either a) compiling to WASM with asc, or executing the AssemblyScript code directly with deno or bun using --bindings

AssemblyScript as well as WebAssembly knows absolutely nothing about the host's runtime. It can be browser, node, deno, wasm3 or wasmtime. That is, Wasm module is absolutely isolated from the host it runs on. So I don't really understand exactly what the question is. The host's fragmentation problem is solved on the host side (not on AS side) and then the unified solution is imported into Wasm (AS) module through bindings.

process.stdout.write should be handled the same way in the browser as in deno / node. Currently process.stdout.write handled by separate wasi-shim which utilize wasi-ABI. WASI doesn't provide any api layer for navigator. WASI basically is unified layer for mimic to something like POSIX

@CountBleck
Copy link
Member

If AS were to support navigator.userAgent, it would be a binding to the Web API and not something AS-specific. It is not the intention of AS to pretend to be a runtime.

@guest271314
Copy link
Author

BTW, read() in wasi_process is incompatible with Node.js, too.

That is, Wasm module is absolutely isolated from the host it runs on.

I don't think so https://github.com/humodz/node-wasi-preopens-escape. But that's not what I'm trying to get done here.

I'm just trying to not repeat myself by using the same code across AssemblyScript, TypeScript, JavaScript, WebAssembly.

navigator.userAgent is just my idea of switching code paths depending on what runtime executes the code.

If wasmtime or wasmer executes the .wasm file compiled by asc I can get that information from process.platform or process.argv.

The real issue is that read() in wasi_process.ts is AssemblyScript-specific. If that code signature was compatible with node:fs readSync() then that would solve what I'm working on here.

@guest271314
Copy link
Author

Well, almost solve. Because process.stdin in Node.js world doesn't read like Node.js's readSync(), so there's that, too.

Basically, I'm saying maintainers could try to implement signatures that match the existing signature they are copying/extending.

read() in wasi_process.ts is all about AssemblyScript - without thinking about slight modifications can allow the same code to be run by a JavaScript/TypeScript runtime, and WebAssembly runtimes.

@guest271314
Copy link
Author

This is what I came up with so far

Add readSync() with node:fs definition to @assemblyscript/wasi-shim/wasi_process.ts

  readSync(_fd:i32, b: Uint8Array, offset: isize = 0): i32 {
    var buffer: ArrayBuffer = b.buffer;
    var end = <usize>buffer.byteLength;
    if (offset < 0 || <usize>offset > end) {
      throw new Error(E_INDEXOUTOFRANGE);
    }
    store<usize>(tempbuf, changetype<usize>(buffer) + offset);
    store<usize>(tempbuf, end - offset, sizeof<usize>());
    var err = fd_read(<u32>changetype<usize>(this), tempbuf, 1, tempbuf + 2 * sizeof<usize>());
    if (err) throw new Error(errnoToString(err));
    return <i32>load<isize>(tempbuf, 2 * sizeof<usize>());
  }

Create an external ECMAScript Module file defining node:fs readSync as a property of node:process process.stdin, preprocess-module.ts

import process from "node:process";
import { readSync } from "node:fs";
process.stdin.readSync = readSync;
export {};

Modify module.ts to substitute process.stdin.readSync for read in wasi_process.ts to read stdin; substitute encoding to string with String.fromCodePoint() for String.UTF8.decode()

if (process.argv.length >= 3) {
  input = process.argv.at(-2);
  lex = process.argv.at(-1);
} else {
  //let stdin = process.stdin;
  let buffer = new Uint8Array(64);
  // let view = new DataView(buffer);
  // @ts-ignore
  /*
error: Uncaught (in promise) TypeError: The "buffer" argument must be an instance of Buffer, TypedArray, or DataView. Received an instance of ArrayBuffer
  let n: i32 = process.stdin.readSync(0, buffer); // readSync(0, buffer);

ERROR TS2322: Type '~lib/dataview/DataView' is not assignable to type '~lib/arraybuffer/ArrayBuffer'.

  */
  let n: i32 = process.stdin.readSync(0, buffer); // readSync(0, buffer);
  if (n > 0) {
    let data: string = "";
    for (let i: i32 = 0; i < n; i++) {
      data += String.fromCodePoint(buffer[i]);
    }
    // @ts-ignore
    //let data = String.UTF8.decode(buffer);
    input = data.slice(0, data.indexOf(" "));
    lex = data.slice(data.indexOf(" "), data.length);
  }
}

Compile to WASM with asc

node_modules/.bin/asc --exportStart --config ./node_modules/@assemblyscript/wasi-shim/asconfig.json  module.ts -o module.wasm

Test processing stdin and arguments passed to WebAssembly runtimes and JavaScript/TypeScript runtimes

echo '13 2' | wasmtime  module.wasm
2 of 6227020799 (0-indexed, factorial 6227020800) => [0,1,2,3,4,5,6,7,8,9,11,10,12]
wasmer  module.wasm 13 2
2 of 6227020799 (0-indexed, factorial 6227020800) => [0,1,2,3,4,5,6,7,8,9,11,10,12]
echo '13 2' | bun module.wasm
2 of 6227020799 (0-indexed, factorial 6227020800) => [0,1,2,3,4,5,6,7,8,9,11,10,12]

Preload the ECMAScript Module, then execute AssemblyScript source module.ts directly

'13 2' | bun -r ./preprocess-module.ts module.ts
2 of 6227020799 (0-indexed, factorial 6227020800) => [0,1,2,3,4,5,6,7,8,9,11,10,12]
bun -r ./preprocess-module.ts module.ts 13 2
2 of 6227020799 (0-indexed, factorial 6227020800) => [0,1,2,3,4,5,6,7,8,9,11,10,12]
echo '13 2' | node --no-warnings --experimental-transform-types --import ./preprocess-module.ts module.ts
2 of 6227020799 (0-indexed, factorial 6227020800) => [0,1,2,3,4,5,6,7,8,9,11,10,12]
node --no-warnings --experimental-transform-types --import ./preprocess-module.ts module.ts 13 2
2 of 6227020799 (0-indexed, factorial 6227020800) => [0,1,2,3,4,5,6,7,8,9,11,10,12]

Now I'll figure out a way to do the same thing using Deno.

@guest271314
Copy link
Author

I'm not finding a way to preload an ECMAScript Module in Deno, yet. Modifying preprocess-module.ts to make use of navigator.userAgent, and executing preprocess-module.ts that imports module.ts

import process from "node:process";
import { readSync } from "node:fs";
process.stdin.readSync = readSync;
export {};
if (navigator.userAgent.includes("Deno")) {
  import("./module.ts")
}
echo '13 2' | deno preprocess-module.ts
2 of 6227020799 (0-indexed, factorial 6227020800) => [0,1,2,3,4,5,6,7,8,9,11,10,12]
deno preprocess-module.ts 13 2
2 of 6227020799 (0-indexed, factorial 6227020800) => [0,1,2,3,4,5,6,7,8,9,11,10,12]

@guest271314
Copy link
Author

Another way to use Deno without executing preprocess-module.ts

import process from "node:process";
import { readSync } from "node:fs";
process.stdin.readSync = readSync;
export {};
/*
if (navigator.userAgent.includes("Deno")) {
  import("./module.ts")
}
*/
cat preprocess-module.ts module.ts | deno run - 13 2
2 of 6227020799 (0-indexed, factorial 6227020800) => [0,1,2,3,4,5,6,7,8,9,11,10,12]
echo '13 2' | deno $HOME/.cache/deno/gen/file$PWD/\$deno\$stdin.mts.js
2 of 6227020799 (0-indexed, factorial 6227020800) => [0,1,2,3,4,5,6,7,8,9,11,10,12]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants