Skip to content

Commit 3bdbddc

Browse files
committed
Better support for Uint8Array in ReadableStream
There's always going to be ambiguity between a string and a Uint8Array. We already had TypedArray(u8) as a discriminator when _returning_ values. But now the type is also used by mapping JS values to Zig. To support this efficiently when probing the union, the typed array mapping logic was extracted into its own function (so that it can be used by the probe).
1 parent 54f9bfb commit 3bdbddc

File tree

7 files changed

+159
-100
lines changed

7 files changed

+159
-100
lines changed

src/browser/fetch/Request.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ pub fn constructor(input: RequestInput, _options: ?RequestInit, page: *Page) !Re
181181
pub fn get_body(self: *const Request, page: *Page) !?*ReadableStream {
182182
if (self.body) |body| {
183183
const stream = try ReadableStream.constructor(null, null, page);
184-
try stream.queue.append(page.arena, body);
184+
try stream.queue.append(page.arena, .{ .string = body });
185185
return stream;
186186
} else return null;
187187
}

src/browser/fetch/Response.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ pub fn constructor(_input: ?ResponseBody, _options: ?ResponseOptions, page: *Pag
109109
pub fn get_body(self: *const Response, page: *Page) !*ReadableStream {
110110
const stream = try ReadableStream.constructor(null, null, page);
111111
if (self.body) |body| {
112-
try stream.queue.append(page.arena, body);
112+
try stream.queue.append(page.arena, .{ .string = body });
113113
}
114114
return stream;
115115
}

src/browser/streams/ReadableStream.zig

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@
1919
const std = @import("std");
2020
const log = @import("../../log.zig");
2121

22-
const Page = @import("../page.zig").Page;
22+
const Allocator = std.mem.Allocator;
2323
const Env = @import("../env.zig").Env;
24+
const Page = @import("../page.zig").Page;
2425

2526
const ReadableStream = @This();
2627
const ReadableStreamDefaultReader = @import("ReadableStreamDefaultReader.zig");
@@ -45,16 +46,42 @@ cancel_fn: ?Env.Function = null,
4546
pull_fn: ?Env.Function = null,
4647

4748
strategy: QueueingStrategy,
48-
queue: std.ArrayListUnmanaged([]const u8) = .empty,
49+
queue: std.ArrayListUnmanaged(Chunk) = .empty,
50+
51+
pub const Chunk = union(enum) {
52+
// the order matters, sorry.
53+
uint8array: Env.TypedArray(u8),
54+
string: []const u8,
55+
56+
pub fn dupe(self: Chunk, allocator: Allocator) !Chunk {
57+
return switch (self) {
58+
.string => |str| .{ .string = try allocator.dupe(u8, str) },
59+
.uint8array => |arr| .{ .uint8array = try arr.dupe(allocator) },
60+
};
61+
}
62+
};
4963

5064
pub const ReadableStreamReadResult = struct {
51-
const ValueUnion =
52-
union(enum) { data: []const u8, empty: void };
53-
54-
value: ValueUnion,
5565
done: bool,
66+
value: Value = .empty,
67+
68+
const Value = union(enum) {
69+
empty,
70+
data: Chunk,
71+
};
72+
73+
pub fn init(chunk: Chunk, done: bool) ReadableStreamReadResult {
74+
if (done) {
75+
return .{ .done = true, .value = .empty };
76+
}
77+
78+
return .{
79+
.done = false,
80+
.value = .{ .data = chunk },
81+
};
82+
}
5683

57-
pub fn get_value(self: *const ReadableStreamReadResult) ValueUnion {
84+
pub fn get_value(self: *const ReadableStreamReadResult) Value {
5885
return self.value;
5986
}
6087

src/browser/streams/ReadableStreamDefaultController.zig

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,17 @@ pub fn _close(self: *ReadableStreamDefaultController, _reason: ?[]const u8, page
5151
// to discard, must use cancel.
5252
}
5353

54-
pub fn _enqueue(self: *ReadableStreamDefaultController, chunk: []const u8, page: *Page) !void {
54+
pub fn _enqueue(self: *ReadableStreamDefaultController, chunk: ReadableStream.Chunk, page: *Page) !void {
5555
const stream = self.stream;
5656

5757
if (stream.state != .readable) {
5858
return error.TypeError;
5959
}
6060

61-
const duped_chunk = try page.arena.dupe(u8, chunk);
61+
const duped_chunk = try chunk.dupe(page.arena);
6262

6363
if (self.stream.reader_resolver) |*rr| {
64-
try rr.resolve(ReadableStreamReadResult{ .value = .{ .data = duped_chunk }, .done = false });
64+
try rr.resolve(ReadableStreamReadResult.init(duped_chunk, false));
6565
self.stream.reader_resolver = null;
6666
}
6767

src/browser/streams/ReadableStreamDefaultReader.zig

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ pub fn _read(self: *const ReadableStreamDefaultReader, page: *Page) !Env.Promise
4949
const data = self.stream.queue.orderedRemove(0);
5050
const resolver = page.main_context.createPromiseResolver();
5151

52-
try resolver.resolve(ReadableStreamReadResult{ .value = .{ .data = data }, .done = false });
52+
try resolver.resolve(ReadableStreamReadResult.init(data, false));
5353
try self.stream.pullIf();
5454
return resolver.promise();
5555
} else {
@@ -67,9 +67,9 @@ pub fn _read(self: *const ReadableStreamDefaultReader, page: *Page) !Env.Promise
6767

6868
if (stream.queue.items.len > 0) {
6969
const data = self.stream.queue.orderedRemove(0);
70-
try resolver.resolve(ReadableStreamReadResult{ .value = .{ .data = data }, .done = false });
70+
try resolver.resolve(ReadableStreamReadResult.init(data, false));
7171
} else {
72-
try resolver.resolve(ReadableStreamReadResult{ .value = .empty, .done = true });
72+
try resolver.resolve(ReadableStreamReadResult{ .done = true });
7373
}
7474

7575
return resolver.promise();

src/runtime/js.zig

Lines changed: 100 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -844,7 +844,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
844844

845845
const entry = ModuleEntry{
846846
.module = PersistentModule.init(self.isolate, m),
847-
.promise = PersistentPromise.init(self.isolate, .{.handle = evaluated.handle}),
847+
.promise = PersistentPromise.init(self.isolate, .{ .handle = evaluated.handle }),
848848
};
849849
if (cacheable) {
850850
try self.module_cache.putNoClobber(arena, owned_url, entry);
@@ -1036,88 +1036,10 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
10361036
}
10371037
},
10381038
.slice => {
1039-
var force_u8 = false;
1040-
var array_buffer: ?v8.ArrayBuffer = null;
1041-
if (js_value.isTypedArray()) {
1042-
const buffer_view = js_value.castTo(v8.ArrayBufferView);
1043-
array_buffer = buffer_view.getBuffer();
1044-
} else if (js_value.isArrayBufferView()) {
1045-
force_u8 = true;
1046-
const buffer_view = js_value.castTo(v8.ArrayBufferView);
1047-
array_buffer = buffer_view.getBuffer();
1048-
} else if (js_value.isArrayBuffer()) {
1049-
force_u8 = true;
1050-
array_buffer = js_value.castTo(v8.ArrayBuffer);
1051-
}
1052-
1053-
if (array_buffer) |buffer| {
1054-
const backing_store = v8.BackingStore.sharedPtrGet(&buffer.getBackingStore());
1055-
const data = backing_store.getData();
1056-
const byte_len = backing_store.getByteLength();
1057-
1058-
switch (ptr.child) {
1059-
u8 => {
1060-
// need this sentinel check to keep the compiler happy
1061-
if (ptr.sentinel() == null) {
1062-
if (force_u8 or js_value.isUint8Array() or js_value.isUint8ClampedArray()) {
1063-
if (byte_len == 0) return &[_]u8{};
1064-
const arr_ptr = @as([*]u8, @ptrCast(@alignCast(data)));
1065-
return arr_ptr[0..byte_len];
1066-
}
1067-
}
1068-
},
1069-
i8 => {
1070-
if (js_value.isInt8Array()) {
1071-
if (byte_len == 0) return &[_]i8{};
1072-
const arr_ptr = @as([*]i8, @ptrCast(@alignCast(data)));
1073-
return arr_ptr[0..byte_len];
1074-
}
1075-
},
1076-
u16 => {
1077-
if (js_value.isUint16Array()) {
1078-
if (byte_len == 0) return &[_]u16{};
1079-
const arr_ptr = @as([*]u16, @ptrCast(@alignCast(data)));
1080-
return arr_ptr[0 .. byte_len / 2];
1081-
}
1082-
},
1083-
i16 => {
1084-
if (js_value.isInt16Array()) {
1085-
if (byte_len == 0) return &[_]i16{};
1086-
const arr_ptr = @as([*]i16, @ptrCast(@alignCast(data)));
1087-
return arr_ptr[0 .. byte_len / 2];
1088-
}
1089-
},
1090-
u32 => {
1091-
if (js_value.isUint32Array()) {
1092-
if (byte_len == 0) return &[_]u32{};
1093-
const arr_ptr = @as([*]u32, @ptrCast(@alignCast(data)));
1094-
return arr_ptr[0 .. byte_len / 4];
1095-
}
1096-
},
1097-
i32 => {
1098-
if (js_value.isInt32Array()) {
1099-
if (byte_len == 0) return &[_]i32{};
1100-
const arr_ptr = @as([*]i32, @ptrCast(@alignCast(data)));
1101-
return arr_ptr[0 .. byte_len / 4];
1102-
}
1103-
},
1104-
u64 => {
1105-
if (js_value.isBigUint64Array()) {
1106-
if (byte_len == 0) return &[_]u64{};
1107-
const arr_ptr = @as([*]u64, @ptrCast(@alignCast(data)));
1108-
return arr_ptr[0 .. byte_len / 8];
1109-
}
1110-
},
1111-
i64 => {
1112-
if (js_value.isBigInt64Array()) {
1113-
if (byte_len == 0) return &[_]i64{};
1114-
const arr_ptr = @as([*]i64, @ptrCast(@alignCast(data)));
1115-
return arr_ptr[0 .. byte_len / 8];
1116-
}
1117-
},
1118-
else => {},
1039+
if (ptr.sentinel() == null) {
1040+
if (try self.jsValueToTypedArray(ptr.child, js_value)) |value| {
1041+
return value;
11191042
}
1120-
return error.InvalidArgument;
11211043
}
11221044

11231045
if (ptr.child == u8) {
@@ -1223,6 +1145,11 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
12231145
}
12241146
return try self.createFunction(js_value);
12251147
}
1148+
if (@hasDecl(T, "_TYPED_ARRAY_ID_KLUDGE")) {
1149+
const VT = @typeInfo(std.meta.fieldInfo(T, .values).type).pointer.child;
1150+
const arr = (try self.jsValueToTypedArray(VT, js_value)) orelse return null;
1151+
return .{ .values = arr };
1152+
}
12261153

12271154
if (T == String) {
12281155
return .{ .string = try valueToString(self.context_arena, js_value, self.isolate, self.v8_context) };
@@ -1262,6 +1189,90 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
12621189
return value;
12631190
}
12641191

1192+
fn jsValueToTypedArray(_: *JsContext, comptime T: type, js_value: v8.Value) !?[]T {
1193+
var force_u8 = false;
1194+
var array_buffer: ?v8.ArrayBuffer = null;
1195+
if (js_value.isTypedArray()) {
1196+
const buffer_view = js_value.castTo(v8.ArrayBufferView);
1197+
array_buffer = buffer_view.getBuffer();
1198+
} else if (js_value.isArrayBufferView()) {
1199+
force_u8 = true;
1200+
const buffer_view = js_value.castTo(v8.ArrayBufferView);
1201+
array_buffer = buffer_view.getBuffer();
1202+
} else if (js_value.isArrayBuffer()) {
1203+
force_u8 = true;
1204+
array_buffer = js_value.castTo(v8.ArrayBuffer);
1205+
}
1206+
1207+
const buffer = array_buffer orelse return null;
1208+
1209+
const backing_store = v8.BackingStore.sharedPtrGet(&buffer.getBackingStore());
1210+
const data = backing_store.getData();
1211+
const byte_len = backing_store.getByteLength();
1212+
1213+
switch (T) {
1214+
u8 => {
1215+
// need this sentinel check to keep the compiler happy
1216+
if (force_u8 or js_value.isUint8Array() or js_value.isUint8ClampedArray()) {
1217+
if (byte_len == 0) return &[_]u8{};
1218+
const arr_ptr = @as([*]u8, @ptrCast(@alignCast(data)));
1219+
return arr_ptr[0..byte_len];
1220+
}
1221+
},
1222+
i8 => {
1223+
if (js_value.isInt8Array()) {
1224+
if (byte_len == 0) return &[_]i8{};
1225+
const arr_ptr = @as([*]i8, @ptrCast(@alignCast(data)));
1226+
return arr_ptr[0..byte_len];
1227+
}
1228+
},
1229+
u16 => {
1230+
if (js_value.isUint16Array()) {
1231+
if (byte_len == 0) return &[_]u16{};
1232+
const arr_ptr = @as([*]u16, @ptrCast(@alignCast(data)));
1233+
return arr_ptr[0 .. byte_len / 2];
1234+
}
1235+
},
1236+
i16 => {
1237+
if (js_value.isInt16Array()) {
1238+
if (byte_len == 0) return &[_]i16{};
1239+
const arr_ptr = @as([*]i16, @ptrCast(@alignCast(data)));
1240+
return arr_ptr[0 .. byte_len / 2];
1241+
}
1242+
},
1243+
u32 => {
1244+
if (js_value.isUint32Array()) {
1245+
if (byte_len == 0) return &[_]u32{};
1246+
const arr_ptr = @as([*]u32, @ptrCast(@alignCast(data)));
1247+
return arr_ptr[0 .. byte_len / 4];
1248+
}
1249+
},
1250+
i32 => {
1251+
if (js_value.isInt32Array()) {
1252+
if (byte_len == 0) return &[_]i32{};
1253+
const arr_ptr = @as([*]i32, @ptrCast(@alignCast(data)));
1254+
return arr_ptr[0 .. byte_len / 4];
1255+
}
1256+
},
1257+
u64 => {
1258+
if (js_value.isBigUint64Array()) {
1259+
if (byte_len == 0) return &[_]u64{};
1260+
const arr_ptr = @as([*]u64, @ptrCast(@alignCast(data)));
1261+
return arr_ptr[0 .. byte_len / 8];
1262+
}
1263+
},
1264+
i64 => {
1265+
if (js_value.isBigInt64Array()) {
1266+
if (byte_len == 0) return &[_]i64{};
1267+
const arr_ptr = @as([*]i64, @ptrCast(@alignCast(data)));
1268+
return arr_ptr[0 .. byte_len / 8];
1269+
}
1270+
},
1271+
else => {},
1272+
}
1273+
return error.InvalidArgument;
1274+
}
1275+
12651276
fn createFunction(self: *JsContext, js_value: v8.Value) !Function {
12661277
// caller should have made sure this was a function
12671278
std.debug.assert(js_value.isFunction());
@@ -1618,12 +1629,12 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
16181629
const isolate = self.isolate;
16191630

16201631
const resource = jsStringToZig(self.call_arena, .{ .handle = resource_name.? }, isolate) catch |err| {
1621-
log.err(.app, "OOM", .{.err = err, .src = "dynamicModuleCallback1"});
1632+
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback1" });
16221633
return @constCast(self.rejectPromise("Out of memory").handle);
16231634
};
16241635

16251636
const specifier = jsStringToZig(self.call_arena, .{ .handle = v8_specifier.? }, isolate) catch |err| {
1626-
log.err(.app, "OOM", .{.err = err, .src = "dynamicModuleCallback2"});
1637+
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback2" });
16271638
return @constCast(self.rejectPromise("Out of memory").handle);
16281639
};
16291640

@@ -1633,7 +1644,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
16331644
resource,
16341645
.{ .alloc = .if_needed, .null_terminated = true },
16351646
) catch |err| {
1636-
log.err(.app, "OOM", .{.err = err, .src = "dynamicModuleCallback3"});
1647+
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback3" });
16371648
return @constCast(self.rejectPromise("Out of memory").handle);
16381649
};
16391650

@@ -2193,6 +2204,10 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
21932204
const _TYPED_ARRAY_ID_KLUDGE = true;
21942205

21952206
values: []const T,
2207+
2208+
pub fn dupe(self: TypedArray(T), allocator: Allocator) !TypedArray(T) {
2209+
return .{ .values = try allocator.dupe(T, self.values) };
2210+
}
21962211
};
21972212
}
21982213

src/tests/streams/readable_stream.html

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
<!DOCTYPE html>
12
<script src="../testing.js"></script>
23

34
<script id=readable_stream>
@@ -17,6 +18,22 @@
1718
});
1819
</script>
1920

21+
<script id=readable_stream_binary>
22+
const input = new TextEncoder().encode('over 9000!');
23+
const binStream = new ReadableStream({
24+
start(controller) {
25+
controller.enqueue(input);
26+
controller.enqueue("world");
27+
controller.close();
28+
}
29+
});
30+
31+
testing.async(binStream.getReader().read(), (data) => {
32+
testing.expectEqual(input, data.value);
33+
testing.expectEqual(false, data.done);
34+
});
35+
</script>
36+
2037
<script id=readable_stream_close>
2138
var closeResult;
2239

0 commit comments

Comments
 (0)