From 0749f60702478dcaf1afa21c643fafc1cd6f22f0 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Mon, 2 Mar 2026 14:24:49 +0100 Subject: [PATCH] Preserve chunk value types through ReadableStream enqueue/read When JS called controller.enqueue(42), the value was coerced to the string "42" because Chunk only had uint8array and string variants. Add a js_value variant that persists the raw JS value handle, and expose enqueueValue(js.Value) as the JS-facing enqueue method so numbers, booleans, and objects round-trip with their original types. --- .../tests/streams/readable_stream.html | 71 +++++++++++++++++++ .../ReadableStreamDefaultController.zig | 38 +++++++++- .../streams/ReadableStreamDefaultReader.zig | 2 + .../webapi/streams/TransformStream.zig | 7 +- 4 files changed, 116 insertions(+), 2 deletions(-) diff --git a/src/browser/tests/streams/readable_stream.html b/src/browser/tests/streams/readable_stream.html index 3d00d6cf..c82fd985 100644 --- a/src/browser/tests/streams/readable_stream.html +++ b/src/browser/tests/streams/readable_stream.html @@ -301,3 +301,74 @@ testing.expectEqual(false, data3.done); })(); + + + + + + diff --git a/src/browser/webapi/streams/ReadableStreamDefaultController.zig b/src/browser/webapi/streams/ReadableStreamDefaultController.zig index c2f750bc..18228475 100644 --- a/src/browser/webapi/streams/ReadableStreamDefaultController.zig +++ b/src/browser/webapi/streams/ReadableStreamDefaultController.zig @@ -33,11 +33,13 @@ pub const Chunk = union(enum) { // the order matters, sorry. uint8array: js.TypedArray(u8), string: []const u8, + js_value: js.Value.Global, pub fn dupe(self: Chunk, allocator: std.mem.Allocator) !Chunk { return switch (self) { .string => |str| .{ .string = try allocator.dupe(u8, str) }, .uint8array => |arr| .{ .uint8array = try arr.dupe(allocator) }, + .js_value => |val| .{ .js_value = val }, }; } }; @@ -98,6 +100,40 @@ pub fn enqueue(self: *ReadableStreamDefaultController, chunk: Chunk) !void { ls.toLocal(resolver).resolve("stream enqueue", result); } +/// Enqueue a raw JS value, preserving its type (number, bool, object, etc.). +/// Used by the JS-facing API; internal Zig callers should use enqueue(Chunk). +pub fn enqueueValue(self: *ReadableStreamDefaultController, value: js.Value) !void { + if (self._stream._state != .readable) { + return error.StreamNotReadable; + } + + if (self._pending_reads.items.len == 0) { + const persisted = try value.persist(); + try self._queue.append(self._arena, .{ .js_value = persisted }); + return; + } + + const resolver = self._pending_reads.orderedRemove(0); + const persisted = try value.persist(); + const result = ReadableStreamDefaultReader.ReadResult{ + .done = false, + .value = .{ .js_value = persisted }, + }; + + if (comptime IS_DEBUG) { + if (self._page.js.local == null) { + log.fatal(.bug, "null context scope", .{ .src = "ReadableStreamDefaultController.enqueueValue", .url = self._page.url }); + std.debug.assert(self._page.js.local != null); + } + } + + var ls: js.Local.Scope = undefined; + self._page.js.localScope(&ls); + defer ls.deinit(); + + ls.toLocal(resolver).resolve("stream enqueue value", result); +} + pub fn close(self: *ReadableStreamDefaultController) !void { if (self._stream._state != .readable) { return error.StreamNotReadable; @@ -176,7 +212,7 @@ pub const JsApi = struct { pub var class_id: bridge.ClassId = undefined; }; - pub const enqueue = bridge.function(ReadableStreamDefaultController.enqueue, .{}); + pub const enqueue = bridge.function(ReadableStreamDefaultController.enqueueValue, .{}); 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 index 053080c4..2d3c5bbe 100644 --- a/src/browser/webapi/streams/ReadableStreamDefaultReader.zig +++ b/src/browser/webapi/streams/ReadableStreamDefaultReader.zig @@ -44,11 +44,13 @@ pub const ReadResult = struct { empty, string: []const u8, uint8array: js.TypedArray(u8), + js_value: js.Value.Global, pub fn fromChunk(chunk: ReadableStreamDefaultController.Chunk) Chunk { return switch (chunk) { .string => |s| .{ .string = s }, .uint8array => |arr| .{ .uint8array = arr }, + .js_value => |val| .{ .js_value = val }, }; } }; diff --git a/src/browser/webapi/streams/TransformStream.zig b/src/browser/webapi/streams/TransformStream.zig index 6e51515d..c0ca34b6 100644 --- a/src/browser/webapi/streams/TransformStream.zig +++ b/src/browser/webapi/streams/TransformStream.zig @@ -178,6 +178,11 @@ pub const TransformStreamDefaultController = struct { try self._stream._readable._controller.enqueue(chunk); } + /// Enqueue a raw JS value, preserving its type. Used by the JS-facing API. + pub fn enqueueValue(self: *TransformStreamDefaultController, value: js.Value) !void { + try self._stream._readable._controller.enqueueValue(value); + } + pub fn doError(self: *TransformStreamDefaultController, reason: []const u8) !void { try self._stream._readable._controller.doError(reason); } @@ -195,7 +200,7 @@ pub const TransformStreamDefaultController = struct { pub var class_id: bridge.ClassId = undefined; }; - pub const enqueue = bridge.function(TransformStreamDefaultController.enqueue, .{}); + pub const enqueue = bridge.function(TransformStreamDefaultController.enqueueValue, .{}); pub const @"error" = bridge.function(TransformStreamDefaultController.doError, .{}); pub const terminate = bridge.function(TransformStreamDefaultController.terminate, .{}); };