@couch-kit/display 0.2.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -0
- package/dist/index.js +34 -6
- package/lib/relay-display-host.d.ts +41 -3
- package/lib/relay-display-host.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/relay-display-host.ts +90 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,74 @@
|
|
|
1
1
|
# @couch-kit/display
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#164](https://github.com/faluciano/react-native-couch-kit/pull/164) [`a157126`](https://github.com/faluciano/react-native-couch-kit/commit/a157126424e4d73dcc7185118d5be0db6719792e) Thanks [@faluciano](https://github.com/faluciano)! - Send a projected state update as one relay frame instead of one per player
|
|
8
|
+
|
|
9
|
+
A game with a `project` function sends every player their own view, which meant
|
|
10
|
+
one WebSocket frame per player for every state change. Relays bill and
|
|
11
|
+
rate-limit per inbound frame, so a four-player table paid four messages for one
|
|
12
|
+
update and spent four of the display's 30-per-second budget.
|
|
13
|
+
|
|
14
|
+
`GameRuntimeTransport` gains an optional `sendMany(entries)`. When a transport
|
|
15
|
+
implements it, the runtime hands over the whole projected batch at once;
|
|
16
|
+
transports that do not — the LAN WebSocket path — keep receiving one `send` per
|
|
17
|
+
connection and are unaffected.
|
|
18
|
+
|
|
19
|
+
`RelayDisplayHost` implements it with a new `DATA_MULTI` envelope carrying a
|
|
20
|
+
peer-id-to-payload map, which the relay unpacks into ordinary `DATA` frames.
|
|
21
|
+
Phones need no update — nothing on the client side can tell a batched update
|
|
22
|
+
from a unicast one. If the combined frame would exceed the relay's 256KB
|
|
23
|
+
ceiling, the display falls back to individual frames rather than send something
|
|
24
|
+
the relay would drop.
|
|
25
|
+
|
|
26
|
+
Relays must be updated before displays: both bundled implementations
|
|
27
|
+
(`services/relay`, `services/relay-worker`) understand `DATA_MULTI`, and an
|
|
28
|
+
older relay answers it with `MALFORMED`. The type is host-only — a phone sending
|
|
29
|
+
it is rejected, so it cannot be used to reach another phone directly.
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- Updated dependencies [[`a157126`](https://github.com/faluciano/react-native-couch-kit/commit/a157126424e4d73dcc7185118d5be0db6719792e), [`a157126`](https://github.com/faluciano/react-native-couch-kit/commit/a157126424e4d73dcc7185118d5be0db6719792e)]:
|
|
34
|
+
- @couch-kit/runtime@0.3.0
|
|
35
|
+
- @couch-kit/client@0.14.0
|
|
36
|
+
- @couch-kit/core@0.10.0
|
|
37
|
+
|
|
38
|
+
## 0.3.0
|
|
39
|
+
|
|
40
|
+
### Minor Changes
|
|
41
|
+
|
|
42
|
+
- [#158](https://github.com/faluciano/react-native-couch-kit/pull/158) [`509ea7c`](https://github.com/faluciano/react-native-couch-kit/commit/509ea7c02aa6f2e56ebf01e781d6de74f0ded021) Thanks [@faluciano](https://github.com/faluciano)! - Let the relay assign room codes
|
|
43
|
+
|
|
44
|
+
`RelayDisplayHost`'s `roomId` is now optional. Omit it and the relay mints an
|
|
45
|
+
unused six-character code, reported through the new `onRoomCode` callback and
|
|
46
|
+
the `roomCode` getter.
|
|
47
|
+
|
|
48
|
+
A display could never check its own code for collisions — only the relay knows
|
|
49
|
+
which codes are live — so a self-chosen code could land on a game already in
|
|
50
|
+
progress, and did so only after it was on screen. Minted codes are drawn from
|
|
51
|
+
the CSPRNG over a 32-character alphabet without `O`/`0` or `I`/`1`, giving about
|
|
52
|
+
1.07 billion codes.
|
|
53
|
+
|
|
54
|
+
Existing callers that pass `roomId` keep their current behaviour, including
|
|
55
|
+
`ROOM_EXISTS` when the code is taken. New callers should expect the code to
|
|
56
|
+
arrive one round trip after connecting rather than being known up front:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
const [roomCode, setRoomCode] = useState<string | null>(null);
|
|
60
|
+
new RelayDisplayHost({ url, onRoomCode: setRoomCode, reducer, initialState });
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Relays need a matching update to mint: both bundled implementations
|
|
64
|
+
(`services/relay`, `services/relay-worker`) support it. A display that omits
|
|
65
|
+
`roomId` against an older relay gets `MALFORMED`.
|
|
66
|
+
|
|
67
|
+
### Patch Changes
|
|
68
|
+
|
|
69
|
+
- Updated dependencies [[`509ea7c`](https://github.com/faluciano/react-native-couch-kit/commit/509ea7c02aa6f2e56ebf01e781d6de74f0ded021)]:
|
|
70
|
+
- @couch-kit/client@0.13.0
|
|
71
|
+
|
|
3
72
|
## 0.2.3
|
|
4
73
|
|
|
5
74
|
### Patch Changes
|
package/dist/index.js
CHANGED
|
@@ -12,17 +12,19 @@ import {
|
|
|
12
12
|
class RelayDisplayHost {
|
|
13
13
|
runtime;
|
|
14
14
|
ws;
|
|
15
|
-
|
|
15
|
+
assignedRoomId;
|
|
16
|
+
onRoomCode;
|
|
16
17
|
peers = new Set;
|
|
17
18
|
constructor(options) {
|
|
18
|
-
const { url, roomId, ...runtimeConfig } = options;
|
|
19
|
-
this.
|
|
19
|
+
const { url, roomId, onRoomCode, ...runtimeConfig } = options;
|
|
20
|
+
this.assignedRoomId = roomId ?? null;
|
|
21
|
+
this.onRoomCode = onRoomCode;
|
|
20
22
|
this.runtime = new GameHostRuntime(runtimeConfig);
|
|
21
23
|
this.ws = new WebSocket(relayRoomUrl(url, roomId));
|
|
22
24
|
this.ws.onopen = () => {
|
|
23
25
|
this.ws.send(JSON.stringify({
|
|
24
26
|
type: RelayMessageTypes.CREATE_ROOM,
|
|
25
|
-
roomId: this.
|
|
27
|
+
roomId: this.assignedRoomId ?? undefined
|
|
26
28
|
}));
|
|
27
29
|
};
|
|
28
30
|
this.ws.onmessage = (event) => {
|
|
@@ -37,10 +39,14 @@ class RelayDisplayHost {
|
|
|
37
39
|
this.ws.onerror = (event) => this.runtime.handleError(event instanceof Error ? event : new Error("Relay socket error"));
|
|
38
40
|
const transport = {
|
|
39
41
|
send: (connectionId, message) => this.sendEnvelope(message, connectionId),
|
|
40
|
-
broadcast: (message) => this.sendEnvelope(message)
|
|
42
|
+
broadcast: (message) => this.sendEnvelope(message),
|
|
43
|
+
sendMany: (entries) => this.sendMultiEnvelope(entries)
|
|
41
44
|
};
|
|
42
45
|
this.runtime.setTransport(transport);
|
|
43
46
|
}
|
|
47
|
+
get roomCode() {
|
|
48
|
+
return this.assignedRoomId;
|
|
49
|
+
}
|
|
44
50
|
getState = () => this.runtime.getState();
|
|
45
51
|
subscribe = (listener) => this.runtime.subscribe(listener);
|
|
46
52
|
dispatch = (action) => this.runtime.dispatch(action);
|
|
@@ -49,10 +55,28 @@ class RelayDisplayHost {
|
|
|
49
55
|
this.runtime.stop();
|
|
50
56
|
this.ws.close();
|
|
51
57
|
}
|
|
58
|
+
sendMultiEnvelope(entries) {
|
|
59
|
+
const payloads = {};
|
|
60
|
+
for (const { connectionId, message } of entries) {
|
|
61
|
+
payloads[connectionId] = JSON.stringify(message);
|
|
62
|
+
}
|
|
63
|
+
const frame = JSON.stringify({
|
|
64
|
+
type: RelayMessageTypes.DATA_MULTI,
|
|
65
|
+
roomId: this.assignedRoomId ?? undefined,
|
|
66
|
+
payloads
|
|
67
|
+
});
|
|
68
|
+
if (frameByteLength(frame) > DEFAULT_MAX_MESSAGE_BYTES) {
|
|
69
|
+
for (const { connectionId, message } of entries) {
|
|
70
|
+
this.sendEnvelope(message, connectionId);
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
this.ws.send(frame);
|
|
75
|
+
}
|
|
52
76
|
sendEnvelope(message, to) {
|
|
53
77
|
const envelope = {
|
|
54
78
|
type: RelayMessageTypes.DATA,
|
|
55
|
-
roomId: this.
|
|
79
|
+
roomId: this.assignedRoomId ?? undefined,
|
|
56
80
|
data: JSON.stringify(message)
|
|
57
81
|
};
|
|
58
82
|
if (to !== undefined)
|
|
@@ -83,6 +107,10 @@ class RelayDisplayHost {
|
|
|
83
107
|
this.runtime.handleMessage(msg.from, parsed).catch((err) => this.runtime.handleError(err instanceof Error ? err : new Error(String(err))));
|
|
84
108
|
break;
|
|
85
109
|
}
|
|
110
|
+
case RelayMessageTypes.ROOM_CREATED:
|
|
111
|
+
this.assignedRoomId = msg.roomId;
|
|
112
|
+
this.onRoomCode?.(msg.roomId);
|
|
113
|
+
break;
|
|
86
114
|
case RelayMessageTypes.ERROR:
|
|
87
115
|
this.runtime.handleError(new Error(msg.message));
|
|
88
116
|
break;
|
|
@@ -10,8 +10,23 @@ import type { IGameState, IAction } from "@couch-kit/core";
|
|
|
10
10
|
export interface RelayDisplayHostOptions<S extends IGameState, A extends IAction> extends GameHostRuntimeConfig<S, A> {
|
|
11
11
|
/** WebSocket URL of the shared relay server. */
|
|
12
12
|
url: string;
|
|
13
|
-
/**
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Room code phones will use to reach this display.
|
|
15
|
+
*
|
|
16
|
+
* Omit it — the normal case — and the relay allocates one, reporting it via
|
|
17
|
+
* {@link RelayDisplayHostOptions.onRoomCode} and {@link RelayDisplayHost.roomCode}.
|
|
18
|
+
* Only the relay can tell whether a code is already in use, so a code chosen
|
|
19
|
+
* here may be rejected as `ROOM_EXISTS`; supply one only when something
|
|
20
|
+
* outside the relay already fixed it.
|
|
21
|
+
*/
|
|
22
|
+
roomId?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Called once the room exists and its code is known.
|
|
25
|
+
*
|
|
26
|
+
* A minted code is not available synchronously, so a display renders a
|
|
27
|
+
* placeholder until this fires — roughly a round trip to the relay.
|
|
28
|
+
*/
|
|
29
|
+
onRoomCode?: (roomCode: string) => void;
|
|
15
30
|
}
|
|
16
31
|
/**
|
|
17
32
|
* Browser **display host** for the cross-network relay transport.
|
|
@@ -33,10 +48,18 @@ export interface RelayDisplayHostOptions<S extends IGameState, A extends IAction
|
|
|
33
48
|
export declare class RelayDisplayHost<S extends IGameState, A extends IAction> {
|
|
34
49
|
private readonly runtime;
|
|
35
50
|
private readonly ws;
|
|
36
|
-
|
|
51
|
+
/** Null until the relay confirms the room, when the code is relay-assigned. */
|
|
52
|
+
private assignedRoomId;
|
|
53
|
+
private readonly onRoomCode?;
|
|
37
54
|
/** Connected phone connection ids (relay peer ids). */
|
|
38
55
|
private readonly peers;
|
|
39
56
|
constructor(options: RelayDisplayHostOptions<S, A>);
|
|
57
|
+
/**
|
|
58
|
+
* The room code phones join with, or `null` before the relay has assigned
|
|
59
|
+
* one. See {@link RelayDisplayHostOptions.onRoomCode} to be told when it
|
|
60
|
+
* arrives.
|
|
61
|
+
*/
|
|
62
|
+
get roomCode(): string | null;
|
|
40
63
|
/** Current authoritative game state. */
|
|
41
64
|
getState: () => S;
|
|
42
65
|
/** Subscribe to state changes (for `useSyncExternalStore` or manual render). */
|
|
@@ -45,6 +68,21 @@ export declare class RelayDisplayHost<S extends IGameState, A extends IAction> {
|
|
|
45
68
|
dispatch: (action: A) => void;
|
|
46
69
|
/** Tear down the runtime and relay socket. */
|
|
47
70
|
stop(): void;
|
|
71
|
+
/**
|
|
72
|
+
* Sends per-connection messages as one `DATA_MULTI` frame, which the relay
|
|
73
|
+
* unpacks into an ordinary `DATA` frame per phone.
|
|
74
|
+
*
|
|
75
|
+
* A projected game re-sends every player's view on every state change, so on
|
|
76
|
+
* a four-player table this is the difference between one billed relay message
|
|
77
|
+
* and four — and between one and four against the relay's per-connection rate
|
|
78
|
+
* limit, which the display shares across all its fan-out.
|
|
79
|
+
*
|
|
80
|
+
* Falls back to individual sends if the combined frame would exceed the
|
|
81
|
+
* relay's message ceiling: N views in one envelope is N times the bytes, and
|
|
82
|
+
* a frame the relay rejects delivers nothing to anyone. Splitting costs
|
|
83
|
+
* messages; being dropped costs the game.
|
|
84
|
+
*/
|
|
85
|
+
private sendMultiEnvelope;
|
|
48
86
|
private sendEnvelope;
|
|
49
87
|
private handleRelayMessage;
|
|
50
88
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relay-display-host.d.ts","sourceRoot":"","sources":["../src/relay-display-host.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"relay-display-host.d.ts","sourceRoot":"","sources":["../src/relay-display-host.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,qBAAqB,EAE3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,UAAU,EAAE,OAAO,EAAe,MAAM,iBAAiB,CAAC;AAOxE;;;;;;GAMG;AACH,MAAM,WAAW,uBAAuB,CACtC,CAAC,SAAS,UAAU,EACpB,CAAC,SAAS,OAAO,CACjB,SAAQ,qBAAqB,CAAC,CAAC,EAAE,CAAC,CAAC;IACnC,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,gBAAgB,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO;IACnE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAY;IAC/B,+EAA+E;IAC/E,OAAO,CAAC,cAAc,CAAgB;IACtC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAA6B;IACzD,uDAAuD;IACvD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;IAE3C,YAAY,OAAO,EAAE,uBAAuB,CAAC,CAAC,EAAE,CAAC,CAAC,EAwCjD;IAED;;;;OAIG;IACH,IAAI,QAAQ,IAAI,MAAM,GAAG,IAAI,CAE5B;IAED,wCAAwC;IACxC,QAAQ,QAAO,CAAC,CAA4B;IAE5C,gFAAgF;IAChF,SAAS,aAAc,MAAM,IAAI,KAAG,CAAC,MAAM,IAAI,CAAC,CACb;IAEnC,8CAA8C;IAC9C,QAAQ,WAAY,CAAC,KAAG,IAAI,CAAkC;IAE9D,8CAA8C;IAC9C,IAAI,IAAI,IAAI,CAIX;IAED;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,iBAAiB;IAsBzB,OAAO,CAAC,YAAY;IAapB,OAAO,CAAC,kBAAkB;CA2C3B"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@couch-kit/display",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"provenance": true
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
"clean": "rm -rf dist lib"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@couch-kit/client": "0.
|
|
55
|
-
"@couch-kit/core": "0.
|
|
56
|
-
"@couch-kit/runtime": "0.
|
|
54
|
+
"@couch-kit/client": "0.14.0",
|
|
55
|
+
"@couch-kit/core": "0.10.0",
|
|
56
|
+
"@couch-kit/runtime": "0.3.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"typescript": "^7.0.0"
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
GameHostRuntime,
|
|
3
3
|
frameByteLength,
|
|
4
4
|
DEFAULT_MAX_MESSAGE_BYTES,
|
|
5
|
+
type AddressedMessage,
|
|
5
6
|
type GameHostRuntimeConfig,
|
|
6
7
|
type GameRuntimeTransport,
|
|
7
8
|
} from "@couch-kit/runtime";
|
|
@@ -19,12 +20,29 @@ import {
|
|
|
19
20
|
* object used by the RN-TV host) and the shared relay coordinates; the display
|
|
20
21
|
* host owns the authoritative runtime and the relay socket.
|
|
21
22
|
*/
|
|
22
|
-
export interface RelayDisplayHostOptions<
|
|
23
|
-
extends
|
|
23
|
+
export interface RelayDisplayHostOptions<
|
|
24
|
+
S extends IGameState,
|
|
25
|
+
A extends IAction,
|
|
26
|
+
> extends GameHostRuntimeConfig<S, A> {
|
|
24
27
|
/** WebSocket URL of the shared relay server. */
|
|
25
28
|
url: string;
|
|
26
|
-
/**
|
|
27
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Room code phones will use to reach this display.
|
|
31
|
+
*
|
|
32
|
+
* Omit it — the normal case — and the relay allocates one, reporting it via
|
|
33
|
+
* {@link RelayDisplayHostOptions.onRoomCode} and {@link RelayDisplayHost.roomCode}.
|
|
34
|
+
* Only the relay can tell whether a code is already in use, so a code chosen
|
|
35
|
+
* here may be rejected as `ROOM_EXISTS`; supply one only when something
|
|
36
|
+
* outside the relay already fixed it.
|
|
37
|
+
*/
|
|
38
|
+
roomId?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Called once the room exists and its code is known.
|
|
41
|
+
*
|
|
42
|
+
* A minted code is not available synchronously, so a display renders a
|
|
43
|
+
* placeholder until this fires — roughly a round trip to the relay.
|
|
44
|
+
*/
|
|
45
|
+
onRoomCode?: (roomCode: string) => void;
|
|
28
46
|
}
|
|
29
47
|
|
|
30
48
|
/**
|
|
@@ -47,21 +65,27 @@ export interface RelayDisplayHostOptions<S extends IGameState, A extends IAction
|
|
|
47
65
|
export class RelayDisplayHost<S extends IGameState, A extends IAction> {
|
|
48
66
|
private readonly runtime: GameHostRuntime<S, A>;
|
|
49
67
|
private readonly ws: WebSocket;
|
|
50
|
-
|
|
68
|
+
/** Null until the relay confirms the room, when the code is relay-assigned. */
|
|
69
|
+
private assignedRoomId: string | null;
|
|
70
|
+
private readonly onRoomCode?: (roomCode: string) => void;
|
|
51
71
|
/** Connected phone connection ids (relay peer ids). */
|
|
52
72
|
private readonly peers = new Set<string>();
|
|
53
73
|
|
|
54
74
|
constructor(options: RelayDisplayHostOptions<S, A>) {
|
|
55
|
-
const { url, roomId, ...runtimeConfig } = options;
|
|
56
|
-
this.
|
|
75
|
+
const { url, roomId, onRoomCode, ...runtimeConfig } = options;
|
|
76
|
+
this.assignedRoomId = roomId ?? null;
|
|
77
|
+
this.onRoomCode = onRoomCode;
|
|
57
78
|
this.runtime = new GameHostRuntime<S, A>(runtimeConfig);
|
|
58
79
|
this.ws = new WebSocket(relayRoomUrl(url, roomId));
|
|
59
80
|
|
|
60
81
|
this.ws.onopen = () => {
|
|
82
|
+
// No roomId asks the relay to allocate one. Sending the field as
|
|
83
|
+
// undefined omits it from the JSON, which is what the relay reads as
|
|
84
|
+
// "you pick".
|
|
61
85
|
this.ws.send(
|
|
62
86
|
JSON.stringify({
|
|
63
87
|
type: RelayMessageTypes.CREATE_ROOM,
|
|
64
|
-
roomId: this.
|
|
88
|
+
roomId: this.assignedRoomId ?? undefined,
|
|
65
89
|
}),
|
|
66
90
|
);
|
|
67
91
|
};
|
|
@@ -82,13 +106,22 @@ export class RelayDisplayHost<S extends IGameState, A extends IAction> {
|
|
|
82
106
|
);
|
|
83
107
|
|
|
84
108
|
const transport: GameRuntimeTransport = {
|
|
85
|
-
send: (connectionId, message) =>
|
|
86
|
-
this.sendEnvelope(message, connectionId),
|
|
109
|
+
send: (connectionId, message) => this.sendEnvelope(message, connectionId),
|
|
87
110
|
broadcast: (message) => this.sendEnvelope(message),
|
|
111
|
+
sendMany: (entries) => this.sendMultiEnvelope(entries),
|
|
88
112
|
};
|
|
89
113
|
this.runtime.setTransport(transport);
|
|
90
114
|
}
|
|
91
115
|
|
|
116
|
+
/**
|
|
117
|
+
* The room code phones join with, or `null` before the relay has assigned
|
|
118
|
+
* one. See {@link RelayDisplayHostOptions.onRoomCode} to be told when it
|
|
119
|
+
* arrives.
|
|
120
|
+
*/
|
|
121
|
+
get roomCode(): string | null {
|
|
122
|
+
return this.assignedRoomId;
|
|
123
|
+
}
|
|
124
|
+
|
|
92
125
|
/** Current authoritative game state. */
|
|
93
126
|
getState = (): S => this.runtime.getState();
|
|
94
127
|
|
|
@@ -106,10 +139,49 @@ export class RelayDisplayHost<S extends IGameState, A extends IAction> {
|
|
|
106
139
|
this.ws.close();
|
|
107
140
|
}
|
|
108
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Sends per-connection messages as one `DATA_MULTI` frame, which the relay
|
|
144
|
+
* unpacks into an ordinary `DATA` frame per phone.
|
|
145
|
+
*
|
|
146
|
+
* A projected game re-sends every player's view on every state change, so on
|
|
147
|
+
* a four-player table this is the difference between one billed relay message
|
|
148
|
+
* and four — and between one and four against the relay's per-connection rate
|
|
149
|
+
* limit, which the display shares across all its fan-out.
|
|
150
|
+
*
|
|
151
|
+
* Falls back to individual sends if the combined frame would exceed the
|
|
152
|
+
* relay's message ceiling: N views in one envelope is N times the bytes, and
|
|
153
|
+
* a frame the relay rejects delivers nothing to anyone. Splitting costs
|
|
154
|
+
* messages; being dropped costs the game.
|
|
155
|
+
*/
|
|
156
|
+
private sendMultiEnvelope(entries: readonly AddressedMessage[]): void {
|
|
157
|
+
const payloads: Record<string, string> = {};
|
|
158
|
+
for (const { connectionId, message } of entries) {
|
|
159
|
+
payloads[connectionId] = JSON.stringify(message);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const frame = JSON.stringify({
|
|
163
|
+
type: RelayMessageTypes.DATA_MULTI,
|
|
164
|
+
roomId: this.assignedRoomId ?? undefined,
|
|
165
|
+
payloads,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (frameByteLength(frame) > DEFAULT_MAX_MESSAGE_BYTES) {
|
|
169
|
+
for (const { connectionId, message } of entries) {
|
|
170
|
+
this.sendEnvelope(message, connectionId);
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
this.ws.send(frame);
|
|
176
|
+
}
|
|
177
|
+
|
|
109
178
|
private sendEnvelope(message: HostMessage, to?: string): void {
|
|
110
179
|
const envelope: Record<string, unknown> = {
|
|
111
180
|
type: RelayMessageTypes.DATA,
|
|
112
|
-
|
|
181
|
+
// The relay routes by the sender's membership, not this field, so it is
|
|
182
|
+
// only ever informational — and nothing is sent before a peer joins,
|
|
183
|
+
// which cannot happen until the room exists.
|
|
184
|
+
roomId: this.assignedRoomId ?? undefined,
|
|
113
185
|
data: JSON.stringify(message),
|
|
114
186
|
};
|
|
115
187
|
if (to !== undefined) envelope.to = to;
|
|
@@ -147,10 +219,16 @@ export class RelayDisplayHost<S extends IGameState, A extends IAction> {
|
|
|
147
219
|
);
|
|
148
220
|
break;
|
|
149
221
|
}
|
|
222
|
+
case RelayMessageTypes.ROOM_CREATED:
|
|
223
|
+
// Carries the code when the relay chose it, and confirms the code when
|
|
224
|
+
// the caller supplied one.
|
|
225
|
+
this.assignedRoomId = msg.roomId;
|
|
226
|
+
this.onRoomCode?.(msg.roomId);
|
|
227
|
+
break;
|
|
150
228
|
case RelayMessageTypes.ERROR:
|
|
151
229
|
this.runtime.handleError(new Error(msg.message));
|
|
152
230
|
break;
|
|
153
|
-
//
|
|
231
|
+
// ROOM_JOINED is an acknowledgement; no action needed.
|
|
154
232
|
}
|
|
155
233
|
}
|
|
156
234
|
}
|