mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-16 08:18:59 +00:00
ReadableStream
This commit is contained in:
@@ -752,11 +752,7 @@ const Script = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
log.debug(.browser, "executed script", .{
|
log.debug(.browser, "executed script", .{ .src = url, .success = success, .on_load = script_element._on_load != null });
|
||||||
.src = url,
|
|
||||||
.success = success,
|
|
||||||
.on_load = script_element._on_load != null
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defer page.tick();
|
defer page.tick();
|
||||||
|
|||||||
@@ -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 }) };
|
return .{ .string = try self.valueToString(js_value, .{ .allocator = self.arena }) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (comptime T == js.Value) {
|
if (comptime T == js.Value) {
|
||||||
// Caller wants an opaque js.Object. Probably a parameter
|
// Caller wants an opaque js.Object. Probably a parameter
|
||||||
// that it needs to pass back into a callback
|
// that it needs to pass back into a callback
|
||||||
@@ -1157,15 +1156,14 @@ pub fn stackTrace(self: *const Context) !?[]const u8 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// == Promise Helpers ==
|
// == 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;
|
const ctx = self.v8_context;
|
||||||
var resolver = v8.PromiseResolver.init(ctx);
|
var resolver = v8.PromiseResolver.init(ctx);
|
||||||
if (self.zigValueToJs(value, .{})) |js_value| {
|
const js_value = try self.zigValueToJs(value, .{});
|
||||||
_ = resolver.reject(ctx, js_value);
|
if (resolver.reject(ctx, js_value) == null) {
|
||||||
} else |err| {
|
return error.FailedToResolvePromise;
|
||||||
const str = self.isolate.initStringUtf8(@errorName(err));
|
|
||||||
_ = resolver.reject(ctx, str.toValue());
|
|
||||||
}
|
}
|
||||||
|
self.runMicrotasks();
|
||||||
return resolver.getPromise();
|
return resolver.getPromise();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1174,7 +1172,10 @@ pub fn resolvePromise(self: *Context, value: anytype) !js.Promise {
|
|||||||
const js_value = try self.zigValueToJs(value, .{});
|
const js_value = try self.zigValueToJs(value, .{});
|
||||||
|
|
||||||
var resolver = v8.PromiseResolver.init(ctx);
|
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();
|
return resolver.getPromise();
|
||||||
}
|
}
|
||||||
@@ -1257,12 +1258,12 @@ pub fn dynamicModuleCallback(
|
|||||||
|
|
||||||
const resource = self.jsStringToZigZ(.{ .handle = resource_name.? }, .{}) catch |err| {
|
const resource = self.jsStringToZigZ(.{ .handle = resource_name.? }, .{}) catch |err| {
|
||||||
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback1" });
|
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| {
|
const specifier = self.jsStringToZigZ(.{ .handle = v8_specifier.? }, .{}) catch |err| {
|
||||||
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback2" });
|
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(
|
const normalized_specifier = self.script_manager.?.resolveSpecifier(
|
||||||
@@ -1271,14 +1272,14 @@ pub fn dynamicModuleCallback(
|
|||||||
specifier,
|
specifier,
|
||||||
) catch |err| {
|
) catch |err| {
|
||||||
log.err(.app, "OOM", .{ .err = err, .src = "dynamicModuleCallback3" });
|
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: {
|
const promise = self._dynamicModuleCallback(normalized_specifier, resource) catch |err| blk: {
|
||||||
log.err(.js, "dynamic module callback", .{
|
log.err(.js, "dynamic module callback", .{
|
||||||
.err = err,
|
.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);
|
return @constCast(promise.handle);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -270,7 +270,10 @@ pub fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.Funct
|
|||||||
bridge.Iterator => {
|
bridge.Iterator => {
|
||||||
// Same as a function, but with a specific name
|
// Same as a function, but with a specific name
|
||||||
const function_template = v8.FunctionTemplate.initCallback(isolate, value.func);
|
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);
|
template_proto.set(js_name, function_template, v8.PropertyAttribute.None);
|
||||||
},
|
},
|
||||||
bridge.Property => {
|
bridge.Property => {
|
||||||
|
|||||||
@@ -284,28 +284,36 @@ pub const NamedIndexed = struct {
|
|||||||
|
|
||||||
pub const Iterator = struct {
|
pub const Iterator = struct {
|
||||||
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
|
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 {
|
fn init(comptime T: type, comptime struct_or_func: anytype, comptime opts: Opts) Iterator {
|
||||||
_ = opts;
|
|
||||||
if (@typeInfo(@TypeOf(struct_or_func)) == .type) {
|
if (@typeInfo(@TypeOf(struct_or_func)) == .type) {
|
||||||
return .{ .func = struct {
|
return .{
|
||||||
|
.async = opts.async,
|
||||||
|
.func = struct {
|
||||||
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
||||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
||||||
info.getReturnValue().set(info.getThis());
|
info.getReturnValue().set(info.getThis());
|
||||||
}
|
}
|
||||||
}.wrap };
|
}.wrap,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return .{ .func = struct {
|
return .{
|
||||||
|
.async = opts.async,
|
||||||
|
.func = struct {
|
||||||
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
||||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
||||||
var caller = Caller.init(info);
|
var caller = Caller.init(info);
|
||||||
defer caller.deinit();
|
defer caller.deinit();
|
||||||
caller.method(T, struct_or_func, info, .{});
|
caller.method(T, struct_or_func, info, .{});
|
||||||
}
|
}
|
||||||
}.wrap };
|
}.wrap,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -564,6 +572,9 @@ pub const JsApis = flattenTypes(&.{
|
|||||||
@import("../webapi/net/URLSearchParams.zig"),
|
@import("../webapi/net/URLSearchParams.zig"),
|
||||||
@import("../webapi/net/XMLHttpRequest.zig"),
|
@import("../webapi/net/XMLHttpRequest.zig"),
|
||||||
@import("../webapi/net/XMLHttpRequestEventTarget.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/Node.zig"),
|
||||||
@import("../webapi/storage/storage.zig"),
|
@import("../webapi/storage/storage.zig"),
|
||||||
@import("../webapi/URL.zig"),
|
@import("../webapi/URL.zig"),
|
||||||
|
|||||||
192
src/browser/tests/streams/readable_stream.html
Normal file
192
src/browser/tests/streams/readable_stream.html
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<script src="../testing.js"></script>
|
||||||
|
<script id=readable_stream_basic>
|
||||||
|
{
|
||||||
|
// Test basic stream creation
|
||||||
|
const stream = new ReadableStream();
|
||||||
|
testing.expectEqual('object', typeof stream);
|
||||||
|
testing.expectEqual('function', typeof stream.getReader);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=readable_stream_reader>
|
||||||
|
{
|
||||||
|
// Test getting a reader
|
||||||
|
const stream = new ReadableStream();
|
||||||
|
const reader = stream.getReader();
|
||||||
|
testing.expectEqual('object', typeof reader);
|
||||||
|
testing.expectEqual('function', typeof reader.read);
|
||||||
|
testing.expectEqual('function', typeof reader.releaseLock);
|
||||||
|
testing.expectEqual('function', typeof reader.cancel);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=readable_stream_read_empty>
|
||||||
|
(async function() {
|
||||||
|
const stream = new ReadableStream();
|
||||||
|
const reader = stream.getReader();
|
||||||
|
|
||||||
|
// Reading from empty stream should return done:true
|
||||||
|
const result = await reader.read();
|
||||||
|
testing.expectEqual('object', typeof result);
|
||||||
|
testing.expectEqual(true, result.done);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=response_body>
|
||||||
|
(async function() {
|
||||||
|
const response = new Response('hello world');
|
||||||
|
testing.expectEqual('object', typeof response.body);
|
||||||
|
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const result = await reader.read();
|
||||||
|
|
||||||
|
testing.expectEqual(false, result.done);
|
||||||
|
testing.expectEqual(true, result.value instanceof Uint8Array);
|
||||||
|
|
||||||
|
// Convert Uint8Array to string
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const text = decoder.decode(result.value);
|
||||||
|
testing.expectEqual('hello world', text);
|
||||||
|
|
||||||
|
// Next read should be done
|
||||||
|
const result2 = await reader.read();
|
||||||
|
testing.expectEqual(true, result2.done);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=response_text>
|
||||||
|
(async function() {
|
||||||
|
const response = new Response('hello from text()');
|
||||||
|
const text = await response.text();
|
||||||
|
testing.expectEqual('hello from text()', text);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=response_json>
|
||||||
|
(async function() {
|
||||||
|
const response = new Response('{"foo":"bar","num":42}');
|
||||||
|
const json = await response.json();
|
||||||
|
testing.expectEqual('object', typeof json);
|
||||||
|
testing.expectEqual('bar', json.foo);
|
||||||
|
testing.expectEqual(42, json.num);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=response_null_body>
|
||||||
|
{
|
||||||
|
// Response with no body should have null body
|
||||||
|
const response = new Response();
|
||||||
|
testing.expectEqual(null, response.body);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=response_empty_string_body>
|
||||||
|
(async function() {
|
||||||
|
// Response with empty string should have a stream that's immediately closed
|
||||||
|
const response = new Response('');
|
||||||
|
testing.expectEqual('object', typeof response.body);
|
||||||
|
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
const result = await reader.read();
|
||||||
|
|
||||||
|
// Stream should be closed immediately (done: true, no value)
|
||||||
|
testing.expectEqual(true, result.done);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=response_status>
|
||||||
|
(async function() {
|
||||||
|
const response = new Response('test', { status: 404 });
|
||||||
|
testing.expectEqual(404, response.status);
|
||||||
|
testing.expectEqual(false, response.ok);
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
testing.expectEqual('test', text);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=async_iterator_exists>
|
||||||
|
(async function() {
|
||||||
|
const stream = new ReadableStream();
|
||||||
|
testing.expectEqual('function', typeof stream[Symbol.asyncIterator]);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=async_iterator_basic>
|
||||||
|
(async function() {
|
||||||
|
const stream = new ReadableStream();
|
||||||
|
const iterator = stream[Symbol.asyncIterator]();
|
||||||
|
testing.expectEqual('object', typeof iterator);
|
||||||
|
testing.expectEqual('function', typeof iterator.next);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=async_iterator_for_await>
|
||||||
|
(async function() {
|
||||||
|
const response = new Response('test data');
|
||||||
|
const stream = response.body;
|
||||||
|
|
||||||
|
const chunks = [];
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
chunks.push(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
testing.expectEqual(1, chunks.length);
|
||||||
|
testing.expectEqual(true, chunks[0] instanceof Uint8Array);
|
||||||
|
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const text = decoder.decode(chunks[0]);
|
||||||
|
testing.expectEqual('test data', text);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=async_iterator_locks_stream>
|
||||||
|
(async function() {
|
||||||
|
const response = new Response('test');
|
||||||
|
const stream = response.body;
|
||||||
|
|
||||||
|
// Get async iterator (locks stream)
|
||||||
|
const iterator = stream[Symbol.asyncIterator]();
|
||||||
|
|
||||||
|
// Try to get reader - should fail
|
||||||
|
let errorThrown = false;
|
||||||
|
try {
|
||||||
|
stream.getReader();
|
||||||
|
} catch (e) {
|
||||||
|
errorThrown = true;
|
||||||
|
}
|
||||||
|
testing.expectEqual(true, errorThrown);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=async_iterator_manual_next>
|
||||||
|
(async function() {
|
||||||
|
const response = new Response('hello');
|
||||||
|
const stream = response.body;
|
||||||
|
const iterator = stream[Symbol.asyncIterator]();
|
||||||
|
|
||||||
|
const result = await iterator.next();
|
||||||
|
testing.expectEqual('object', typeof result);
|
||||||
|
testing.expectEqual(false, result.done);
|
||||||
|
testing.expectEqual(true, result.value instanceof Uint8Array);
|
||||||
|
|
||||||
|
// Second call should be done
|
||||||
|
const result2 = await iterator.next();
|
||||||
|
testing.expectEqual(true, result2.done);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id=async_iterator_early_break>
|
||||||
|
(async function() {
|
||||||
|
const response = new Response('test data');
|
||||||
|
const stream = response.body;
|
||||||
|
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
break; // Early exit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should not throw errors
|
||||||
|
testing.expectEqual('object', typeof stream);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -37,7 +37,6 @@ pub fn init(page: *Page) !*MessageChannel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn getPort1(self: *const MessageChannel) *MessagePort {
|
pub fn getPort1(self: *const MessageChannel) *MessagePort {
|
||||||
return self._port1;
|
return self._port1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -271,7 +271,6 @@ pub fn postMessage(self: *Window, message: js.Object, target_origin: ?[]const u8
|
|||||||
});
|
});
|
||||||
errdefer page._factory.destroy(callback);
|
errdefer page._factory.destroy(callback);
|
||||||
|
|
||||||
|
|
||||||
try page.scheduler.add(callback, PostMessageCallback.run, 0, .{
|
try page.scheduler.add(callback, PostMessageCallback.run, 0, .{
|
||||||
.name = "postMessage",
|
.name = "postMessage",
|
||||||
.low_priority = false,
|
.low_priority = false,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const js = @import("../../js/js.zig");
|
|||||||
|
|
||||||
const Page = @import("../../Page.zig");
|
const Page = @import("../../Page.zig");
|
||||||
const Headers = @import("Headers.zig");
|
const Headers = @import("Headers.zig");
|
||||||
|
const ReadableStream = @import("../streams/ReadableStream.zig");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const Response = @This();
|
const Response = @This();
|
||||||
@@ -28,7 +29,7 @@ const Response = @This();
|
|||||||
_status: u16,
|
_status: u16,
|
||||||
_arena: Allocator,
|
_arena: Allocator,
|
||||||
_headers: *Headers,
|
_headers: *Headers,
|
||||||
_body: []const u8,
|
_body: ?[]const u8,
|
||||||
|
|
||||||
const InitOpts = struct {
|
const InitOpts = struct {
|
||||||
status: u16 = 200,
|
status: u16 = 200,
|
||||||
@@ -39,10 +40,13 @@ const InitOpts = struct {
|
|||||||
pub fn init(body_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*Response {
|
pub fn init(body_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*Response {
|
||||||
const opts = opts_ orelse InitOpts{};
|
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{
|
return page._factory.create(Response{
|
||||||
._arena = page.arena,
|
._arena = page.arena,
|
||||||
._status = opts.status,
|
._status = opts.status,
|
||||||
._body = if (body_) |b| try page.arena.dupe(u8, b) else "",
|
._body = body,
|
||||||
._headers = opts.headers orelse try Headers.init(page),
|
._headers = opts.headers orelse try Headers.init(page),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -55,15 +59,34 @@ pub fn getHeaders(self: *const Response) *Headers {
|
|||||||
return self._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 {
|
pub fn isOK(self: *const Response) bool {
|
||||||
return self._status >= 200 and self._status <= 299;
|
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 {
|
pub fn getJson(self: *Response, page: *Page) !js.Promise {
|
||||||
|
const body = self._body orelse "";
|
||||||
const value = std.json.parseFromSliceLeaky(
|
const value = std.json.parseFromSliceLeaky(
|
||||||
std.json.Value,
|
std.json.Value,
|
||||||
page.call_arena,
|
page.call_arena,
|
||||||
self._body,
|
body,
|
||||||
.{},
|
.{},
|
||||||
) catch |err| {
|
) catch |err| {
|
||||||
return page.js.rejectPromise(.{@errorName(err)});
|
return page.js.rejectPromise(.{@errorName(err)});
|
||||||
@@ -83,6 +106,8 @@ pub const JsApi = struct {
|
|||||||
pub const constructor = bridge.constructor(Response.init, .{});
|
pub const constructor = bridge.constructor(Response.init, .{});
|
||||||
pub const ok = bridge.accessor(Response.isOK, null, .{});
|
pub const ok = bridge.accessor(Response.isOK, null, .{});
|
||||||
pub const status = bridge.accessor(Response.getStatus, 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 json = bridge.function(Response.getJson, .{});
|
||||||
pub const headers = bridge.accessor(Response.getHeaders, null, .{});
|
pub const headers = bridge.accessor(Response.getHeaders, null, .{});
|
||||||
|
pub const body = bridge.accessor(Response.getBody, null, .{});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ pub fn init(opts_: ?InitOpts, page: *Page) !*URLSearchParams {
|
|||||||
break :blk try paramsFromString(arena, try js_val.toString(arena), &page.buf);
|
break :blk try paramsFromString(arena, try js_val.toString(arena), &page.buf);
|
||||||
}
|
}
|
||||||
return error.InvalidArgument;
|
return error.InvalidArgument;
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
140
src/browser/webapi/streams/ReadableStream.zig
Normal file
140
src/browser/webapi/streams/ReadableStream.zig
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// 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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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", .{});
|
||||||
|
}
|
||||||
100
src/browser/webapi/streams/ReadableStreamDefaultController.zig
Normal file
100
src/browser/webapi/streams/ReadableStreamDefaultController.zig
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// 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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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, .{});
|
||||||
|
};
|
||||||
107
src/browser/webapi/streams/ReadableStreamDefaultReader.zig
Normal file
107
src/browser/webapi/streams/ReadableStreamDefaultReader.zig
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
|
||||||
|
//
|
||||||
|
// Francis Bouvier <francis@lightpanda.io>
|
||||||
|
// Pierre Tachoire <pierre@lightpanda.io>
|
||||||
|
//
|
||||||
|
// 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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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, .{});
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user