web icon indicating copy to clipboard operation
web copied to clipboard

Allow converting `List<int>` to a `JSArray` / `JSTypedArray`

Open navaronbracke opened this issue 1 year ago • 5 comments

Currently, there is no easy way to pass a List<int>/Uint8List to the Blob constructor. We have to do stuff like:

    final List<int> bytes = ...;
    final Blob blob = Blob(bytes.map((int byte) => byte.toJS).toList().toJS);

Per https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob#array the constructor's field for the array supports TypedArray.

There is also no extension to convert a List<int> (which Uint8List implements) to a JSArray, since we only have that for List<JSAny?>. But int is a primitive that is allowed.

I propose we do either of (or both?) a) add an extension that converts a List<int> to a JSArray b) update the Blob constructor to also allow a JSTypedArray

~Personally I would just add option a, since it is easy and we don't need to change the Blob constructor.~

Context: https://github.com/flutter/flutter/issues/137374

navaronbracke avatar Oct 28 '23 10:10 navaronbracke

a) This one is complex and we've discussed it several times. We can definitely do the simple route of exposing an extension member that would be essentially the code you wrote above. The semantics there are consistent across all backends.

It's a bit more complicated if we want to take advantage of the type representation for performance benefits on the JS backends. On the JS backends, a List<int> is a subtype of the runtime type of JSArray, which is List<Object?>, so we can just cast. On dart2wasm, this isn't the case and we need to do the iterative copy conversion. We could make this an implementation detail, but we come across the same "liveness" issue as we do with List<JSAny?>. The resulting JSArray may or may not be the same object depending on the backend (since dart2wasm will need a copy or a proxy). This already is an issue with List<JSAny?>.toJS (and why we have toJSProxyOrRef), and so we're introducing more pitfalls here by having an inconsistent conversion.

Note that while int is allowed, int as a type parameter/within a higher-order type gets more complicated (moreso coming from JS rather than going in).

If we were to add this extension method, I propose we do the consistent semantics.

b) The IDL defines blobParts as a sequence, which we treat as JSArray, so that's likely where the gap here is. https://webidl.spec.whatwg.org/#idl-sequence

We could instead generate JSObject as that's the least upper bound here whenever we see sequence, but this might be too permissive. We could also add a new abstract type into our JS type hierarchy e.g. JSListLike that can be the supertype of JSArray and the typed array types and then use that here for sequence. This is also useful for types like NodeList.

cc @sigmundch

srujzs avatar Oct 31 '23 21:10 srujzs

cc @rakudrama

For (b), I wonder if we should be exposing something like JavaScriptIndexingBehavior (see https://github.com/dart-lang/sdk/blob/8b3d93234d1df69c3db5428fa02bb6b39c671ed8/sdk/lib/_internal/js_runtime/lib/js_helper.dart#L2719),

For (a) - a lot of our concerns with the List<int> => JSArray conversion came from supporting the List<int> type in general. Would this be any different if we provided the extension directly on typed arrays instead? Would that address the need we have here for Blob?

sigmundch avatar Oct 31 '23 22:10 sigmundch

Currently, there is no easy way to pass a List<int>/Uint8List to the Blob constructor. We have to do stuff like:

    final List<int> bytes = ...;
    final Blob blob = Blob(bytes.map((int byte) => byte.toJS).toList().toJS);

Per https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob#array the constructor's field for the array supports TypedArray.

The Blob constructor takes an iterable. If you pass a typed array that is not wrapped in another Iterable (like a JavaScript Array), it is seen as an iterable of numbers, and the numbers are converted to strings. Consider

var a = new Blob(new Int32Array(3));    // Blob {size: 3}
var b = new Blob([new Int32Array(3)]);  // Blob {size: 12}

a contains the byte sequence 48,48,48 (three zero characters, i.e. "000") b contains twelve bytes, all zero.

So... I think it is a bad idea to enabling the Blob constructor in Dart to directly take any TypedData types.

rakudrama avatar Nov 01 '23 18:11 rakudrama

Note @rakudrama that this is referring to the Blob API in package:web here: https://github.com/dart-lang/web/blob/e0564a4cc3cb96c88d15def1c545aa8dff375d94/lib/src/dom/fileapi.dart#L17

So the request is whether we can provide a zero-cost conversion between Int32Array and dart:js_interop's JSArray type.

sigmundch avatar Nov 01 '23 20:11 sigmundch

I wonder if we should be exposing something like JavaScriptIndexingBehavior (see https://github.com/dart-lang/sdk/blob/8b3d93234d1df69c3db5428fa02bb6b39c671ed8/sdk/lib/_internal/js_runtime/lib/js_helper.dart#L2719).

You mean as the representation type for the JS type interface? I think that would only work if we have an interceptor for every possible indexable JS type. I was imagining it to be interceptors.JSObject still, but merely having a static separation from js_interop.JSObject.

If you pass a typed array that is not wrapped in another Iterable (like a JavaScript Array), it is seen as an iterable of numbers, and the numbers are converted to strings.

That's...pretty confusing. I'm torn because while it is confusing, it's also what the API allows you to do.

srujzs avatar Nov 02 '23 00:11 srujzs