Skip to content

Commit 16900f8

Browse files
Merge branch 'main' into jkt/p-r-c-tag
2 parents 4a0bfe6 + 2397249 commit 16900f8

File tree

18 files changed

+754
-330
lines changed

18 files changed

+754
-330
lines changed

injected/unit-test/messaging.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,29 @@ describe('Messaging Transports', () => {
4949
}),
5050
);
5151
});
52+
it("calls transport with a NotificationMessage and doesn't throw (but does log)", () => {
53+
const { messaging, transport } = createMessaging();
54+
const notifySpy = spyOn(transport, 'notify').and.throwError('Test error 1');
55+
const errorLoggingSpy = spyOn(console, 'error');
56+
57+
try {
58+
messaging.notify('helloWorld', { foo: 'bar' });
59+
} catch (e) {
60+
fail('Should not throw');
61+
}
62+
63+
expect(notifySpy).toHaveBeenCalledWith(
64+
new NotificationMessage({
65+
context: 'contentScopeScripts',
66+
featureName: 'hello-world',
67+
method: 'helloWorld',
68+
params: { foo: 'bar' },
69+
}),
70+
);
71+
72+
expect(errorLoggingSpy.calls.first().args[0]).toContain('[Messaging] Failed to send notification:');
73+
expect(errorLoggingSpy.calls.first().args[1].message).toEqual('Test error 1');
74+
});
5275
it('calls transport with a Subscription', () => {
5376
const { messaging, transport } = createMessaging();
5477

messaging/docs/examples.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
---
2+
title: Example JSON payloads
3+
---
4+
5+
## Example JSON payloads
6+
7+
## Notifications
8+
9+
**{@link Messaging.NotificationMessage}**
10+
11+
```json
12+
{
13+
"context": "contentScopeScripts",
14+
"featureName": "duckPlayer",
15+
"method": "saveUserValues"
16+
}
17+
```
18+
19+
**{@link Messaging.NotificationMessage} with params**
20+
21+
```json
22+
{
23+
"context": "contentScopeScripts",
24+
"featureName": "duckPlayer",
25+
"method": "saveUserValues",
26+
"params": { "hello": "world" }
27+
}
28+
```
29+
30+
**{@link Messaging.NotificationMessage} with `invalid` params**
31+
32+
```json
33+
{
34+
"context": "contentScopeScripts",
35+
"featureName": "duckPlayer",
36+
"method": "getUserValues",
37+
"params": "oops! <- cannot be a string/number/boolean/null"
38+
}
39+
```
40+
41+
## Requests
42+
43+
**{@link Messaging.RequestMessage}**
44+
45+
```json
46+
{
47+
"context": "contentScopeScripts",
48+
"featureName": "duckPlayer",
49+
"method": "getUserValues",
50+
"id": "abc123"
51+
}
52+
```
53+
54+
55+
**{@link Messaging.RequestMessage} with params**
56+
57+
```json
58+
{
59+
"context": "contentScopeScripts",
60+
"featureName": "duckPlayer",
61+
"method": "getUserValues",
62+
"params": { "hello": "world" },
63+
"id": "abc123"
64+
}
65+
```
66+
67+
68+
**{@link Messaging.RequestMessage} with invalid params**
69+
70+
```json
71+
{
72+
"context": "contentScopeScripts",
73+
"featureName": "duckPlayer",
74+
"method": "getUserValues",
75+
"params": "oops! <- cannot be a string/number/boolean/null",
76+
"id": "abc123"
77+
}
78+
```
79+
80+
## Responses
81+
82+
**{@link Messaging.MessageResponse} with data**
83+
84+
```json
85+
{
86+
"context": "contentScopeScripts",
87+
"featureName": "duckPlayer",
88+
"id": "abc123",
89+
"result": { "hello": "world" }
90+
}
91+
```
92+
93+
## Error Response
94+
95+
**{@link Messaging.MessageResponse} with error**
96+
97+
```json
98+
{
99+
"context": "contentScopeScripts",
100+
"featureName": "duckPlayer",
101+
"id": "abc123",
102+
"error": {
103+
"message": "Method not found"
104+
}
105+
}
106+
```
107+
108+
109+
## Subscriptions
110+
111+
**{@link Messaging.SubscriptionEvent} without data**
112+
113+
```json
114+
{
115+
"context": "contentScopeScripts",
116+
"featureName": "duckPlayer",
117+
"subscriptionName": "onUserValuesUpdated"
118+
}
119+
```
120+
121+
**{@link Messaging.SubscriptionEvent} with data**
122+
123+
```json
124+
{
125+
"context": "contentScopeScripts",
126+
"featureName": "duckPlayer",
127+
"subscriptionName": "onUserValuesUpdated",
128+
"params": { "hello": "world" }
129+
}
130+
```
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
title: Implementation Guide
3+
---
4+
5+
# Messaging Implementation Guide
6+
7+
## Step 1) Receiving a notification or request message:
8+
9+
Each platform will 'receive' messages according to their own best practices, the following spec describes everything **after**
10+
the message has been delivered from the clientside JavaScript (deliberately avoiding the platform specifics of *how* messages arrive)
11+
12+
For example, in Android this would be what happens within a `@Javascript` Interface, but on macOS it would be within
13+
the WebKit messaging protocol, etc.
14+
15+
### Algorithm
16+
17+
1. let `s` be an incoming raw `JSON` payload
18+
2. let `msg` be the result of parsing `s` into key/value pairs
19+
- 2.1 Note: 'parsing' here may not be required if the platform in question receives JSON data directly (ie: JavaScript environments)
20+
3. if parsing was not successful, log an "invalid message" exception and exit.
21+
4. validate that `msg.context` exists and is a `string` value
22+
5. validate that `msg.featureName` exists and is a `string` value
23+
6. validate that `msg.method` exists and is a `string` value
24+
- 6.1 if `context`, `featureName` or `method` are invalid (not a string, or missing), log an "invalid message" Exception and exit.
25+
7. let `params` be a reference to `msg.params` or a new, empty key/value structure
26+
8. if `params` is not a valid key/value structure, log an "invalid params" exception and exit.
27+
9. if the `msg.id` field is absent, then:
28+
- 9.1. mark `msg` as being of type {@link Messaging.NotificationMessage}
29+
10. if the `msg.id` field is present, then:
30+
- 10.1. validate that `msg.id` a string value, log an "invalid id" exception if it isn't, and exit.
31+
- 10.2. mark `msg` as being of type {@link Messaging.RequestMessage}
32+
11. At this point, you should have a structure that represents either a {@link Messaging.NotificationMessage} or
33+
{@link Messaging.RequestMessage}. Then move to Step 2)
34+
35+
36+
## Step 2) Choosing and executing a handler
37+
38+
Once you've completed Step 1), you'll know whether you are dealing with a notification or a request (something you need
39+
to respond to). At this point you don't know which feature will attempt the message, you just know the format was correct.
40+
41+
### Algorithm
42+
43+
1. let `feature` be the result of looking up a feature that matches name `msg.featureName`
44+
2. if `feature` is not found:
45+
- 2.1 if `msg` was marked as type `Request`, return a "feature not found" [Error Response](./examples.md#error-response)
46+
- 2.2 if `msg` was marked as type `Notification`, optionally log a "feature not found" exception and exit
47+
3. let `handler` be the result of calling `feature.handlerFor(msg.method)`
48+
4. if `handler` is not found:
49+
- 4.1 if `msg` was marked as type `Request`, return a "method not found" [Error Response](./examples.md#error-response)
50+
- 4.2 if `msg` was marked as type `Notification`, optionally log a "feature not found" exception and exit
51+
5. execute `handler` with `msg.params`
52+
- 5.1. if `msg` was marked as a {@link Messaging.NotificationMessage} (via step 1), then:
53+
1. do not wait for a response
54+
2. if the platform must respond (to prevent errors), then:
55+
1. respond with an empty key/value JSON structure `{}`
56+
- 5.2. if `msg` was marked as a {@link Messaging.RequestMessage}, then:
57+
1. let `response` be a new instance of {@link Messaging.MessageResponse}
58+
1. assign `msg.context` to `response.context`
59+
2. assign `msg.featureName` to `response.featureName`
60+
3. assign `msg.id` to `response.id`
61+
2. let `result` be the return value of _executing_ `handler(msg.params)`
62+
3. if `result` is empty, assign `result` to an empty key/value structure
63+
4. if an **error** occurred during execution, then:
64+
1. let `error` be a new instance of {@link Messaging.MessageError}
65+
2. assign a descriptive message if possible, to `error.message`
66+
3. assign `error` to `response.error`
67+
5. if an error **did not occur**, assign `result` to `response.result`
68+
6. let `json` be the string result of converting `response` into JSON
69+
7. deliver the JSON response in the platform-specified way
70+
71+
## Step 3) Push-based messaging
72+
73+
1. let `event` be a new instance of {@link Messaging.SubscriptionEvent}
74+
2. assign `event.context` to the target context
75+
3. assign `event.featureName` to the target feature
76+
4. assign `event.subscriptionName` to the target subscriptionName
77+
5. if the message contains data, then
78+
1. let `params` be a key/value structure
79+
1. Note: only key/value structures are permitted.
80+
2. assign `params` to `event.params`
81+
6. let `json` be the string result of converting `event` into JSON
82+
7. deliver the JSON response in the platform-specified way

messaging/docs/messaging.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
---
2+
title: Messaging
3+
children:
4+
- ./implementation-guide.md
5+
- ./examples.md
6+
---
7+
8+
# Messaging
9+
10+
An abstraction for communications between JavaScript and host platforms. Note: We avoid the term 'client' in these docs
11+
since it can be confused with the Browser itself, which is often referred to as the 'client' in other contexts.
12+
13+
The purpose of this library is to enable three idiomatic JavaScript methods for communicating with native platforms:
14+
15+
**tl;dr:**
16+
17+
```javascript
18+
// notification
19+
messaging.notify("helloWorld", { some: "data" })
20+
21+
// requests
22+
const response = await messaging.request("helloWorld", { some: "data" });
23+
24+
// subscriptions
25+
const unsubscribe = messaging.subscribe("helloWorld", (data) => {
26+
console.log(data)
27+
});
28+
```
29+
30+
## Notifications
31+
32+
Notifications do not produce a response, they are fire+forget by nature. A call to `.notify(method, params)` will never
33+
throw an exception, so is safe to call at all times. If you need acknowledgement, or a response, use a Request instead.
34+
35+
```js
36+
// with params
37+
messaging.notify("helloWorld", { some: "data" })
38+
39+
// without params
40+
messaging.notify("helloWorld")
41+
```
42+
43+
## Requests
44+
45+
Request should be used when you require acknowledgement or a response. Calls to `.request(method, params)` return a promise
46+
that can be awaited. Note: calls to `.request()` can throw exceptions - this is deliberate to ensure compatibility with
47+
JavaScript APIs like `Promise.all([...])`.
48+
49+
**A single request->response**
50+
```js
51+
const response = await messaging.request("helloWorld", { some: "data" });
52+
```
53+
54+
**With try/catch**
55+
```js
56+
try {
57+
const response = await messaging.request("helloWorld", { some: "data" });
58+
// use the response
59+
} catch (e) {
60+
// handle the error
61+
}
62+
```
63+
64+
### Ignoring errors (default values)
65+
In cases where you don't need to handle the error, use the await/catch pattern to simulate a default value
66+
67+
```js
68+
const response = await messaging.request("helloWorld", { some: "data" }).catch(() => null);
69+
```
70+
71+
**With Platform APIs, like `Promise.all`**
72+
```js
73+
const request1 = messaging.request("helloWorld", { some: "data" });
74+
const request2 = messaging.request("other");
75+
76+
const [response1, response2] = await Promise.all([request1, request2])
77+
```
78+
79+
## Subscriptions
80+
81+
A subscription is created in JavaScript as a means for the host platform to _push_ values. Note: Subscriptions are created
82+
in JavaScript and DO NOT include acknowledgment from the host platform. If you need that kind of guarantee,
83+
use a request first successful request->response.
84+
85+
```js
86+
const unsubscribe = messaging.subscribe("helloWorld", (data) => {
87+
console.log(data)
88+
});
89+
```

messaging/index.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ export class Messaging {
6969

7070
/**
7171
* Send a 'fire-and-forget' message.
72-
* @throws {MissingHandler}
7372
*
7473
* @example
7574
*
@@ -87,12 +86,21 @@ export class Messaging {
8786
method: name,
8887
params: data,
8988
});
90-
this.transport.notify(message);
89+
try {
90+
this.transport.notify(message);
91+
} catch (e) {
92+
// Silently ignoring any transport errors in production, as per section 4.1 of https://www.jsonrpc.org/specification
93+
// Notifications are fire+forget and should be able to be sent without any knowledge of the receiving ends support
94+
if (this.messagingContext.env === 'development') {
95+
console.error('[Messaging] Failed to send notification:', e);
96+
console.error('[Messaging] Message details:', { name, data });
97+
}
98+
}
9199
}
92100

93101
/**
94-
* Send a request, and wait for a response
95-
* @throws {MissingHandler}
102+
* Send a request and wait for a response
103+
* @throws {Error}
96104
*
97105
* @example
98106
* ```

0 commit comments

Comments
 (0)