mirror of
				https://github.com/lightpanda-io/browser.git
				synced 2025-10-29 15:13:28 +00:00 
			
		
		
		
	Merge pull request #903 from lightpanda-io/MessageChannel
Add MessageChannel
This commit is contained in:
		| @@ -1,4 +1,4 @@ | ||||
| // Copyright (C) 2023-2025s  Lightpanda (Selecy SAS) | ||||
| // Copyright (C) 2023-2025  Lightpanda (Selecy SAS) | ||||
| // | ||||
| // Francis Bouvier <francis@lightpanda.io> | ||||
| // Pierre Tachoire <pierre@lightpanda.io> | ||||
|   | ||||
							
								
								
									
										361
									
								
								src/browser/dom/MessageChannel.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										361
									
								
								src/browser/dom/MessageChannel.zig
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,361 @@ | ||||
| // 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 log = @import("../../log.zig"); | ||||
| const parser = @import("../netsurf.zig"); | ||||
|  | ||||
| const Env = @import("../env.zig").Env; | ||||
| const Page = @import("../page.zig").Page; | ||||
| const EventTarget = @import("../dom/event_target.zig").EventTarget; | ||||
| const EventHandler = @import("../events/event.zig").EventHandler; | ||||
|  | ||||
| const JsObject = Env.JsObject; | ||||
| const Function = Env.Function; | ||||
| const Allocator = std.mem.Allocator; | ||||
|  | ||||
| const MAX_QUEUE_SIZE = 10; | ||||
|  | ||||
| pub const Interfaces = .{ MessageChannel, MessagePort }; | ||||
|  | ||||
| const MessageChannel = @This(); | ||||
|  | ||||
| port1: *MessagePort, | ||||
| port2: *MessagePort, | ||||
|  | ||||
| pub fn constructor(page: *Page) !MessageChannel { | ||||
|     // Why do we allocate this rather than storing directly in the struct? | ||||
|     // https://github.com/lightpanda-io/project/discussions/165 | ||||
|     const port1 = try page.arena.create(MessagePort); | ||||
|     const port2 = try page.arena.create(MessagePort); | ||||
|     port1.* = .{ | ||||
|         .pair = port2, | ||||
|     }; | ||||
|     port2.* = .{ | ||||
|         .pair = port1, | ||||
|     }; | ||||
|  | ||||
|     return .{ | ||||
|         .port1 = port1, | ||||
|         .port2 = port2, | ||||
|     }; | ||||
| } | ||||
|  | ||||
| pub fn get_port1(self: *const MessageChannel) *MessagePort { | ||||
|     return self.port1; | ||||
| } | ||||
|  | ||||
| pub fn get_port2(self: *const MessageChannel) *MessagePort { | ||||
|     return self.port2; | ||||
| } | ||||
|  | ||||
| pub const MessagePort = struct { | ||||
|     pub const prototype = *EventTarget; | ||||
|  | ||||
|     proto: parser.EventTargetTBase = .{ .internal_target_type = .message_port }, | ||||
|  | ||||
|     pair: *MessagePort, | ||||
|     closed: bool = false, | ||||
|     started: bool = false, | ||||
|     onmessage_cbk: ?Function = null, | ||||
|     onmessageerror_cbk: ?Function = null, | ||||
|     // This is the queue of messages to dispatch to THIS MessagePort when the | ||||
|     // MessagePort is started. | ||||
|     queue: std.ArrayListUnmanaged(JsObject) = .empty, | ||||
|  | ||||
|     pub const PostMessageOption = union(enum) { | ||||
|         transfer: JsObject, | ||||
|         options: Opts, | ||||
|  | ||||
|         pub const Opts = struct { | ||||
|             transfer: JsObject, | ||||
|         }; | ||||
|     }; | ||||
|  | ||||
|     pub fn _postMessage(self: *MessagePort, obj: JsObject, opts_: ?PostMessageOption, page: *Page) !void { | ||||
|         if (self.closed) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (opts_ != null) { | ||||
|             log.warn(.web_api, "not implemented", .{ .feature = "MessagePort postMessage options" }); | ||||
|             return error.NotImplemented; | ||||
|         } | ||||
|  | ||||
|         try self.pair.dispatchOrQueue(obj, page.arena); | ||||
|     } | ||||
|  | ||||
|     // Start impacts the ability to receive a message. | ||||
|     // Given pair1 (started) and pair2 (not started), then: | ||||
|     //    pair2.postMessage('x'); //will be dispatched to pair1.onmessage | ||||
|     //    pair1.postMessage('x'); // will be queued until pair2 is started | ||||
|     pub fn _start(self: *MessagePort) !void { | ||||
|         if (self.started) { | ||||
|             return; | ||||
|         } | ||||
|         self.started = true; | ||||
|         for (self.queue.items) |data| { | ||||
|             try self.dispatch(data); | ||||
|         } | ||||
|         // we'll never use this queue again, but it's allocated with an arena | ||||
|         // we don't even need to clear it, but it seems a bit safer to do at | ||||
|         // least that | ||||
|         self.queue.clearRetainingCapacity(); | ||||
|     } | ||||
|  | ||||
|     // Closing seems to stop both the publishing and receiving of messages, | ||||
|     // effectively rendering the channel useless. It cannot be reversed. | ||||
|     pub fn _close(self: *MessagePort) void { | ||||
|         self.closed = true; | ||||
|         self.pair.closed = true; | ||||
|     } | ||||
|  | ||||
|     pub fn get_onmessage(self: *MessagePort) ?Function { | ||||
|         return self.onmessage_cbk; | ||||
|     } | ||||
|     pub fn get_onmessageerror(self: *MessagePort) ?Function { | ||||
|         return self.onmessageerror_cbk; | ||||
|     } | ||||
|  | ||||
|     pub fn set_onmessage(self: *MessagePort, listener: EventHandler.Listener, page: *Page) !void { | ||||
|         if (self.onmessage_cbk) |cbk| { | ||||
|             try self.unregister("message", cbk.id); | ||||
|         } | ||||
|         self.onmessage_cbk = try self.register(page.arena, "message", listener); | ||||
|  | ||||
|         // When onmessage is set directly, then it's like start() was called. | ||||
|         // If addEventListener('message') is used, the app has to call start() | ||||
|         // explicitly. | ||||
|         try self._start(); | ||||
|     } | ||||
|  | ||||
|     pub fn set_onmessageerror(self: *MessagePort, listener: EventHandler.Listener, page: *Page) !void { | ||||
|         if (self.onmessageerror_cbk) |cbk| { | ||||
|             try self.unregister("messageerror", cbk.id); | ||||
|         } | ||||
|         self.onmessageerror_cbk = try self.register(page.arena, "messageerror", listener); | ||||
|     } | ||||
|  | ||||
|     // called from our pair. If port1.postMessage("x") is called, then this | ||||
|     // will be called on port2. | ||||
|     fn dispatchOrQueue(self: *MessagePort, obj: JsObject, arena: Allocator) !void { | ||||
|         // our pair should have checked this already | ||||
|         std.debug.assert(self.closed == false); | ||||
|  | ||||
|         if (self.started) { | ||||
|             return self.dispatch(try obj.persist()); | ||||
|         } | ||||
|  | ||||
|         if (self.queue.items.len > MAX_QUEUE_SIZE) { | ||||
|             // This isn't part of the spec, but not putting a limit is reckless | ||||
|             return error.MessageQueueLimit; | ||||
|         } | ||||
|         return self.queue.append(arena, try obj.persist()); | ||||
|     } | ||||
|  | ||||
|     fn dispatch(self: *MessagePort, obj: JsObject) !void { | ||||
|         // obj is already persisted, don't use `MessageEvent.constructor`, but | ||||
|         // go directly to `init`, which assumes persisted objects. | ||||
|         var evt = try MessageEvent.init(.{ .data = obj }); | ||||
|         _ = try parser.eventTargetDispatchEvent( | ||||
|             parser.toEventTarget(MessagePort, self), | ||||
|             @as(*parser.Event, @ptrCast(&evt)), | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     fn register( | ||||
|         self: *MessagePort, | ||||
|         alloc: Allocator, | ||||
|         typ: []const u8, | ||||
|         listener: EventHandler.Listener, | ||||
|     ) !?Function { | ||||
|         const target = @as(*parser.EventTarget, @ptrCast(self)); | ||||
|         const eh = (try EventHandler.register(alloc, target, typ, listener, null)) orelse unreachable; | ||||
|         return eh.callback; | ||||
|     } | ||||
|  | ||||
|     fn unregister(self: *MessagePort, typ: []const u8, cbk_id: usize) !void { | ||||
|         const et = @as(*parser.EventTarget, @ptrCast(self)); | ||||
|         const lst = try parser.eventTargetHasListener(et, typ, false, cbk_id); | ||||
|         if (lst == null) { | ||||
|             return; | ||||
|         } | ||||
|         try parser.eventTargetRemoveEventListener(et, typ, lst.?, false); | ||||
|     } | ||||
| }; | ||||
|  | ||||
| pub const MessageEvent = struct { | ||||
|     const Event = @import("../events/event.zig").Event; | ||||
|     const DOMException = @import("exceptions.zig").DOMException; | ||||
|  | ||||
|     pub const prototype = *Event; | ||||
|     pub const Exception = DOMException; | ||||
|     pub const union_make_copy = true; | ||||
|  | ||||
|     proto: parser.Event, | ||||
|     data: ?JsObject, | ||||
|  | ||||
|     // You would think if port1 sends to port2, the source would be port2 | ||||
|     // (which is how I read the documentation), but it appears to always be | ||||
|     // null. It can always be set explicitly via the constructor; | ||||
|     source: ?JsObject, | ||||
|  | ||||
|     origin: []const u8, | ||||
|  | ||||
|     // This is used for Server-Sent events. Appears to always be an empty | ||||
|     // string for MessagePort messages. | ||||
|     last_event_id: []const u8, | ||||
|  | ||||
|     // This might be related to the "transfer" option of postMessage which | ||||
|     // we don't yet support. For "normal" message, it's always an empty array. | ||||
|     // Though it could be set explicitly via the constructor | ||||
|     ports: []*MessagePort, | ||||
|  | ||||
|     const Options = struct { | ||||
|         data: ?JsObject = null, | ||||
|         source: ?JsObject = null, | ||||
|         origin: []const u8 = "", | ||||
|         lastEventId: []const u8 = "", | ||||
|         ports: []*MessagePort = &.{}, | ||||
|     }; | ||||
|  | ||||
|     pub fn constructor(opts: Options) !MessageEvent { | ||||
|         return init(.{ | ||||
|             .data = if (opts.data) |obj| try obj.persist() else null, | ||||
|             .source = if (opts.source) |obj| try obj.persist() else null, | ||||
|             .ports = opts.ports, | ||||
|             .origin = opts.origin, | ||||
|             .lastEventId = opts.lastEventId, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     // This is like "constructor", but it assumes JsObjects have already been | ||||
|     // persisted. Necessary because this `new MessageEvent()` can be called | ||||
|     // directly from JS OR from a port.postMessage. In the latter case, data | ||||
|     // may have already been persisted (as it might need to be queued); | ||||
|     fn init(opts: Options) !MessageEvent { | ||||
|         const event = try parser.eventCreate(); | ||||
|         defer parser.eventDestroy(event); | ||||
|         try parser.eventInit(event, "message", .{}); | ||||
|         try parser.eventSetInternalType(event, .message_event); | ||||
|  | ||||
|         return .{ | ||||
|             .proto = event.*, | ||||
|             .data = opts.data, | ||||
|             .source = opts.source, | ||||
|             .ports = opts.ports, | ||||
|             .origin = opts.origin, | ||||
|             .last_event_id = opts.lastEventId, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     pub fn get_data(self: *const MessageEvent) !?JsObject { | ||||
|         return self.data; | ||||
|     } | ||||
|  | ||||
|     pub fn get_origin(self: *const MessageEvent) []const u8 { | ||||
|         return self.origin; | ||||
|     } | ||||
|  | ||||
|     pub fn get_source(self: *const MessageEvent) ?JsObject { | ||||
|         return self.source; | ||||
|     } | ||||
|  | ||||
|     pub fn get_ports(self: *const MessageEvent) []*MessagePort { | ||||
|         return self.ports; | ||||
|     } | ||||
|  | ||||
|     pub fn get_lastEventId(self: *const MessageEvent) []const u8 { | ||||
|         return self.last_event_id; | ||||
|     } | ||||
| }; | ||||
|  | ||||
| const testing = @import("../../testing.zig"); | ||||
| test "Browser.MessageChannel" { | ||||
|     var runner = try testing.jsRunner(testing.tracking_allocator, .{ | ||||
|         .html = "", | ||||
|     }); | ||||
|     defer runner.deinit(); | ||||
|  | ||||
|     try runner.testCases(&.{ | ||||
|         .{ "const mc1 = new MessageChannel()", null }, | ||||
|         .{ "mc1.port1 == mc1.port1", "true" }, | ||||
|         .{ "mc1.port2 == mc1.port2", "true" }, | ||||
|         .{ "mc1.port1 != mc1.port2", "true" }, | ||||
|         .{ "mc1.port1.postMessage('msg1');", "undefined" }, | ||||
|         .{ | ||||
|             \\ let message = null; | ||||
|             \\ let target = null; | ||||
|             \\ let currentTarget = null; | ||||
|             \\ mc1.port2.onmessage = (e) => { | ||||
|             \\   message = e.data; | ||||
|             \\   target = e.target; | ||||
|             \\   currentTarget = e.currentTarget; | ||||
|             \\ }; | ||||
|             , | ||||
|             null, | ||||
|         }, | ||||
|         //  as soon as onmessage is called, queued messages are delivered | ||||
|         .{ "message", "msg1" }, | ||||
|         .{ "target == mc1.port2", "true" }, | ||||
|         .{ "currentTarget == mc1.port2", "true" }, | ||||
|  | ||||
|         .{ "mc1.port1.postMessage('msg2');", "undefined" }, | ||||
|         .{ "message", "msg2" }, | ||||
|         .{ "target == mc1.port2", "true" }, | ||||
|         .{ "currentTarget == mc1.port2", "true" }, | ||||
|  | ||||
|         .{ "message = null", null }, | ||||
|         .{ "mc1.port1.close();", null }, | ||||
|         .{ "mc1.port1.postMessage('msg3');", "undefined" }, | ||||
|         .{ "message", "null" }, | ||||
|     }, .{}); | ||||
|  | ||||
|     try runner.testCases(&.{ | ||||
|         .{ "const mc2 = new MessageChannel()", null }, | ||||
|         .{ "mc2.port2.postMessage('msg1');", "undefined" }, | ||||
|         .{ "mc2.port1.postMessage('msg2');", "undefined" }, | ||||
|         .{ | ||||
|             \\ let message1 = null; | ||||
|             \\ mc2.port1.addEventListener('message', (e) => { | ||||
|             \\   message1 = e.data; | ||||
|             \\ }); | ||||
|             , | ||||
|             null, | ||||
|         }, | ||||
|         .{ | ||||
|             \\ let message2 = null; | ||||
|             \\ mc2.port2.addEventListener('message', (e) => { | ||||
|             \\   message2 = e.data; | ||||
|             \\ }); | ||||
|             , | ||||
|             null, | ||||
|         }, | ||||
|         .{ "message1", "null" }, | ||||
|         .{ "message2", "null" }, | ||||
|         .{ "mc2.port2.start()", null }, | ||||
|  | ||||
|         .{ "message1", "null" }, | ||||
|         .{ "message2", "msg2" }, | ||||
|         .{ "message2 = null", null }, | ||||
|  | ||||
|         .{ "mc2.port1.start()", null }, | ||||
|         .{ "message1", "msg1" }, | ||||
|         .{ "message2", "null" }, | ||||
|     }, .{}); | ||||
| } | ||||
| @@ -53,4 +53,5 @@ pub const Interfaces = .{ | ||||
|     PerformanceObserver, | ||||
|     @import("range.zig").Interfaces, | ||||
|     @import("Animation.zig"), | ||||
|     @import("MessageChannel.zig").Interfaces, | ||||
| }; | ||||
|   | ||||
| @@ -30,6 +30,7 @@ pub const Union = union(enum) { | ||||
|     node: nod.Union, | ||||
|     xhr: *@import("../xhr/xhr.zig").XMLHttpRequest, | ||||
|     plain: *parser.EventTarget, | ||||
|     message_port: *@import("MessageChannel.zig").MessagePort, | ||||
| }; | ||||
|  | ||||
| // EventTarget implementation | ||||
| @@ -63,6 +64,9 @@ pub const EventTarget = struct { | ||||
|                 const base: *XMLHttpRequestEventTarget = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et))); | ||||
|                 return .{ .xhr = @fieldParentPtr("proto", base) }; | ||||
|             }, | ||||
|             .message_port => { | ||||
|                 return .{ .message_port = @fieldParentPtr("proto", @as(*parser.EventTargetTBase, @ptrCast(et))) }; | ||||
|             }, | ||||
|             else => return error.MissingEventTargetType, | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -33,9 +33,10 @@ const CustomEvent = @import("custom_event.zig").CustomEvent; | ||||
| const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent; | ||||
| const MouseEvent = @import("mouse_event.zig").MouseEvent; | ||||
| const ErrorEvent = @import("../html/error_event.zig").ErrorEvent; | ||||
| const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent; | ||||
|  | ||||
| // Event interfaces | ||||
| pub const Interfaces = .{ Event, CustomEvent, ProgressEvent, MouseEvent, ErrorEvent }; | ||||
| pub const Interfaces = .{ Event, CustomEvent, ProgressEvent, MouseEvent, ErrorEvent, MessageEvent }; | ||||
|  | ||||
| pub const Union = generate.Union(Interfaces); | ||||
|  | ||||
| @@ -60,6 +61,7 @@ pub const Event = struct { | ||||
|             .progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* }, | ||||
|             .mouse_event => .{ .MouseEvent = @as(*parser.MouseEvent, @ptrCast(evt)) }, | ||||
|             .error_event => .{ .ErrorEvent = @as(*ErrorEvent, @ptrCast(evt)).* }, | ||||
|             .message_event => .{ .MessageEvent = @as(*MessageEvent, @ptrCast(evt)).* }, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -528,6 +528,7 @@ pub const EventType = enum(u8) { | ||||
|     error_event = 4, | ||||
|     abort_signal = 5, | ||||
|     xhr_event = 6, | ||||
|     message_event = 7, | ||||
| }; | ||||
|  | ||||
| pub const MutationEvent = c.dom_mutation_event; | ||||
| @@ -786,6 +787,7 @@ pub const EventTargetTBase = extern struct { | ||||
|         window = 4, | ||||
|         performance = 5, | ||||
|         media_query_list = 6, | ||||
|         message_port = 7, | ||||
|     }; | ||||
|  | ||||
|     vtable: ?*const c.struct_dom_event_target_vtable = &c.struct_dom_event_target_vtable{ | ||||
|   | ||||
| @@ -634,13 +634,15 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { | ||||
|             // The key is the @intFromPtr of the Zig value | ||||
|             identity_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .empty, | ||||
|  | ||||
|             // Similar to the identity map, but used much less frequently. Some | ||||
|             // web APIs have to manage opaque values. Ideally, they use an | ||||
|             // Some web APIs have to manage opaque values. Ideally, they use an | ||||
|             // JsObject, but the JsObject has no lifetime guarantee beyond the | ||||
|             // current call. They can call .persist() on their JsObject to get | ||||
|             // a `*PersistentObject()`. We need to track these to free them. | ||||
|             // The key is the @intFromPtr of the v8.Object.handle. | ||||
|             js_object_map: std.AutoHashMapUnmanaged(usize, PersistentObject) = .empty, | ||||
|             // This used to be a map and acted like identity_map; the key was | ||||
|             // the @intFromPtr(js_obj.handle). But v8 can re-use address. Without | ||||
|             // a reliable way to know if an object has already been persisted, | ||||
|             // we now simply persist every time persist() is called. | ||||
|             js_object_list: std.ArrayListUnmanaged(PersistentObject) = .empty, | ||||
|  | ||||
|             // When we need to load a resource (i.e. an external script), we call | ||||
|             // this function to get the source. This is always a reference to the | ||||
| @@ -690,11 +692,8 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 { | ||||
|                     var it = self.js_object_map.valueIterator(); | ||||
|                     while (it.next()) |p| { | ||||
|                         p.deinit(); | ||||
|                     } | ||||
|                 for (self.js_object_list.items) |*p| { | ||||
|                     p.deinit(); | ||||
|                 } | ||||
|  | ||||
|                 { | ||||
| @@ -1883,16 +1882,13 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { | ||||
|             pub fn persist(self: JsObject) !JsObject { | ||||
|                 var js_context = self.js_context; | ||||
|                 const js_obj = self.js_obj; | ||||
|                 const handle = js_obj.handle; | ||||
|  | ||||
|                 const gop = try js_context.js_object_map.getOrPut(js_context.context_arena, @intFromPtr(handle)); | ||||
|                 if (gop.found_existing == false) { | ||||
|                     gop.value_ptr.* = PersistentObject.init(js_context.isolate, js_obj); | ||||
|                 } | ||||
|                 const persisted = PersistentObject.init(js_context.isolate, js_obj); | ||||
|                 try js_context.js_object_list.append(js_context.context_arena, persisted); | ||||
|  | ||||
|                 return .{ | ||||
|                     .js_context = js_context, | ||||
|                     .js_obj = gop.value_ptr.castToObject(), | ||||
|                     .js_obj = persisted.castToObject(), | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Karl Seguin
					Karl Seguin