@rbxts/tether 1.2.2 → 1.2.4

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/README.md CHANGED
@@ -1,197 +1,197 @@
1
- # Tether
2
- A message-based networking solution for Roblox with automatic binary serialization and type validation.
3
-
4
- > [!CAUTION]
5
- > Depends on `rbxts-transformer-flamework`!
6
-
7
- ## Usage
8
-
9
- ### In `shared/messaging.ts`
10
- ```ts
11
- import type { DataType } from "@rbxts/flamework-binary-serializer";
12
- import { MessageEmitter } from "@rbxts/tether";
13
-
14
- export const messaging = MessageEmitter.create<MessageData>();
15
-
16
- export const enum Message {
17
- Test,
18
- Packed
19
- }
20
-
21
- export interface MessageData {
22
- [Message.Test]: {
23
- readonly foo: string;
24
- readonly n: DataType.u8;
25
- };
26
- [Message.Packed]: DataType.Packed<{
27
- readonly boolean1: boolean;
28
- readonly boolean2: boolean;
29
- readonly boolean3: boolean;
30
- readonly boolean4: boolean;
31
- readonly boolean5: boolean;
32
- readonly boolean6: boolean;
33
- readonly boolean7: boolean;
34
- readonly boolean8: boolean;
35
- }>;
36
- }
37
- ```
38
-
39
- > [!CAUTION]
40
- > Every single message kind must implement an interface for it's data (in the example that would be the object types in `MessageData`). Message serialization (as well as your message itself) will not work if you don't do this.
41
-
42
- ### Server
43
- ```ts
44
- import { Message, messaging } from "shared/messaging";
45
-
46
- messaging.server.on(Message.Test, (player, data) => print(player, "sent data:", data));
47
- ```
48
-
49
- ### Client
50
- ```ts
51
- import { Message, messaging } from "shared/messaging";
52
-
53
- messaging.server.emit(Message.Test, {
54
- foo: "bar",
55
- n: 69
56
- });
57
- ```
58
-
59
- ## Simulated Remote Functions
60
- Tether does not directly use RemoteFunctions since it's based on the MessageEmitter structure. However I have created a small framework to simulate remote functions, as shown below.
61
-
62
- For each function you will need two messages. One to invoke the function, and one to send the return value back (which is done automatically).
63
-
64
- ### In `shared/messaging.ts`
65
- ```ts
66
- import type { DataType } from "@rbxts/flamework-binary-serializer";
67
- import { MessageEmitter } from "@rbxts/tether";
68
-
69
- export const messaging = MessageEmitter.create<MessageData>();
70
-
71
- export const enum Message {
72
- Increment,
73
- IncrementReturn
74
- }
75
-
76
- export interface MessageData {
77
- [Message.Increment]: DataType.u8;
78
- [Message.IncrementReturn]: DataType.u8;
79
- }
80
- ```
81
-
82
- ### Server
83
- ```ts
84
- import { Message, messaging } from "shared/messaging";
85
-
86
- messaging.server.setCallback(Message.Increment, Message.IncrementReturn, (_, n) => n + 1);
87
- ```
88
-
89
- ### Client
90
- ```ts
91
- import { Message, messaging } from "shared/messaging";
92
-
93
- messaging.server
94
- .invoke(Message.Increment, Message.IncrementReturn, 69)
95
- .then(print); // 70 - incremented by the server
96
-
97
- // or use await style
98
- async function main(): Promise<void> {
99
- const value = await messaging.server.invoke(Message.Increment, Message.IncrementReturn, 69);
100
- print(value) // 70
101
- }
102
-
103
- main();
104
- ```
105
-
106
- ## Middleware
107
- Drop, delay, or modify requests
108
-
109
- ### Creating middleware
110
-
111
- **Note:** These client/server middlewares can be implemented as shared middlewares. This is strictly an example.
112
- #### Client
113
- ```ts
114
- import type { ClientMiddleware } from "@rbxts/tether";
115
-
116
- export function logClient(): ClientMiddleware {
117
- return message => (player, ctx) => print(`[LOG]: Sent message '${message}' to player ${player} with data:`, ctx.data);
118
- }
119
- ```
120
-
121
- #### Server
122
- ```ts
123
- import type { ServerMiddleware } from "@rbxts/tether";
124
-
125
- export function logServer(): ServerMiddleware {
126
- return message => ctx => print(`[LOG]: Sent message '${message}' to server with data:`, ctx.data);
127
- }
128
- ```
129
-
130
- #### Shared
131
- ```ts
132
- import { type SharedMiddleware, DropRequest } from "@rbxts/tether";
133
-
134
- export function rateLimit(interval: number): SharedMiddleware {
135
- let lastRequest = 0;
136
-
137
- return message => // message attempting to be sent
138
- () => { // no data/player - it's a shared middleware
139
- if (os.clock() - lastRequest < interval)
140
- return DropRequest;
141
-
142
- lastRequest = os.clock();
143
- };
144
- }
145
- ```
146
-
147
- #### Transforming data
148
- ```ts
149
- import type { ServerMiddleware } from "@rbxts/tether";
150
-
151
- export function incrementNumberData(): ServerMiddleware<number> {
152
- // sets the data to be used by the any subsequent middlewares as well as sent through the remote
153
- return () => ({ data, updateData }) => updateData(data + 1);
154
- }
155
- ```
156
-
157
- ### Using middleware
158
- ```ts
159
- import type { DataType } from "@rbxts/flamework-binary-serializer";
160
- import { MessageEmitter, BuiltinMiddlewares } from "@rbxts/tether";
161
-
162
- export const messaging = MessageEmitter.create<MessageData>();
163
- messaging.middleware
164
- // only allows requests to the server every 5 seconds,
165
- // drops any requests that occur within 5 seconds of each other
166
- .useServer(Message.Test, BuiltinMiddlewares.rateLimit(5))
167
- // will be just one byte!
168
- .useShared(Message.Packed, () => ctx => print("Packed object size:", buffer.len(ctx.getRawData().buffer)));
169
- // logs every message fired
170
- .useServerGlobal(logServer())
171
- .useClientGlobal(logClient())
172
- .useSharedGlobal(BuiltinMiddlewares.debug()); // verbosely logs every packet sent
173
- .useServer(Message.Test, incrementNumberData()) // error! - data for Message.Test is not a number
174
- .useServerGlobal(incrementNumberData()); // error! - global data type is always 'unknown', we cannot guarantee a number
175
-
176
- export const enum Message {
177
- Test,
178
- Packed
179
- }
180
-
181
- export interface MessageData {
182
- [Message.Test]: {
183
- readonly foo: string;
184
- readonly n: DataType.u8;
185
- };
186
- [Message.Packed]: DataType.Packed<{
187
- readonly boolean1: boolean;
188
- readonly boolean2: boolean;
189
- readonly boolean3: boolean;
190
- readonly boolean4: boolean;
191
- readonly boolean5: boolean;
192
- readonly boolean6: boolean;
193
- readonly boolean7: boolean;
194
- readonly boolean8: boolean;
195
- }>;
196
- }
1
+ # Tether
2
+ A message-based networking solution for Roblox with automatic binary serialization and type validation.
3
+
4
+ > [!CAUTION]
5
+ > Depends on `rbxts-transformer-flamework`!
6
+
7
+ ## Usage
8
+
9
+ ### In `shared/messaging.ts`
10
+ ```ts
11
+ import type { DataType } from "@rbxts/flamework-binary-serializer";
12
+ import { MessageEmitter } from "@rbxts/tether";
13
+
14
+ export const messaging = MessageEmitter.create<MessageData>();
15
+
16
+ export const enum Message {
17
+ Test,
18
+ Packed
19
+ }
20
+
21
+ export interface MessageData {
22
+ [Message.Test]: {
23
+ readonly foo: string;
24
+ readonly n: DataType.u8;
25
+ };
26
+ [Message.Packed]: DataType.Packed<{
27
+ readonly boolean1: boolean;
28
+ readonly boolean2: boolean;
29
+ readonly boolean3: boolean;
30
+ readonly boolean4: boolean;
31
+ readonly boolean5: boolean;
32
+ readonly boolean6: boolean;
33
+ readonly boolean7: boolean;
34
+ readonly boolean8: boolean;
35
+ }>;
36
+ }
37
+ ```
38
+
39
+ > [!CAUTION]
40
+ > Every single message kind must implement an interface for it's data (in the example that would be the object types in `MessageData`). Message serialization (as well as your message itself) will not work if you don't do this.
41
+
42
+ ### Server
43
+ ```ts
44
+ import { Message, messaging } from "shared/messaging";
45
+
46
+ messaging.server.on(Message.Test, (player, data) => print(player, "sent data:", data));
47
+ ```
48
+
49
+ ### Client
50
+ ```ts
51
+ import { Message, messaging } from "shared/messaging";
52
+
53
+ messaging.server.emit(Message.Test, {
54
+ foo: "bar",
55
+ n: 69
56
+ });
57
+ ```
58
+
59
+ ## Simulated Remote Functions
60
+ Tether does not directly use RemoteFunctions since it's based on the MessageEmitter structure. However I have created a small framework to simulate remote functions, as shown below.
61
+
62
+ For each function you will need two messages. One to invoke the function, and one to send the return value back (which is done automatically).
63
+
64
+ ### In `shared/messaging.ts`
65
+ ```ts
66
+ import type { DataType } from "@rbxts/flamework-binary-serializer";
67
+ import { MessageEmitter } from "@rbxts/tether";
68
+
69
+ export const messaging = MessageEmitter.create<MessageData>();
70
+
71
+ export const enum Message {
72
+ Increment,
73
+ IncrementReturn
74
+ }
75
+
76
+ export interface MessageData {
77
+ [Message.Increment]: DataType.u8;
78
+ [Message.IncrementReturn]: DataType.u8;
79
+ }
80
+ ```
81
+
82
+ ### Server
83
+ ```ts
84
+ import { Message, messaging } from "shared/messaging";
85
+
86
+ messaging.server.setCallback(Message.Increment, Message.IncrementReturn, (_, n) => n + 1);
87
+ ```
88
+
89
+ ### Client
90
+ ```ts
91
+ import { Message, messaging } from "shared/messaging";
92
+
93
+ messaging.server
94
+ .invoke(Message.Increment, Message.IncrementReturn, 69)
95
+ .then(print); // 70 - incremented by the server
96
+
97
+ // or use await style
98
+ async function main(): Promise<void> {
99
+ const value = await messaging.server.invoke(Message.Increment, Message.IncrementReturn, 69);
100
+ print(value) // 70
101
+ }
102
+
103
+ main();
104
+ ```
105
+
106
+ ## Middleware
107
+ Drop, delay, or modify requests
108
+
109
+ ### Creating middleware
110
+
111
+ **Note:** These client/server middlewares can be implemented as shared middlewares. This is strictly an example.
112
+ #### Client
113
+ ```ts
114
+ import type { ClientMiddleware } from "@rbxts/tether";
115
+
116
+ export function logClient(): ClientMiddleware {
117
+ return message => (player, ctx) => print(`[LOG]: Sent message '${message}' to player ${player} with data:`, ctx.data);
118
+ }
119
+ ```
120
+
121
+ #### Server
122
+ ```ts
123
+ import type { ServerMiddleware } from "@rbxts/tether";
124
+
125
+ export function logServer(): ServerMiddleware {
126
+ return message => ctx => print(`[LOG]: Sent message '${message}' to server with data:`, ctx.data);
127
+ }
128
+ ```
129
+
130
+ #### Shared
131
+ ```ts
132
+ import { type SharedMiddleware, DropRequest } from "@rbxts/tether";
133
+
134
+ export function rateLimit(interval: number): SharedMiddleware {
135
+ let lastRequest = 0;
136
+
137
+ return message => // message attempting to be sent
138
+ () => { // no data/player - it's a shared middleware
139
+ if (os.clock() - lastRequest < interval)
140
+ return DropRequest;
141
+
142
+ lastRequest = os.clock();
143
+ };
144
+ }
145
+ ```
146
+
147
+ #### Transforming data
148
+ ```ts
149
+ import type { ServerMiddleware } from "@rbxts/tether";
150
+
151
+ export function incrementNumberData(): ServerMiddleware<number> {
152
+ // sets the data to be used by the any subsequent middlewares as well as sent through the remote
153
+ return () => ({ data, updateData }) => updateData(data + 1);
154
+ }
155
+ ```
156
+
157
+ ### Using middleware
158
+ ```ts
159
+ import type { DataType } from "@rbxts/flamework-binary-serializer";
160
+ import { MessageEmitter, BuiltinMiddlewares } from "@rbxts/tether";
161
+
162
+ export const messaging = MessageEmitter.create<MessageData>();
163
+ messaging.middleware
164
+ // only allows requests to the server every 5 seconds,
165
+ // drops any requests that occur within 5 seconds of each other
166
+ .useServer(Message.Test, BuiltinMiddlewares.rateLimit(5))
167
+ // will be just one byte!
168
+ .useShared(Message.Packed, () => ctx => print("Packed object size:", buffer.len(ctx.getRawData().buffer)));
169
+ // logs every message fired
170
+ .useServerGlobal(logServer())
171
+ .useClientGlobal(logClient())
172
+ .useSharedGlobal(BuiltinMiddlewares.debug()); // verbosely logs every packet sent
173
+ .useServer(Message.Test, incrementNumberData()) // error! - data for Message.Test is not a number
174
+ .useServerGlobal(incrementNumberData()); // error! - global data type is always 'unknown', we cannot guarantee a number
175
+
176
+ export const enum Message {
177
+ Test,
178
+ Packed
179
+ }
180
+
181
+ export interface MessageData {
182
+ [Message.Test]: {
183
+ readonly foo: string;
184
+ readonly n: DataType.u8;
185
+ };
186
+ [Message.Packed]: DataType.Packed<{
187
+ readonly boolean1: boolean;
188
+ readonly boolean2: boolean;
189
+ readonly boolean3: boolean;
190
+ readonly boolean4: boolean;
191
+ readonly boolean5: boolean;
192
+ readonly boolean6: boolean;
193
+ readonly boolean7: boolean;
194
+ readonly boolean8: boolean;
195
+ }>;
196
+ }
197
197
  ```
@@ -61,14 +61,23 @@ export declare class MessageEmitter<MessageData> extends Destroyable {
61
61
  */
62
62
  once: <Kind extends keyof MessageData>(message: Kind & BaseMessage, callback: ClientMessageCallback<MessageData[Kind]>) => () => void;
63
63
  /**
64
- * Emits a message to a specific client
64
+ * Emits a message to a specific client or multiple clients
65
65
  *
66
- * @param player The player to whom the message is sent
66
+ * @param player The player(s) to whom the message is sent
67
67
  * @param message The message kind to be sent
68
68
  * @param data The data associated with the message
69
69
  * @param unreliable Whether the message should be sent unreliably
70
70
  */
71
71
  emit: <Kind extends keyof MessageData>(player: Player | Player[], message: Kind & BaseMessage, data?: MessageData[Kind], unreliable?: boolean) => void;
72
+ /**
73
+ * Emits a message to all clients except the specified client(s)
74
+ *
75
+ * @param player The player(s) to whom the message is not sent
76
+ * @param message The message kind to be sent
77
+ * @param data The data associated with the message
78
+ * @param unreliable Whether the message should be sent unreliably
79
+ */
80
+ emitExcept: <Kind extends keyof MessageData>(player: Player | Player[], message: Kind & BaseMessage, data?: MessageData[Kind], unreliable?: boolean) => void;
72
81
  /**
73
82
  * Emits a message to all connected clients
74
83
  *
@@ -94,6 +103,7 @@ export declare class MessageEmitter<MessageData> extends Destroyable {
94
103
  };
95
104
  private validateData;
96
105
  private initialize;
106
+ private onRemoteFire;
97
107
  private readMessageFromPacket;
98
108
  private executeFunctions;
99
109
  private executeEventCallbacks;
@@ -175,6 +175,36 @@ do
175
175
  send(player, getPacket())
176
176
  end)
177
177
  end,
178
+ emitExcept = function(player, message, data, unreliable)
179
+ if unreliable == nil then
180
+ unreliable = false
181
+ end
182
+ local shouldSendTo = function(p)
183
+ local _player = player
184
+ local _result
185
+ if typeof(_player) == "Instance" then
186
+ _result = p ~= player
187
+ else
188
+ local _player_1 = player
189
+ local _p = p
190
+ _result = not (table.find(_player_1, _p) ~= nil)
191
+ end
192
+ return _result
193
+ end
194
+ local _client = self.client
195
+ local _exp = Players:GetPlayers()
196
+ -- ▼ ReadonlyArray.filter ▼
197
+ local _newValue = {}
198
+ local _length = 0
199
+ for _k, _v in _exp do
200
+ if shouldSendTo(_v, _k - 1, _exp) == true then
201
+ _length += 1
202
+ _newValue[_length] = _v
203
+ end
204
+ end
205
+ -- ▲ ReadonlyArray.filter ▲
206
+ _client.emit(_newValue, message, data, unreliable)
207
+ end,
178
208
  emitAll = function(message, data, unreliable)
179
209
  if unreliable == nil then
180
210
  unreliable = false
@@ -342,19 +372,26 @@ do
342
372
  function MessageEmitter:initialize()
343
373
  if RunService:IsClient() then
344
374
  self.janitor:Add(self.clientEvents.sendClientMessage:connect(function(serializedPacket)
345
- local sentMessage = self:readMessageFromPacket(serializedPacket)
346
- self:executeEventCallbacks(sentMessage, serializedPacket)
347
- self:executeFunctions(sentMessage, serializedPacket)
375
+ return self:onRemoteFire(serializedPacket)
376
+ end))
377
+ self.janitor:Add(self.clientEvents.sendUnreliableClientMessage:connect(function(serializedPacket)
378
+ return self:onRemoteFire(serializedPacket)
348
379
  end))
349
380
  else
350
381
  self.janitor:Add(self.serverEvents.sendServerMessage:connect(function(player, serializedPacket)
351
- local sentMessage = self:readMessageFromPacket(serializedPacket)
352
- self:executeEventCallbacks(sentMessage, serializedPacket, player)
353
- self:executeFunctions(sentMessage, serializedPacket)
382
+ return self:onRemoteFire(serializedPacket, player)
383
+ end))
384
+ self.janitor:Add(self.serverEvents.sendUnreliableServerMessage:connect(function(player, serializedPacket)
385
+ return self:onRemoteFire(serializedPacket, player)
354
386
  end))
355
387
  end
356
388
  return self
357
389
  end
390
+ function MessageEmitter:onRemoteFire(serializedPacket, player)
391
+ local sentMessage = self:readMessageFromPacket(serializedPacket)
392
+ self:executeEventCallbacks(sentMessage, serializedPacket, player)
393
+ self:executeFunctions(sentMessage, serializedPacket)
394
+ end
358
395
  function MessageEmitter:readMessageFromPacket(serializedPacket)
359
396
  return buffer.readu8(serializedPacket.buffer, 0)
360
397
  end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rbxts/tether",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "main": "out/init.lua",
5
5
  "scripts": {
6
6
  "build": "rbxtsc",