diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig
index bc5468bd..d2c19150 100644
--- a/src/browser/ScriptManager.zig
+++ b/src/browser/ScriptManager.zig
@@ -752,11 +752,7 @@ const Script = struct {
};
if (comptime IS_DEBUG) {
- log.debug(.browser, "executed script", .{
- .src = url,
- .success = success,
- .on_load = script_element._on_load != null
- });
+ log.debug(.browser, "executed script", .{ .src = url, .success = success, .on_load = script_element._on_load != null });
}
defer page.tick();
diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig
index eea764b2..1b0c768f 100644
--- a/src/browser/js/Context.zig
+++ b/src/browser/js/Context.zig
@@ -665,21 +665,21 @@ pub fn mapZigInstanceToJs(self: *Context, js_obj_: ?v8.Object, value: anytype) !
pub fn jsValueToZig(self: *Context, comptime T: type, js_value: v8.Value) !T {
switch (@typeInfo(T)) {
.optional => |o| {
- // If type type is a ?js.Value or a ?js.Object, then we want to pass
- // a js.Object, not null. Consider a function,
- // _doSomething(arg: ?Env.JsObjet) void { ... }
- //
- // And then these two calls:
- // doSomething();
- // doSomething(null);
- //
- // In the first case, we'll pass `null`. But in the
- // second, we'll pass a js.Object which represents
- // null.
- // If we don't have this code, both cases will
- // pass in `null` and the the doSomething won't
- // be able to tell if `null` was explicitly passed
- // or whether no parameter was passed.
+ // If type type is a ?js.Value or a ?js.Object, then we want to pass
+ // a js.Object, not null. Consider a function,
+ // _doSomething(arg: ?Env.JsObjet) void { ... }
+ //
+ // And then these two calls:
+ // doSomething();
+ // doSomething(null);
+ //
+ // In the first case, we'll pass `null`. But in the
+ // second, we'll pass a js.Object which represents
+ // null.
+ // If we don't have this code, both cases will
+ // pass in `null` and the the doSomething won't
+ // be able to tell if `null` was explicitly passed
+ // or whether no parameter was passed.
if (comptime o.child == js.Value) {
return js.Value{
.context = self,
@@ -838,7 +838,6 @@ fn jsValueToStruct(self: *Context, comptime T: type, js_value: v8.Value) !?T {
return .{ .string = try self.valueToString(js_value, .{ .allocator = self.arena }) };
}
-
if (comptime T == js.Value) {
// Caller wants an opaque js.Object. Probably a parameter
// that it needs to pass back into a callback
@@ -1157,15 +1156,14 @@ pub fn stackTrace(self: *const Context) !?[]const u8 {
}
// == Promise Helpers ==
-pub fn rejectPromise(self: *Context, value: anytype) js.Promise {
+pub fn rejectPromise(self: *Context, value: anytype) !js.Promise {
const ctx = self.v8_context;
var resolver = v8.PromiseResolver.init(ctx);
- if (self.zigValueToJs(value, .{})) |js_value| {
- _ = resolver.reject(ctx, js_value);
- } else |err| {
- const str = self.isolate.initStringUtf8(@errorName(err));
- _ = resolver.reject(ctx, str.toValue());
+ const js_value = try self.zigValueToJs(value, .{});
+ if (resolver.reject(ctx, js_value) == null) {
+ return error.FailedToResolvePromise;
}
+ self.runMicrotasks();
return resolver.getPromise();
}
@@ -1174,7 +1172,10 @@ pub fn resolvePromise(self: *Context, value: anytype) !js.Promise {
const js_value = try self.zigValueToJs(value, .{});
var resolver = v8.PromiseResolver.init(ctx);
- _ = resolver.resolve(ctx, js_value);
+ if (resolver.resolve(ctx, js_value) == null) {
+ return error.FailedToResolvePromise;
+ }
+ self.runMicrotasks();
return resolver.getPromise();
}
@@ -1257,12 +1258,12 @@ pub fn dynamicModuleCallback(
const resource = self.jsStringToZigZ(.{ .handle = resource_name.? }, .{}) catch |err| {
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback1" });
- return @constCast(self.rejectPromise("Out of memory").handle);
+ return @constCast((self.rejectPromise("Out of memory") catch return null).handle);
};
const specifier = self.jsStringToZigZ(.{ .handle = v8_specifier.? }, .{}) catch |err| {
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback2" });
- return @constCast(self.rejectPromise("Out of memory").handle);
+ return @constCast((self.rejectPromise("Out of memory") catch return null).handle);
};
const normalized_specifier = self.script_manager.?.resolveSpecifier(
@@ -1271,14 +1272,14 @@ pub fn dynamicModuleCallback(
specifier,
) catch |err| {
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback3" });
- return @constCast(self.rejectPromise("Out of memory").handle);
+ return @constCast((self.rejectPromise("Out of memory") catch return null).handle);
};
const promise = self._dynamicModuleCallback(normalized_specifier, resource) catch |err| blk: {
log.err(.js, "dynamic module callback", .{
.err = err,
});
- break :blk self.rejectPromise("Failed to load module");
+ break :blk self.rejectPromise("Failed to load module") catch return null;
};
return @constCast(promise.handle);
}
diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig
index 052c1916..505eb6e8 100644
--- a/src/browser/js/Env.zig
+++ b/src/browser/js/Env.zig
@@ -270,7 +270,10 @@ pub fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.Funct
bridge.Iterator => {
// Same as a function, but with a specific name
const function_template = v8.FunctionTemplate.initCallback(isolate, value.func);
- const js_name = v8.Symbol.getIterator(isolate).toName();
+ const js_name = if (value.async)
+ v8.Symbol.getAsyncIterator(isolate).toName()
+ else
+ v8.Symbol.getIterator(isolate).toName();
template_proto.set(js_name, function_template, v8.PropertyAttribute.None);
},
bridge.Property => {
diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig
index d3d983d4..73b588a1 100644
--- a/src/browser/js/bridge.zig
+++ b/src/browser/js/bridge.zig
@@ -284,28 +284,36 @@ pub const NamedIndexed = struct {
pub const Iterator = struct {
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
+ async: bool,
- const Opts = struct {};
+ const Opts = struct {
+ async: bool = false,
+ };
fn init(comptime T: type, comptime struct_or_func: anytype, comptime opts: Opts) Iterator {
- _ = opts;
if (@typeInfo(@TypeOf(struct_or_func)) == .type) {
- return .{ .func = struct {
- fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
- const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
- info.getReturnValue().set(info.getThis());
- }
- }.wrap };
+ return .{
+ .async = opts.async,
+ .func = struct {
+ fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
+ const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
+ info.getReturnValue().set(info.getThis());
+ }
+ }.wrap,
+ };
}
- return .{ .func = struct {
- fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
- const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
- var caller = Caller.init(info);
- defer caller.deinit();
- caller.method(T, struct_or_func, info, .{});
- }
- }.wrap };
+ return .{
+ .async = opts.async,
+ .func = struct {
+ fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
+ const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
+ var caller = Caller.init(info);
+ defer caller.deinit();
+ caller.method(T, struct_or_func, info, .{});
+ }
+ }.wrap,
+ };
}
};
@@ -564,6 +572,9 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/net/URLSearchParams.zig"),
@import("../webapi/net/XMLHttpRequest.zig"),
@import("../webapi/net/XMLHttpRequestEventTarget.zig"),
+ @import("../webapi/streams/ReadableStream.zig"),
+ @import("../webapi/streams/ReadableStreamDefaultReader.zig"),
+ @import("../webapi/streams/ReadableStreamDefaultController.zig"),
@import("../webapi/Node.zig"),
@import("../webapi/storage/storage.zig"),
@import("../webapi/URL.zig"),
diff --git a/src/browser/tests/streams/readable_stream.html b/src/browser/tests/streams/readable_stream.html
new file mode 100644
index 00000000..2c74697b
--- /dev/null
+++ b/src/browser/tests/streams/readable_stream.html
@@ -0,0 +1,192 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/webapi/MessageChannel.zig b/src/browser/webapi/MessageChannel.zig
index 76631013..d43ba7df 100644
--- a/src/browser/webapi/MessageChannel.zig
+++ b/src/browser/webapi/MessageChannel.zig
@@ -37,7 +37,6 @@ pub fn init(page: *Page) !*MessageChannel {
});
}
-
pub fn getPort1(self: *const MessageChannel) *MessagePort {
return self._port1;
}
diff --git a/src/browser/webapi/MessagePort.zig b/src/browser/webapi/MessagePort.zig
index 65a7d36b..4d72a934 100644
--- a/src/browser/webapi/MessagePort.zig
+++ b/src/browser/webapi/MessagePort.zig
@@ -134,7 +134,7 @@ const PostMessageCallback = struct {
.origin = "",
.source = null,
}, self.page) catch |err| {
- log.err(.dom, "MessagePort.postMessage", .{.err = err});
+ log.err(.dom, "MessagePort.postMessage", .{ .err = err });
return null;
};
@@ -144,7 +144,7 @@ const PostMessageCallback = struct {
self.port._on_message,
.{ .context = "MessagePort message" },
) catch |err| {
- log.err(.dom, "MessagePort.postMessage", .{.err = err});
+ log.err(.dom, "MessagePort.postMessage", .{ .err = err });
};
return null;
diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig
index ba3683b6..84fae9f8 100644
--- a/src/browser/webapi/Window.zig
+++ b/src/browser/webapi/Window.zig
@@ -265,13 +265,12 @@ pub fn postMessage(self: *Window, message: js.Object, target_origin: ?[]const u8
const origin = try self._location.getOrigin(page);
const callback = try page._factory.create(PostMessageCallback{
.window = self,
- .message = try message.persist() ,
+ .message = try message.persist(),
.origin = try page.arena.dupe(u8, origin),
.page = page,
});
errdefer page._factory.destroy(callback);
-
try page.scheduler.add(callback, PostMessageCallback.run, 0, .{
.name = "postMessage",
.low_priority = false,
diff --git a/src/browser/webapi/net/Response.zig b/src/browser/webapi/net/Response.zig
index bc66fb00..9e79072b 100644
--- a/src/browser/webapi/net/Response.zig
+++ b/src/browser/webapi/net/Response.zig
@@ -21,6 +21,7 @@ const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Headers = @import("Headers.zig");
+const ReadableStream = @import("../streams/ReadableStream.zig");
const Allocator = std.mem.Allocator;
const Response = @This();
@@ -28,7 +29,7 @@ const Response = @This();
_status: u16,
_arena: Allocator,
_headers: *Headers,
-_body: []const u8,
+_body: ?[]const u8,
const InitOpts = struct {
status: u16 = 200,
@@ -39,10 +40,13 @@ const InitOpts = struct {
pub fn init(body_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*Response {
const opts = opts_ orelse InitOpts{};
+ // Store empty string as empty string, not null
+ const body = if (body_) |b| try page.arena.dupe(u8, b) else null;
+
return page._factory.create(Response{
._arena = page.arena,
._status = opts.status,
- ._body = if (body_) |b| try page.arena.dupe(u8, b) else "",
+ ._body = body,
._headers = opts.headers orelse try Headers.init(page),
});
}
@@ -55,15 +59,34 @@ pub fn getHeaders(self: *const Response) *Headers {
return self._headers;
}
+pub fn getBody(self: *const Response, page: *Page) !?*ReadableStream {
+ const body = self._body orelse return null;
+
+ // Empty string should create a closed stream with no data
+ if (body.len == 0) {
+ const stream = try ReadableStream.init(page);
+ try stream._controller.close();
+ return stream;
+ }
+
+ return ReadableStream.initWithData(body, page);
+}
+
pub fn isOK(self: *const Response) bool {
return self._status >= 200 and self._status <= 299;
}
+pub fn getText(self: *const Response, page: *Page) !js.Promise {
+ const body = self._body orelse "";
+ return page.js.resolvePromise(body);
+}
+
pub fn getJson(self: *Response, page: *Page) !js.Promise {
+ const body = self._body orelse "";
const value = std.json.parseFromSliceLeaky(
std.json.Value,
page.call_arena,
- self._body,
+ body,
.{},
) catch |err| {
return page.js.rejectPromise(.{@errorName(err)});
@@ -83,6 +106,8 @@ pub const JsApi = struct {
pub const constructor = bridge.constructor(Response.init, .{});
pub const ok = bridge.accessor(Response.isOK, null, .{});
pub const status = bridge.accessor(Response.getStatus, null, .{});
+ pub const text = bridge.function(Response.getText, .{});
pub const json = bridge.function(Response.getJson, .{});
pub const headers = bridge.accessor(Response.getHeaders, null, .{});
+ pub const body = bridge.accessor(Response.getBody, null, .{});
};
diff --git a/src/browser/webapi/net/URLSearchParams.zig b/src/browser/webapi/net/URLSearchParams.zig
index 8012f23b..9bdecd2e 100644
--- a/src/browser/webapi/net/URLSearchParams.zig
+++ b/src/browser/webapi/net/URLSearchParams.zig
@@ -45,13 +45,13 @@ pub fn init(opts_: ?InitOpts, page: *Page) !*URLSearchParams {
.query_string => |qs| break :blk try paramsFromString(arena, qs, &page.buf),
.value => |js_val| {
if (js_val.isObject()) {
- break :blk try paramsFromObject(arena, js_val.toObject());
+ break :blk try paramsFromObject(arena, js_val.toObject());
}
if (js_val.isString()) {
break :blk try paramsFromString(arena, try js_val.toString(arena), &page.buf);
}
return error.InvalidArgument;
- }
+ },
}
};
diff --git a/src/browser/webapi/streams/ReadableStream.zig b/src/browser/webapi/streams/ReadableStream.zig
new file mode 100644
index 00000000..eec8cb94
--- /dev/null
+++ b/src/browser/webapi/streams/ReadableStream.zig
@@ -0,0 +1,140 @@
+// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+const std = @import("std");
+const js = @import("../../js/js.zig");
+
+const Page = @import("../../Page.zig");
+const ReadableStreamDefaultReader = @import("ReadableStreamDefaultReader.zig");
+const ReadableStreamDefaultController = @import("ReadableStreamDefaultController.zig");
+
+pub fn registerTypes() []const type {
+ return &.{
+ ReadableStream,
+ AsyncIterator,
+ };
+}
+
+const ReadableStream = @This();
+
+pub const State = enum {
+ readable,
+ closed,
+ errored,
+};
+
+_page: *Page,
+_state: State,
+_reader: ?*ReadableStreamDefaultReader,
+_controller: *ReadableStreamDefaultController,
+_stored_error: ?[]const u8,
+
+pub fn init(page: *Page) !*ReadableStream {
+ const stream = try page._factory.create(ReadableStream{
+ ._page = page,
+ ._state = .readable,
+ ._reader = null,
+ ._controller = undefined,
+ ._stored_error = null,
+ });
+
+ stream._controller = try ReadableStreamDefaultController.init(stream, page);
+ return stream;
+}
+
+pub fn initWithData(data: []const u8, page: *Page) !*ReadableStream {
+ const stream = try init(page);
+
+ // For Phase 1: immediately enqueue all data and close
+ try stream._controller.enqueue(data);
+ try stream._controller.close();
+
+ return stream;
+}
+
+pub fn getReader(self: *ReadableStream, page: *Page) !*ReadableStreamDefaultReader {
+ if (self._reader != null) {
+ return error.ReaderLocked;
+ }
+
+ const reader = try ReadableStreamDefaultReader.init(self, page);
+ self._reader = reader;
+ return reader;
+}
+
+pub fn releaseReader(self: *ReadableStream) void {
+ self._reader = null;
+}
+
+pub fn getAsyncIterator(self: *ReadableStream, page: *Page) !*AsyncIterator {
+ return AsyncIterator.init(self, page);
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(ReadableStream);
+
+ pub const Meta = struct {
+ pub const name = "ReadableStream";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+
+ pub const constructor = bridge.constructor(ReadableStream.init, .{});
+ pub const getReader = bridge.function(ReadableStream.getReader, .{});
+ pub const symbol_async_iterator = bridge.iterator(ReadableStream.getAsyncIterator, .{ .async = true });
+};
+
+pub const AsyncIterator = struct {
+ _stream: *ReadableStream,
+ _reader: *ReadableStreamDefaultReader,
+
+ pub fn init(stream: *ReadableStream, page: *Page) !*AsyncIterator {
+ const reader = try stream.getReader(page);
+ return page._factory.create(AsyncIterator{
+ ._reader = reader,
+ ._stream = stream,
+ });
+ }
+
+ pub fn next(self: *AsyncIterator, page: *Page) !js.Promise {
+ return self._reader.read(page);
+ }
+
+ pub fn @"return"(self: *AsyncIterator, page: *Page) !js.Promise {
+ self._reader.releaseLock();
+ return page.js.resolvePromise(.{ .done = true, .value = null });
+ }
+
+ pub const JsApi = struct {
+ pub const bridge = js.Bridge(ReadableStream.AsyncIterator);
+
+ pub const Meta = struct {
+ pub const name = "ReadableStreamAsyncIterator";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+
+ pub const next = bridge.function(ReadableStream.AsyncIterator.next, .{});
+ pub const @"return" = bridge.function(ReadableStream.AsyncIterator.@"return", .{});
+ };
+};
+
+const testing = @import("../../../testing.zig");
+test "WebApi: ReadableStream" {
+ try testing.htmlRunner("streams/readable_stream.html", .{});
+}
diff --git a/src/browser/webapi/streams/ReadableStreamDefaultController.zig b/src/browser/webapi/streams/ReadableStreamDefaultController.zig
new file mode 100644
index 00000000..876f546a
--- /dev/null
+++ b/src/browser/webapi/streams/ReadableStreamDefaultController.zig
@@ -0,0 +1,100 @@
+// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+const std = @import("std");
+const js = @import("../../js/js.zig");
+
+const Page = @import("../../Page.zig");
+const ReadableStream = @import("ReadableStream.zig");
+
+const ReadableStreamDefaultController = @This();
+
+_page: *Page,
+_stream: *ReadableStream,
+_arena: std.mem.Allocator,
+_queue: std.ArrayList([]const u8),
+
+pub fn init(stream: *ReadableStream, page: *Page) !*ReadableStreamDefaultController {
+ return page._factory.create(ReadableStreamDefaultController{
+ ._page = page,
+ ._stream = stream,
+ ._arena = page.arena,
+ ._queue = std.ArrayList([]const u8){},
+ });
+}
+
+pub fn enqueue(self: *ReadableStreamDefaultController, chunk: []const u8) !void {
+ if (self._stream._state != .readable) {
+ return error.StreamNotReadable;
+ }
+
+ // Store a copy of the chunk in the page arena
+ const chunk_copy = try self._page.arena.dupe(u8, chunk);
+ try self._queue.append(self._arena, chunk_copy);
+}
+
+pub fn close(self: *ReadableStreamDefaultController) !void {
+ if (self._stream._state != .readable) {
+ return error.StreamNotReadable;
+ }
+
+ self._stream._state = .closed;
+}
+
+pub fn doError(self: *ReadableStreamDefaultController, err: []const u8) !void {
+ if (self._stream._state != .readable) {
+ return;
+ }
+
+ self._stream._state = .errored;
+ self._stream._stored_error = try self._page.arena.dupe(u8, err);
+}
+
+pub fn dequeue(self: *ReadableStreamDefaultController) ?[]const u8 {
+ if (self._queue.items.len == 0) {
+ return null;
+ }
+ return self._queue.orderedRemove(0);
+}
+
+pub fn getDesiredSize(self: *const ReadableStreamDefaultController) ?i32 {
+ switch (self._stream._state) {
+ .errored => return null,
+ .closed => return 0,
+ .readable => {
+ // For now, just report based on queue size
+ // In a real implementation, this would use highWaterMark
+ return @as(i32, 1) - @as(i32, @intCast(self._queue.items.len));
+ },
+ }
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(ReadableStreamDefaultController);
+
+ pub const Meta = struct {
+ pub const name = "ReadableStreamDefaultController";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+
+ pub const enqueue = bridge.function(ReadableStreamDefaultController.enqueue, .{});
+ pub const close = bridge.function(ReadableStreamDefaultController.close, .{});
+ pub const @"error" = bridge.function(ReadableStreamDefaultController.doError, .{});
+ pub const desiredSize = bridge.accessor(ReadableStreamDefaultController.getDesiredSize, null, .{});
+};
diff --git a/src/browser/webapi/streams/ReadableStreamDefaultReader.zig b/src/browser/webapi/streams/ReadableStreamDefaultReader.zig
new file mode 100644
index 00000000..7a531a3b
--- /dev/null
+++ b/src/browser/webapi/streams/ReadableStreamDefaultReader.zig
@@ -0,0 +1,107 @@
+// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+const std = @import("std");
+const js = @import("../../js/js.zig");
+
+const Page = @import("../../Page.zig");
+const ReadableStream = @import("ReadableStream.zig");
+
+const ReadableStreamDefaultReader = @This();
+
+_page: *Page,
+_stream: ?*ReadableStream,
+
+pub fn init(stream: *ReadableStream, page: *Page) !*ReadableStreamDefaultReader {
+ return page._factory.create(ReadableStreamDefaultReader{
+ ._stream = stream,
+ ._page = page,
+ });
+}
+
+pub const ReadResult = struct {
+ done: bool,
+ value: ?js.TypedArray(u8),
+};
+
+pub fn read(self: *ReadableStreamDefaultReader, page: *Page) !js.Promise {
+ const stream = self._stream orelse {
+ return page.js.rejectPromise("Reader has been released");
+ };
+
+ if (stream._state == .errored) {
+ const err = stream._stored_error orelse "Stream errored";
+ return page.js.rejectPromise(err);
+ }
+
+ if (stream._controller.dequeue()) |chunk| {
+ const result = ReadResult{
+ .done = false,
+ .value = js.TypedArray(u8){ .values = chunk },
+ };
+ return page.js.resolvePromise(result);
+ }
+
+ if (stream._state == .closed) {
+ const result = ReadResult{
+ .value = null,
+ .done = true,
+ };
+ return page.js.resolvePromise(result);
+ }
+
+ const result = ReadResult{
+ .done = true,
+ .value = null,
+ };
+ return page.js.resolvePromise(result);
+}
+
+pub fn releaseLock(self: *ReadableStreamDefaultReader) void {
+ if (self._stream) |stream| {
+ stream.releaseReader();
+ self._stream = null;
+ }
+}
+
+pub fn cancel(self: *ReadableStreamDefaultReader, reason_: ?[]const u8, page: *Page) !js.Promise {
+ const stream = self._stream orelse {
+ return page.js.rejectPromise("Reader has been released");
+ };
+
+ const reason = reason_ orelse "canceled";
+
+ try stream._controller.doError(reason);
+ self.releaseLock();
+
+ return page.js.resolvePromise(.{});
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(ReadableStreamDefaultReader);
+
+ pub const Meta = struct {
+ pub const name = "ReadableStreamDefaultReader";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+
+ pub const read = bridge.function(ReadableStreamDefaultReader.read, .{});
+ pub const cancel = bridge.function(ReadableStreamDefaultReader.cancel, .{});
+ pub const releaseLock = bridge.function(ReadableStreamDefaultReader.releaseLock, .{});
+};