@antha/multiplayer-core 0.0.1

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.
@@ -0,0 +1,331 @@
1
+ import { waitUntil } from '@augment-vir/assert';
2
+ import { ensureError, makeWritable, } from '@augment-vir/common';
3
+ import { convertDuration } from 'date-vir';
4
+ import { defineTypedCustomEvent, ListenTarget } from 'typed-event-target';
5
+ import { WebrtcMultiplayerConnectionUpdateEvent, WebrtcMultiplayerController, WebrtcMultiplayerMessageEvent, } from '../webrtc/webrtc-multiplayer-controller.js';
6
+ import { RoomRejectionError } from './errors.js';
7
+ import { multiplayerHealthEndpoint, multiplayerRoomsEndpoint, } from './multiplayer-api.js';
8
+ import { createMultiplayerApiClient } from './multiplayer-client.js';
9
+ /**
10
+ * Connection state for {@link MultiplayerRoomController}.
11
+ *
12
+ * @category Internal
13
+ */
14
+ export var MultiplayerConnectionState;
15
+ (function (MultiplayerConnectionState) {
16
+ MultiplayerConnectionState["Connecting"] = "connecting";
17
+ MultiplayerConnectionState["Connected"] = "connected";
18
+ /** The connection has not been started or has been gracefully terminated. */
19
+ MultiplayerConnectionState["Disconnected"] = "disconnected";
20
+ })(MultiplayerConnectionState || (MultiplayerConnectionState = {}));
21
+ /**
22
+ * Empty or totally disconnected state for {@link ApiAndRoomConnectionState}.
23
+ *
24
+ * @category Internal
25
+ */
26
+ export const emptyApiAndRoomConnectionState = {
27
+ room: MultiplayerConnectionState.Disconnected,
28
+ api: MultiplayerConnectionState.Disconnected,
29
+ };
30
+ /**
31
+ * This is fired when a room message is received.
32
+ *
33
+ * @category Events
34
+ */
35
+ export class ControllerMessageEvent extends defineTypedCustomEvent()('controller-message') {
36
+ sourceClientId;
37
+ constructor(sourceClientId, detail) {
38
+ super({
39
+ detail,
40
+ });
41
+ this.sourceClientId = sourceClientId;
42
+ }
43
+ }
44
+ /**
45
+ * This is called whenever the room list updates, even if there were no changes to the room list.
46
+ * Note that room list updates are paused while the controller is connected to an actual room.
47
+ *
48
+ * @category Events
49
+ */
50
+ export class ControllerRoomListEvent extends defineTypedCustomEvent()('controller-room-list') {
51
+ }
52
+ /**
53
+ * This is fired in the following situations:
54
+ *
55
+ * - A new host for the room was selected
56
+ * - The room host was lost
57
+ * - A new room client was added (only fired on the host client)
58
+ * - A room client was lost (only fired on the host client)
59
+ *
60
+ * @category Events
61
+ */
62
+ export class ControllerClientEvent extends defineTypedCustomEvent()('controller-client') {
63
+ }
64
+ /**
65
+ * Fires when the controller's connection state is updated.
66
+ *
67
+ * @category Events
68
+ */
69
+ export class ControllerConnectionEvent extends defineTypedCustomEvent()('controller-connection') {
70
+ }
71
+ /**
72
+ * A generic multiplayer room controller. It manages API connectivity, room discovery, WebRTC
73
+ * signaling, and generic room messages. It does not impose a game-state synchronization strategy.
74
+ *
75
+ * @category Main
76
+ */
77
+ export class MultiplayerRoomController extends ListenTarget {
78
+ params;
79
+ /** All events emitted by this controller. */
80
+ static events = {
81
+ ControllerMessageEvent,
82
+ ControllerRoomListEvent,
83
+ ControllerClientEvent,
84
+ ControllerConnectionEvent,
85
+ };
86
+ /** All events emitted by this controller. */
87
+ events = MultiplayerRoomController.events;
88
+ static knownErrors = {
89
+ RoomRejectionError,
90
+ };
91
+ knownErrors = MultiplayerRoomController.knownErrors;
92
+ /**
93
+ * Set to `false` to disable room updates, even when still not connected to a room in
94
+ * multiplayer mode.
95
+ */
96
+ enableRoomUpdates = true;
97
+ /** Currently joined room id. If a room has not been joined yet, this will be empty. */
98
+ roomId;
99
+ /** The current connection state of the controller's connection to a backend API. */
100
+ apiConnectionState = MultiplayerConnectionState.Disconnected;
101
+ /** The current connection state of the controller's connection to a multiplayer room. */
102
+ roomConnectionState = MultiplayerConnectionState.Disconnected;
103
+ /** Current room connection. */
104
+ currentConnection;
105
+ /**
106
+ * Rooms that have rejected the current player, so the player doesn't keep trying to connect to
107
+ * them.
108
+ */
109
+ rejectedRoomIds = new Set();
110
+ /**
111
+ * The current multiplayer API client. This will be `undefined` before multiplayer starts or if
112
+ * playing in single player.
113
+ */
114
+ multiplayerApiClient;
115
+ /**
116
+ * Used to keep track of the room update interval. This will be set when the controller is
117
+ * constructed in multiplayer mode or when a room is left. This will be cleared when a room is
118
+ * joined or if the controller is destroyed.
119
+ */
120
+ roomUpdateIntervalId;
121
+ /** This is populated when `.initMultiplayer` is called. */
122
+ multiplayerParams;
123
+ /**
124
+ * Get the current client's WebRTC client id. This will return `undefined` if there is no
125
+ * current connection.
126
+ */
127
+ getClientId() {
128
+ return this.currentConnection?.clientId;
129
+ }
130
+ /**
131
+ * Get all connected client ids.
132
+ *
133
+ * - For host clients, this indicates how many member clients are connected to the host client,
134
+ * _not_ including the host itself.
135
+ * - For non-host clients, this only lists the local connection used to reach the host.
136
+ */
137
+ getConnectedClientIds() {
138
+ return this.currentConnection?.getConnectedClientIds() || [];
139
+ }
140
+ /**
141
+ * Get all room client ids.
142
+ *
143
+ * - For host clients, this indicates how many clients are connected to the room, including the
144
+ * host client itself.
145
+ * - For non-host clients, this includes the member client and the host client once connected.
146
+ */
147
+ getAllClientIds() {
148
+ return this.currentConnection?.getAllClientIds() || [];
149
+ }
150
+ constructor(params) {
151
+ super();
152
+ this.params = params;
153
+ }
154
+ /**
155
+ * Start multiplayer mode. This initializes
156
+ * {@link MultiplayerRoomController.multiplayerApiClient} and
157
+ * {@link MultiplayerRoomController.roomUpdateIntervalId}.
158
+ */
159
+ async initMultiplayer(params) {
160
+ if (this.currentConnection) {
161
+ throw new Error('Cannot start multiplayer mode again when a multiplayer connection already present.');
162
+ }
163
+ this.multiplayerParams = params;
164
+ this.updateConnectionState({
165
+ api: MultiplayerConnectionState.Connecting,
166
+ });
167
+ try {
168
+ const api = params.multiplayerApiClient ||
169
+ (await createMultiplayerApiClient({
170
+ portScanOptions: params.portScanOptions,
171
+ backendOrigin: params.backendOrigin,
172
+ }));
173
+ const output = await api.fetch(multiplayerHealthEndpoint).GET();
174
+ if (!output.Ok) {
175
+ throw new Error(`Failed to find multiplayer API at ${api.baseUrl}.`);
176
+ }
177
+ this.multiplayerApiClient = api;
178
+ this.updateConnectionState({
179
+ api: MultiplayerConnectionState.Connected,
180
+ });
181
+ }
182
+ catch (error) {
183
+ this.updateConnectionState({
184
+ api: ensureError(error),
185
+ });
186
+ throw error;
187
+ }
188
+ this.startRoomInterval();
189
+ }
190
+ /** Send a generic message to the current room. */
191
+ sendMessage(message) {
192
+ if (!this.currentConnection || !this.currentConnection.isConnected()) {
193
+ throw new Error('Cannot send message: not connected to a room.');
194
+ }
195
+ this.currentConnection.sendMessage(message);
196
+ }
197
+ /** Detects if this controller is the room host or not. */
198
+ isHost() {
199
+ return this.currentConnection?.isHost() || false;
200
+ }
201
+ /** Detects if this controller is connected to a room or not. */
202
+ isConnected() {
203
+ return this.currentConnection?.isConnected() || false;
204
+ }
205
+ /** Cleanup everything. */
206
+ destroy() {
207
+ super.destroy();
208
+ this.updateConnectionState({
209
+ room: MultiplayerConnectionState.Disconnected,
210
+ api: MultiplayerConnectionState.Disconnected,
211
+ });
212
+ this.currentConnection?.destroy();
213
+ globalThis.clearInterval(this.roomUpdateIntervalId);
214
+ }
215
+ /**
216
+ * Join or create a room.
217
+ *
218
+ * @throws `Error` if this controller is already connected to a room.
219
+ */
220
+ async joinOrCreateRoom(room) {
221
+ if (this.currentConnection) {
222
+ throw new Error('Cannot join room: connection already established.');
223
+ }
224
+ else if (!this.multiplayerApiClient || !this.multiplayerParams) {
225
+ throw new Error('Cannot join room. Please start this controller in multiplayer mode to join rooms.');
226
+ }
227
+ else if (this.rejectedRoomIds.has(room.roomId)) {
228
+ throw new RoomRejectionError(room);
229
+ }
230
+ this.updateConnectionState({
231
+ room: MultiplayerConnectionState.Connecting,
232
+ });
233
+ const currentConnection = new WebrtcMultiplayerController(this.params.gameId, this.multiplayerApiClient, this.multiplayerParams.stunServerUrls || [], room, undefined, this.params.acceptConnection
234
+ ? (data) => {
235
+ return this.params.acceptConnection?.(data.connectingClientId, this) ?? true;
236
+ }
237
+ : undefined);
238
+ this.currentConnection = currentConnection;
239
+ currentConnection.listen((WebrtcMultiplayerMessageEvent), (event) => {
240
+ this.dispatch(new ControllerMessageEvent(event.sourceClientId, event.detail));
241
+ });
242
+ currentConnection.listen(WebrtcMultiplayerConnectionUpdateEvent, (event) => {
243
+ this.dispatch(new ControllerClientEvent({
244
+ detail: event.detail,
245
+ }));
246
+ });
247
+ await currentConnection.initConnection();
248
+ const connectionResult = await waitUntil.isDefined(() => {
249
+ const connected = currentConnection.isConnected();
250
+ const destroyed = currentConnection.isDestroyed;
251
+ return !connected && !destroyed
252
+ ? undefined
253
+ : {
254
+ connected,
255
+ destroyed,
256
+ };
257
+ });
258
+ if (connectionResult.connected) {
259
+ makeWritable(this).roomId = room.roomId;
260
+ globalThis.clearInterval(this.roomUpdateIntervalId);
261
+ this.updateConnectionState({
262
+ room: MultiplayerConnectionState.Connected,
263
+ });
264
+ }
265
+ else {
266
+ currentConnection.destroy();
267
+ this.rejectedRoomIds.add(room.roomId);
268
+ this.currentConnection = undefined;
269
+ const error = new RoomRejectionError(room);
270
+ this.updateConnectionState({
271
+ room: error,
272
+ });
273
+ throw error;
274
+ }
275
+ }
276
+ /** Leave the current room or single player connection. */
277
+ leaveRoom() {
278
+ if (!this.currentConnection) {
279
+ return;
280
+ }
281
+ makeWritable(this).roomId = undefined;
282
+ this.currentConnection.destroy();
283
+ this.currentConnection = undefined;
284
+ this.startRoomInterval();
285
+ this.updateConnectionState({
286
+ room: MultiplayerConnectionState.Disconnected,
287
+ });
288
+ }
289
+ /** Set the current connection state and fire listeners. */
290
+ updateConnectionState(state) {
291
+ if (state.api) {
292
+ makeWritable(this).apiConnectionState = state.api;
293
+ }
294
+ if (state.room) {
295
+ makeWritable(this).roomConnectionState = state.room;
296
+ }
297
+ this.dispatch(new ControllerConnectionEvent({
298
+ detail: {
299
+ room: this.roomConnectionState,
300
+ api: this.apiConnectionState,
301
+ },
302
+ }));
303
+ }
304
+ /** Starts polling the multiplayer server for room updates and fires listeners. */
305
+ startRoomInterval() {
306
+ if (this.multiplayerApiClient) {
307
+ const roomUpdateMs = this.multiplayerParams?.roomUpdateInterval
308
+ ? convertDuration(this.multiplayerParams.roomUpdateInterval, {
309
+ milliseconds: true,
310
+ }).milliseconds
311
+ : 10_000;
312
+ this.roomUpdateIntervalId = globalThis.setInterval(async () => {
313
+ if (this.currentConnection ||
314
+ !this.multiplayerApiClient ||
315
+ !this.enableRoomUpdates) {
316
+ return;
317
+ }
318
+ const output = await this.multiplayerApiClient.fetch(multiplayerRoomsEndpoint).GET({
319
+ searchParams: {
320
+ gameId: [this.params.gameId],
321
+ },
322
+ });
323
+ if (output.Ok) {
324
+ this.dispatch(new ControllerRoomListEvent({
325
+ detail: output.Ok.responseData,
326
+ }));
327
+ }
328
+ }, roomUpdateMs);
329
+ }
330
+ }
331
+ }
@@ -0,0 +1,141 @@
1
+ import { MultiplayerWebSocketMessageType } from '../webrtc/web-rtc-communication.js';
2
+ /**
3
+ * A shape definition for {@link ClientIdentification} when joining or creating a room.
4
+ *
5
+ * @category Internal
6
+ */
7
+ export declare const clientIdShape: import("object-shape-tester").Shape<{
8
+ /** This id is used to keep track of each client on the multiplayer server. */
9
+ clientId: import("object-shape-tester").Shape<import("@sinclair/typebox").TUnsafe<import("../multiplayer-id.js").ClientId>>;
10
+ /**
11
+ * The id of the room that the user is communicating with. Set this either to to an existing
12
+ * room to join that room, or a new id to create a new room.
13
+ */
14
+ roomId: import("object-shape-tester").Shape<import("@sinclair/typebox").TUnsafe<import("../multiplayer-id.js").RoomId>>;
15
+ /** The name of the room to create or join. */
16
+ roomName: string;
17
+ }>;
18
+ /**
19
+ * Data included in each multiplayer server message that is used to identify the message client and
20
+ * the room they wish to join or host.
21
+ *
22
+ * @category Internal
23
+ */
24
+ export type ClientIdentification = typeof clientIdShape.runtimeType;
25
+ /**
26
+ * Base shape definition for every message type.
27
+ *
28
+ * @category Internal
29
+ */
30
+ export declare const baseMessageShape: import("object-shape-tester").Shape<{
31
+ /**
32
+ * The id of the original message. If a message is ever a response to another message, the
33
+ * message id will remain the same throughout the chain.
34
+ */
35
+ messageId: import("object-shape-tester").Shape<import("@sinclair/typebox").TUnsafe<import("../multiplayer-id.js").SocketMessageId>>;
36
+ }>;
37
+ /**
38
+ * ==========================================================
39
+ *
40
+ * # Message Shape Definitions
41
+ *
42
+ * ==========================================================
43
+ */
44
+ /**
45
+ * Shape definition for "answer" messages.
46
+ *
47
+ * @category Internal
48
+ */
49
+ export declare const answerMessageShape: import("object-shape-tester").Shape<import("object-shape-tester").Shape<import("@sinclair/typebox").TUnsafe<{
50
+ clientId: import("../multiplayer-id.js").ClientId;
51
+ roomId: import("../multiplayer-id.js").RoomId;
52
+ roomName: string;
53
+ } & {
54
+ messageId: import("../multiplayer-id.js").SocketMessageId;
55
+ } & {
56
+ type: MultiplayerWebSocketMessageType.Answer;
57
+ data: {
58
+ type: MultiplayerWebSocketMessageType.Answer;
59
+ sdp: string;
60
+ } | {
61
+ rejected: true;
62
+ };
63
+ }>>>;
64
+ /**
65
+ * Shape definition for "host ping" messages.
66
+ *
67
+ * @category Internal
68
+ */
69
+ export declare const hostPingMessageShape: import("object-shape-tester").Shape<import("object-shape-tester").Shape<import("@sinclair/typebox").TUnsafe<{
70
+ clientId: import("../multiplayer-id.js").ClientId;
71
+ roomId: import("../multiplayer-id.js").RoomId;
72
+ roomName: string;
73
+ } & {
74
+ messageId: import("../multiplayer-id.js").SocketMessageId;
75
+ } & {
76
+ type: MultiplayerWebSocketMessageType.HostPing;
77
+ clientSecret: string;
78
+ clientCount: number;
79
+ roomPassword: string;
80
+ }>>>;
81
+ /**
82
+ * Shape definition for "offer" messages forwarded from the multiplayer server to the host.
83
+ *
84
+ * @category Internal
85
+ */
86
+ export declare const forwardedOfferMessageShape: import("object-shape-tester").Shape<import("object-shape-tester").Shape<import("@sinclair/typebox").TUnsafe<{
87
+ clientId: import("../multiplayer-id.js").ClientId;
88
+ roomId: import("../multiplayer-id.js").RoomId;
89
+ roomName: string;
90
+ } & {
91
+ messageId: import("../multiplayer-id.js").SocketMessageId;
92
+ } & {
93
+ type: MultiplayerWebSocketMessageType.Offer;
94
+ data: {
95
+ type: MultiplayerWebSocketMessageType.Offer;
96
+ sdp: string;
97
+ };
98
+ }>>>;
99
+ /**
100
+ * Shape definition for "offer" messages.
101
+ *
102
+ * @category Internal
103
+ */
104
+ export declare const offerMessageShape: import("object-shape-tester").Shape<import("object-shape-tester").Shape<import("@sinclair/typebox").TUnsafe<{
105
+ messageId: import("../multiplayer-id.js").SocketMessageId;
106
+ } & {
107
+ clientId: import("../multiplayer-id.js").ClientId;
108
+ roomId: import("../multiplayer-id.js").RoomId;
109
+ roomName: string;
110
+ } & {
111
+ type: MultiplayerWebSocketMessageType.Offer;
112
+ data: {
113
+ type: MultiplayerWebSocketMessageType.Offer;
114
+ sdp: string;
115
+ };
116
+ } & {
117
+ clientSecret: string;
118
+ roomPassword: string;
119
+ }>>>;
120
+ /**
121
+ * Shape definition for "offer result" messages.
122
+ *
123
+ * @category Internal
124
+ */
125
+ export declare const offerResultShape: import("object-shape-tester").Shape<import("object-shape-tester").Shape<import("@sinclair/typebox").TUnsafe<{
126
+ messageId: import("../multiplayer-id.js").SocketMessageId;
127
+ } & {
128
+ type: MultiplayerWebSocketMessageType.OfferResult;
129
+ hostClientId: import("../multiplayer-id.js").ClientId;
130
+ }>>>;
131
+ /**
132
+ * Shape definition for error messages.
133
+ *
134
+ * @category Internal
135
+ */
136
+ export declare const errorMessageShape: import("object-shape-tester").Shape<import("object-shape-tester").Shape<import("@sinclair/typebox").TUnsafe<{
137
+ messageId: import("../multiplayer-id.js").SocketMessageId;
138
+ } & {
139
+ type: MultiplayerWebSocketMessageType.Error;
140
+ errorMessage: string;
141
+ }>>>;
@@ -0,0 +1,114 @@
1
+ import { defineShape, exactShape, intersectShape, unionShape } from 'object-shape-tester';
2
+ import { multiplayerIdShapes } from '../multiplayer-id.js';
3
+ import { MultiplayerWebSocketMessageType, webrtcAnswerShape, webrtcOfferShape, } from '../webrtc/web-rtc-communication.js';
4
+ /**
5
+ * A shape definition for {@link ClientIdentification} when joining or creating a room.
6
+ *
7
+ * @category Internal
8
+ */
9
+ export const clientIdShape = defineShape({
10
+ /** This id is used to keep track of each client on the multiplayer server. */
11
+ clientId: multiplayerIdShapes.client(),
12
+ /**
13
+ * The id of the room that the user is communicating with. Set this either to to an existing
14
+ * room to join that room, or a new id to create a new room.
15
+ */
16
+ roomId: multiplayerIdShapes.room(),
17
+ /** The name of the room to create or join. */
18
+ roomName: '',
19
+ });
20
+ /**
21
+ * Base shape definition for every message type.
22
+ *
23
+ * @category Internal
24
+ */
25
+ export const baseMessageShape = defineShape({
26
+ /**
27
+ * The id of the original message. If a message is ever a response to another message, the
28
+ * message id will remain the same throughout the chain.
29
+ */
30
+ messageId: multiplayerIdShapes.socketMessage(),
31
+ });
32
+ /**
33
+ * ==========================================================
34
+ *
35
+ * # Message Shape Definitions
36
+ *
37
+ * ==========================================================
38
+ */
39
+ /**
40
+ * Shape definition for "answer" messages.
41
+ *
42
+ * @category Internal
43
+ */
44
+ export const answerMessageShape = defineShape(intersectShape(clientIdShape, baseMessageShape, {
45
+ type: exactShape(MultiplayerWebSocketMessageType.Answer),
46
+ /**
47
+ * This data object matches the `RTCSessionDescriptionInit` type from the TS lib. This data
48
+ * should be passed into `RTCPeerConnection.setRemoteDescription` to accept a WebRTC
49
+ * answer.
50
+ */
51
+ data: unionShape({
52
+ rejected: exactShape(true),
53
+ }, webrtcAnswerShape),
54
+ }));
55
+ /**
56
+ * Shape definition for "host ping" messages.
57
+ *
58
+ * @category Internal
59
+ */
60
+ export const hostPingMessageShape = defineShape(intersectShape(clientIdShape, baseMessageShape, {
61
+ type: exactShape(MultiplayerWebSocketMessageType.HostPing),
62
+ /** This secret is used to verify that the sender is indeed the host of the current room. */
63
+ clientSecret: '',
64
+ clientCount: -1,
65
+ roomPassword: '',
66
+ }));
67
+ /**
68
+ * Shape definition for "offer" messages forwarded from the multiplayer server to the host.
69
+ *
70
+ * @category Internal
71
+ */
72
+ export const forwardedOfferMessageShape = defineShape(intersectShape(clientIdShape, baseMessageShape, {
73
+ type: exactShape(MultiplayerWebSocketMessageType.Offer),
74
+ /**
75
+ * This data object matches the `RTCSessionDescriptionInit` type from the TS lib. This data
76
+ * should be passed into `RTCPeerConnection.setRemoteDescription` when creating an offer.
77
+ */
78
+ data: webrtcOfferShape,
79
+ }));
80
+ /**
81
+ * Shape definition for "offer" messages.
82
+ *
83
+ * @category Internal
84
+ */
85
+ export const offerMessageShape = defineShape(intersectShape(forwardedOfferMessageShape, baseMessageShape, {
86
+ /**
87
+ * This secret is used to verify that a client is a host of a room. Do not share this with
88
+ * other clients.
89
+ */
90
+ clientSecret: '',
91
+ /**
92
+ * Set this when joining a room with a password or when creating a room to set a room
93
+ * password.
94
+ */
95
+ roomPassword: '',
96
+ }));
97
+ /**
98
+ * Shape definition for "offer result" messages.
99
+ *
100
+ * @category Internal
101
+ */
102
+ export const offerResultShape = defineShape(intersectShape(baseMessageShape, {
103
+ type: exactShape(MultiplayerWebSocketMessageType.OfferResult),
104
+ hostClientId: multiplayerIdShapes.client(),
105
+ }));
106
+ /**
107
+ * Shape definition for error messages.
108
+ *
109
+ * @category Internal
110
+ */
111
+ export const errorMessageShape = defineShape(intersectShape(baseMessageShape, {
112
+ type: exactShape(MultiplayerWebSocketMessageType.Error),
113
+ errorMessage: '',
114
+ }));
@@ -0,0 +1,59 @@
1
+ import { type Branded } from '@augment-vir/common';
2
+ import { type TUnsafe } from '@sinclair/typebox';
3
+ import { type Shape } from 'object-shape-tester';
4
+ /**
5
+ * Unique id for a multiplayer room client.
6
+ *
7
+ * @category Internal
8
+ */
9
+ export type ClientId = Branded<`${typeof idPrefixes.client}${string}`, 'multiplayer-client-id'>;
10
+ /**
11
+ * Unique id for a multiplayer room.
12
+ *
13
+ * @category Internal
14
+ */
15
+ export type RoomId = Branded<`${typeof idPrefixes.room}${string}`, 'multiplayer-room-id'>;
16
+ /**
17
+ * Unique id for a multiplayer signaling socket message.
18
+ *
19
+ * @category Internal
20
+ */
21
+ export type SocketMessageId = Branded<`${typeof idPrefixes.socketMessage}${string}`, 'multiplayer-socket-message-id'>;
22
+ /**
23
+ * String prefixes used to distinguish multiplayer id types.
24
+ *
25
+ * @category Internal
26
+ */
27
+ export declare const idPrefixes: {
28
+ /** Prefix for {@link RoomId}. */
29
+ readonly room: "r_";
30
+ /** Prefix for {@link ClientId}. */
31
+ readonly client: "c_";
32
+ /** Prefix for {@link SocketMessageId}. */
33
+ readonly socketMessage: "sm_";
34
+ };
35
+ /**
36
+ * Map of multiplayer id keys to their branded id types.
37
+ *
38
+ * @category Internal
39
+ */
40
+ export type IdMap = {
41
+ /** Branded room id type. */
42
+ room: RoomId;
43
+ /** Branded client id type. */
44
+ client: ClientId;
45
+ /** Branded socket message id type. */
46
+ socketMessage: SocketMessageId;
47
+ };
48
+ /**
49
+ * Create branded multiplayer ids with the correct prefix for each id type.
50
+ *
51
+ * @category Internal
52
+ */
53
+ export declare const createMultiplayerId: { [IdType in keyof typeof idPrefixes]: () => IdMap[IdType]; };
54
+ /**
55
+ * Runtime shapes for validating branded multiplayer ids.
56
+ *
57
+ * @category Internal
58
+ */
59
+ export declare const multiplayerIdShapes: { [IdType in keyof typeof idPrefixes]: () => Shape<TUnsafe<IdMap[IdType]>>; };
@@ -0,0 +1,38 @@
1
+ import { check } from '@augment-vir/assert';
2
+ import { createCuid2, mapObjectValues } from '@augment-vir/common';
3
+ import { createCustomShape } from 'object-shape-tester';
4
+ /**
5
+ * String prefixes used to distinguish multiplayer id types.
6
+ *
7
+ * @category Internal
8
+ */
9
+ export const idPrefixes = {
10
+ /** Prefix for {@link RoomId}. */
11
+ room: 'r_',
12
+ /** Prefix for {@link ClientId}. */
13
+ client: 'c_',
14
+ /** Prefix for {@link SocketMessageId}. */
15
+ socketMessage: 'sm_',
16
+ };
17
+ /**
18
+ * Create branded multiplayer ids with the correct prefix for each id type.
19
+ *
20
+ * @category Internal
21
+ */
22
+ export const createMultiplayerId = mapObjectValues(idPrefixes, (idKey, prefix) => {
23
+ return () => `${prefix}${createCuid2()}`;
24
+ });
25
+ /**
26
+ * Runtime shapes for validating branded multiplayer ids.
27
+ *
28
+ * @category Internal
29
+ */
30
+ export const multiplayerIdShapes = mapObjectValues(idPrefixes, (idKey) => {
31
+ return createCustomShape({
32
+ checkValue(value) {
33
+ return check.isString(value) && value.startsWith(idPrefixes[idKey]);
34
+ },
35
+ default: '',
36
+ name: `${idKey} multiplayer id shape`,
37
+ });
38
+ });