@@ -1092,23 +1092,22 @@ pub const Client = struct {
10921092 state : * WindowState ,
10931093 client_id : u64 ,
10941094
1095- fn sendPacket (
1095+ fn retainPeer (self : Client , io : std.Io ) ! Linsang.WebSocketPeer {
1096+ self .state .mutex .lockUncancelable (io );
1097+ defer self .state .mutex .unlock (io );
1098+ const index = self .state .clientIndexById (self .client_id ) orelse
1099+ return error .ConnectionClosed ;
1100+ return self .state .clients .items [index ].peer .clone ();
1101+ }
1102+
1103+ fn sendPacketToPeer (
10961104 self : Client ,
1097- io : std.Io ,
1105+ peer : Linsang.WebSocketPeer ,
10981106 header : protocol.Header ,
10991107 payload : []const u8 ,
11001108 ) ! void {
11011109 if (payload .len > self .state .limits .max_ws_message_size - protocol .header_len )
11021110 return error .MessageTooLarge ;
1103- self .state .mutex .lockUncancelable (io );
1104- const index = self .state .clientIndexById (self .client_id ) orelse {
1105- self .state .mutex .unlock (io );
1106- return error .ConnectionClosed ;
1107- };
1108- var peer = self .state .clients .items [index ].peer .clone ();
1109- self .state .mutex .unlock (io );
1110- defer peer .deinit ();
1111-
11121111 var packet : std .ArrayList (u8 ) = .empty ;
11131112 defer packet .deinit (self .state .gpa );
11141113 try protocol .append (& packet , self .state .gpa , header , payload );
@@ -1118,6 +1117,17 @@ pub const Client = struct {
11181117 };
11191118 }
11201119
1120+ fn sendPacket (
1121+ self : Client ,
1122+ io : std.Io ,
1123+ header : protocol.Header ,
1124+ payload : []const u8 ,
1125+ ) ! void {
1126+ var peer = try self .retainPeer (io );
1127+ defer peer .deinit ();
1128+ try self .sendPacketToPeer (peer , header , payload );
1129+ }
1130+
11211131 fn send (
11221132 self : Client ,
11231133 io : std.Io ,
@@ -1140,6 +1150,31 @@ pub const Client = struct {
11401150 return self .state .clientIndexById (self .client_id ) != null ;
11411151 }
11421152
1153+ /// Replace the window content and navigate only this client to it.
1154+ /// If navigation fails, the replacement remains installed.
1155+ pub fn show (
1156+ self : Client ,
1157+ running : * const Running ,
1158+ content : Content ,
1159+ ) ! void {
1160+ if (running .stopped or ! running .app .started )
1161+ return error .NotRunning ;
1162+ if (! running .app .hasWindow (self .state )) return error .UnknownWindow ;
1163+ var peer = try self .retainPeer (running .inner .io );
1164+ defer peer .deinit ();
1165+
1166+ try self .state .replaceContent (running .inner .io , content );
1167+ const target_url = try (Window { .state = self .state }).url (
1168+ running ,
1169+ self .state .gpa ,
1170+ );
1171+ defer self .state .gpa .free (target_url );
1172+ try self .sendPacketToPeer (peer , .{
1173+ .token = self .state .token ,
1174+ .command = .navigation ,
1175+ }, target_url );
1176+ }
1177+
11431178 pub fn eval (
11441179 self : Client ,
11451180 io : std.Io ,
@@ -3734,6 +3769,59 @@ test "multi-client limits, targeting, and disconnect lifecycle" {
37343769 try std .testing .expectEqual (protocol .Command .js_quick , second_quick .header .command );
37353770 try std .testing .expectEqualStrings (first_quick .payload , second_quick .payload );
37363771
3772+ try std .testing .expectError (
3773+ error .InvalidExternalUrl ,
3774+ first .show (& running , .{ .external_url = "file:///invalid" }),
3775+ );
3776+ try first .run (io , "globalThis.failedShowDidNotNavigate = true" );
3777+ const failed_show_marker = try protocol .decode (try readServerFrame (
3778+ first_stream ,
3779+ io ,
3780+ & first_response ,
3781+ ));
3782+ try std .testing .expectEqual (
3783+ protocol .Command .js_quick ,
3784+ failed_show_marker .header .command ,
3785+ );
3786+
3787+ try first .show (& running , .{ .html = "targeted client page" });
3788+ const targeted_show = try protocol .decode (try readServerFrame (
3789+ first_stream ,
3790+ io ,
3791+ & first_response ,
3792+ ));
3793+ const targeted_url = try window .url (& running , gpa );
3794+ defer gpa .free (targeted_url );
3795+ try std .testing .expectEqual (
3796+ protocol .Command .navigation ,
3797+ targeted_show .header .command ,
3798+ );
3799+ try std .testing .expectEqualStrings (targeted_url , targeted_show .payload );
3800+
3801+ try second .run (io , "globalThis.otherClientWasNotNavigated = true" );
3802+ const second_after_show = try protocol .decode (try readServerFrame (
3803+ second_stream ,
3804+ io ,
3805+ & second_response ,
3806+ ));
3807+ try std .testing .expectEqual (
3808+ protocol .Command .js_quick ,
3809+ second_after_show .header .command ,
3810+ );
3811+ {
3812+ var target : [capability_len + 2 ]u8 = undefined ;
3813+ var response : [512 ]u8 = undefined ;
3814+ _ = try getTestPath (
3815+ running .inner .address ,
3816+ io ,
3817+ try std .fmt .bufPrint (& target , "/{s}/" , .{
3818+ window .state .capability ,
3819+ }),
3820+ "targeted client page" ,
3821+ & response ,
3822+ );
3823+ }
3824+
37373825 try first .navigate (io , "/first" );
37383826 const raw_data = [_ ]u8 { 2 , 3 , 5 };
37393827 try second .sendRaw (io , "receiveRaw" , & raw_data );
@@ -3988,6 +4076,23 @@ test "multi-client limits, targeting, and disconnect lifecycle" {
39884076 try std .testing .expect (second .isConnected (io ));
39894077 try std .testing .expect (! app .closed .load (.acquire ));
39904078 try std .testing .expectError (error .ConnectionClosed , first_eval .await (io ));
4079+ try std .testing .expectError (
4080+ error .ConnectionClosed ,
4081+ first .show (& running , .{ .html = "stale client page" }),
4082+ );
4083+ {
4084+ var target : [capability_len + 2 ]u8 = undefined ;
4085+ var response : [512 ]u8 = undefined ;
4086+ _ = try getTestPath (
4087+ running .inner .address ,
4088+ io ,
4089+ try std .fmt .bufPrint (& target , "/{s}/" , .{
4090+ window .state .capability ,
4091+ }),
4092+ "targeted client page" ,
4093+ & response ,
4094+ );
4095+ }
39914096
39924097 packet .clearRetainingCapacity ();
39934098 try protocol .append (& packet , gpa , .{
0 commit comments