Skip to content

Commit a5d3c2b

Browse files
committed
Update proposal
* Remove executeScriptNoResult * Rename `start_window()` to `start_window_channel()` and add `window_channel()` without the auto-connect behaviour. * Change the (de)serialization model to automatically create local objects (like structuredClone) rather than requiring a `toLocal()` call. * Updates from PR feedback.
1 parent c5232f1 commit a5d3c2b

File tree

1 file changed

+93
-59
lines changed

1 file changed

+93
-59
lines changed

rfcs/remote_channel.md

+93-59
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,6 @@ browser-specific techniques.
6666
via the main test window, make it hard to build an ergonomic
6767
cross-context messaging API.
6868

69-
### Proposal
70-
7169
All the above examples of prior art have some attractive features, and
7270
it's possible to combine them in a way that should provide an
7371
ergonomic API for web-platform-tests
@@ -84,6 +82,8 @@ ergonomic API for web-platform-tests
8482
messages, and the relatively low level API based on passing strings
8583
around, seem like areas for improvement.
8684

85+
### Proposal
86+
8787
The following subsections will set out a proposal for an API that
8888
combines some of these strengths.
8989

@@ -150,7 +150,7 @@ functions shutdown when the socket is closed.
150150
#### Remote Object Serialization
151151

152152
In order to allow passing complex JavaScript objects between contexts,
153-
a serialization format based on the [WebDriver
153+
a serialization format loosely based on the [WebDriver
154154
BiDi](https://w3c.github.io/webdriver-bidi/#type-common-RemoteValue)
155155
proposal is used. An object is represented using a JSON object as
156156
follows:
@@ -159,16 +159,16 @@ follows:
159159
type: Name of the object type,
160160
value: A representation of the object in a JSON-compatible form
161161
where possible,
162-
objectId: A unique id assigned to the object (not for primitives)
162+
objectId: A unique id assigned to the object in case of circular references.
163163
}
164164
```
165165

166-
So, for example, the array `[1, "foo", {"bar": null}]` is represented as:
166+
So, for example, an array `a` with content `[1, "foo", {"bar": null}, a]` is represented as:
167167

168168
```js
169169
{
170170
"type": "array",
171-
"objectId": <uuid>,
171+
"objectId": 0,
172172
"value": [
173173
{
174174
type: "number",
@@ -178,32 +178,46 @@ So, for example, the array `[1, "foo", {"bar": null}]` is represented as:
178178
"type": "string",
179179
"value": "foo"
180180
},
181+
{
181182
"type": "object",
182-
"objectId": <uuid>,
183183
"value": {
184184
"bar": {
185185
"type": null
186186
}
187187
}
188+
},
189+
{
190+
"type": "array",
191+
"objectId": 0
192+
}
188193
]
189194
}
190195
```
191196

192-
In addition to the types specified in the WebDriver-BiDi
193-
specification, `SendChannel` is given first class support with
194-
`"type": "sendchannel"` and `value` set to the UUID of the
195-
channel. This enables an important pattern: to receive messages from a
196-
remote context, you can send it a `SendChannel` object to use for
197-
responses.
198-
199-
For deserialization, primitive values are converted back to
200-
primitives, but complex values are represented by a `RemoteObject`
201-
type. In cases like arrays where there is a `value` field holding a
202-
container object, `RemoteObject.toLocal()` recursively converts the
203-
content of the container into local objects (so e.g. a `type: array` is
204-
converted into a local `Array` instances, and any contained `type: object`
205-
objects are converted into local `Object` instances, and so on through
206-
the full tree).
197+
This supports the following types:
198+
199+
* All JS primitive types
200+
* Builtin types: `Date`, `Regexp`, `Error`
201+
* Functions
202+
* Collections: `Array`, `Map`, `Set`, `Object`
203+
* `SendChannel`. This enables an important pattern: to receive
204+
messages from a remote context, you can send it a `SendChannel`
205+
object to use for responses.
206+
* `RemoteObject`. This enables sending an arbitrary object as a
207+
reference that can be resolved in the original realm. For example an
208+
`Element` named `elem` can be transferred as a
209+
`RemoteObject(elem)`. If this `RemoteObject` is later transferred
210+
back to the original realm it will be converted back to the original
211+
object.
212+
213+
Deserialization creates values equivalent to the original values in
214+
the current realm e.g. a serialized array is reconstructed as an array
215+
object in the realm where deserialization occurs, and similarly for
216+
each element in the array. `RemoteObject` differs slightly; given a
217+
serialized `RemoteObject` referencing some object `obj`, if `obj`
218+
doesn't exist in the current realm a new `RemoteObject` is created
219+
referencing `obj`. If `obj` does exist in that realm, it is returned
220+
as the result of deserialization.
207221

208222
#### Higher Level API
209223

@@ -221,11 +235,16 @@ send messages to the remote. Alternatively the `RemoteWindow` may be
221235
created first and its `uuid` property used when constructing the URL.
222236

223237
Inside the remote browsing context itself, the test author has to call
224-
`await start_window()` in order to set up a `RecvChannel` with UUID given
225-
by the `uuid` parameter in `location.href`. The returned object offers
226-
an `addMessageHandler(callback)` API to receive messages sent with the
227-
`postMessage` API on `RemoteWindow`, and `async nextMessage()` to wait
228-
for the next message.
238+
`window_channel()` in order to set up a `RecvChannel` with UUID given
239+
by the `uuid` parameter in `location.href`. By default this is not
240+
connected until the `async connect()` method is called. This allows
241+
message handlers to be attached before processing any messages. For
242+
convenience `await start_window_channel()` returns an already
243+
connected `RecvChannel`.
244+
245+
The `RecvChannel` object offers an `addMessageHandler(callback)` API
246+
to receive messages sent with the `postMessage` API on `RemoteWindow`,
247+
and `async nextMessage()` to wait for the next message.
229248

230249
##### Script Execution
231250

@@ -242,20 +261,13 @@ execution results in a `Promise` value, the result of that promise is
242261
awaited. The final return value after the promise is resolved is sent
243262
back and forms the async return value of the `executeScript` call. If
244263
the script throws, the thrown value is provided as the result, and
245-
re-thrown in the originating context. In addition an `exceptionDetails`
246-
field provides the line/column numbers of the original exception,
247-
where available.
248-
249-
In addition there is a `RemoteWindow.executeScriptNoResult(fn,
250-
...args)` method. This works the same way except no channel is passed,
251-
and so no result is returned. This can be useful in case the script
252-
does something like trigger a navigation, so there's no need to
253-
synchronize the navigation starting (which will close the socket) with
254-
writing the response.
264+
re-thrown in the originating context. In addition an
265+
`exceptionDetails` field on the response provides the line/column
266+
numbers of the original exception, where available.
255267

256268
TODO: the naming here isn't great. In particular a `RemoteWindow`
257269
could actually be some other kind of global like a worker, and
258-
`start_window()` is a pretty nondescript method name.
270+
`start_window_channel()` is a pretty nondescript method name.
259271

260272
#### Navigation and bfcache
261273

@@ -374,10 +386,19 @@ class RecvChannel() {
374386
async next(): Promise<Object> {}
375387
}
376388

389+
/**
390+
* Create an unconnected channel defined by a `uuid` in
391+
* `location.href` for listening for RemoteWindow messages.
392+
*/
393+
async window_channel(): RemoteWindowCommandRecvChannel {}
394+
395+
377396
/**
378-
* Start listening for RemoteWindow messages on a channel defined by a `uuid` in `location.href`
397+
* Start listening for RemoteWindow messages on a channel defined by
398+
* a `uuid` in `location.href`
379399
*/
380-
async start_window(): Promise<RemoteWindowCommandRecvChannel> {}
400+
async start_window_channel(): Promise<RemoteWindowCommandRecvChannel> {}
401+
381402

382403
/**
383404
* Handler for RemoteWindow commands
@@ -449,31 +470,32 @@ class RemoteWindow {
449470
* Arguments and return values are serialized as RemoteObjects.
450471
*/
451472
async executeScript(fn: (args: ...any) => any, ..args: any): Promise<any> {}
452-
453-
/**
454-
* Run the function `fn` in the remote context, passing arguments
455-
* `args`, but without returning a result
456-
*
457-
* Arguments are serialized as RemoteObjects.
458-
*/
459-
async executeScriptNoResult(fn: (args: ...any) => any, ..args: any) {}
460473
}
461474

462475
/**
463476
* Representation of a non-primitive type passed through a channel
464477
*/
465478
class RemoteObject {
466479
type: string;
467-
value: any;
468480
objectId: string | undefined;
469481

470482
/**
471-
* Recursively convert the object to a local type (where possible)
472-
* so eg. a remote array is converted into a local array.
473-
*
474-
* Objects without a meaningful local representation are passed back unchanged.
483+
* Create a RemoteObject containing a handle to reference obj
484+
*/
485+
static from(obj): RemoteObject
486+
487+
/**
488+
* Return the local object referenced by the objectId of this RemoteObject,
489+
* or null if there isn't a such an object in this realm.
490+
*/
491+
toLocal(): Object?
492+
493+
/**
494+
* Remove the objectId from the local cache. This means that future
495+
* calls to `toLocal` with the same objectId will always return
496+
* `null`.
475497
*/
476-
toLocal(): any {}
498+
delete()
477499
}
478500

479501
/**
@@ -536,12 +558,23 @@ child.html
536558
<p id="nottest">FAIL</p>
537559
<p id="test">PASS</p>
538560
<script>
539-
start_window();
561+
start_window_channel();
540562
</script>
541563
```
542564

565+
## Implementation Requirements
566+
567+
The implementation only depends on wptserve and normal content js; it
568+
doesn't depend on testdriver or any test-only APIs. Therefore
569+
integration into any deployment not using wptrunner/testdriver should
570+
be straightforward.
571+
543572
## Possible Future Additions
544573

574+
Note: Implementation of any suggestions in this section would happen
575+
in the context of a further RFC. This section is only to sketch some
576+
possibilities for further development of the API.
577+
545578
The primitives here could be integrated more completely with
546579
testharness.js. For example we could use a `RemoteWindow` as a source
547580
of tests in `fetch_tests_from_window`. Alternatively, or in addition
@@ -571,10 +604,11 @@ for websockets. By sticking close to WebDriver BiDi proposed semantics
571604
the transition may even be seamless.
572605

573606
testdriver integration is possible. For example we could add
574-
`RemoteContext.testdriver.click` to execute a click in the remote
575-
context (and similarly for the remainder of the testdriver
576-
API). testdriver in this case would identify the target window by
577-
looking for a window with the appropriate `uuid` parameter in its
607+
`RemoteContext.testdriver.set_permission(params)` to execute a
608+
`set_permission` command in the remote context (and similarly for the
609+
remainder of the testdriver API). This would desugar to
610+
`testdriver.set_permission(params, uuid)` and testdriver would be
611+
update to identify the target window from the `uuid` parameter in its
578612
`location.href`.
579613

580614
## Risks

0 commit comments

Comments
 (0)