@rbxts/tether 1.2.7 → 1.2.9

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,4 +1,5 @@
1
1
  # Tether
2
+
2
3
  A message-based networking solution for Roblox with automatic binary serialization and type validation.
3
4
 
4
5
  > [!CAUTION]
@@ -7,15 +8,16 @@ A message-based networking solution for Roblox with automatic binary serializati
7
8
  ## Usage
8
9
 
9
10
  ### In `shared/messaging.ts`
11
+
10
12
  ```ts
11
- import type { DataType } from "@rbxts/flamework-binary-serializer";
12
13
  import { MessageEmitter } from "@rbxts/tether";
14
+ import type { DataType } from "@rbxts/flamework-binary-serializer";
13
15
 
14
16
  export const messaging = MessageEmitter.create<MessageData>();
15
17
 
16
18
  export const enum Message {
17
19
  Test,
18
- Packed
20
+ Packed,
19
21
  }
20
22
 
21
23
  export interface MessageData {
@@ -40,37 +42,43 @@ export interface MessageData {
40
42
  > 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
43
 
42
44
  ### Server
45
+
43
46
  ```ts
44
47
  import { Message, messaging } from "shared/messaging";
45
48
 
46
- messaging.server.on(Message.Test, (player, data) => print(player, "sent data:", data));
49
+ messaging.server.on(Message.Test, (player, data) =>
50
+ print(player, "sent data:", data)
51
+ );
47
52
  ```
48
53
 
49
54
  ### Client
55
+
50
56
  ```ts
51
57
  import { Message, messaging } from "shared/messaging";
52
58
 
53
59
  messaging.server.emit(Message.Test, {
54
60
  foo: "bar",
55
- n: 69
61
+ n: 69,
56
62
  });
57
63
  ```
58
64
 
59
65
  ## Simulated Remote Functions
66
+
60
67
  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
68
 
62
69
  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
70
 
64
71
  ### In `shared/messaging.ts`
72
+
65
73
  ```ts
66
- import type { DataType } from "@rbxts/flamework-binary-serializer";
67
74
  import { MessageEmitter } from "@rbxts/tether";
75
+ import type { DataType } from "@rbxts/flamework-binary-serializer";
68
76
 
69
77
  export const messaging = MessageEmitter.create<MessageData>();
70
78
 
71
79
  export const enum Message {
72
80
  Increment,
73
- IncrementReturn
81
+ IncrementReturn,
74
82
  }
75
83
 
76
84
  export interface MessageData {
@@ -80,13 +88,19 @@ export interface MessageData {
80
88
  ```
81
89
 
82
90
  ### Server
91
+
83
92
  ```ts
84
93
  import { Message, messaging } from "shared/messaging";
85
94
 
86
- messaging.server.setCallback(Message.Increment, Message.IncrementReturn, (_, n) => n + 1);
95
+ messaging.server.setCallback(
96
+ Message.Increment,
97
+ Message.IncrementReturn,
98
+ (_, n) => n + 1
99
+ );
87
100
  ```
88
101
 
89
102
  ### Client
103
+
90
104
  ```ts
91
105
  import { Message, messaging } from "shared/messaging";
92
106
 
@@ -96,81 +110,98 @@ messaging.server
96
110
 
97
111
  // or use await style
98
112
  async function main(): Promise<void> {
99
- const value = await messaging.server.invoke(Message.Increment, Message.IncrementReturn, 69);
100
- print(value) // 70
113
+ const value = await messaging.server.invoke(
114
+ Message.Increment,
115
+ Message.IncrementReturn,
116
+ 69
117
+ );
118
+ print(value); // 70
101
119
  }
102
120
 
103
121
  main();
104
122
  ```
105
123
 
106
124
  ## Middleware
125
+
107
126
  Drop, delay, or modify requests
108
127
 
109
128
  ### Creating middleware
110
129
 
111
130
  **Note:** These client/server middlewares can be implemented as shared middlewares. This is strictly an example.
131
+
112
132
  #### Client
133
+
113
134
  ```ts
114
135
  import type { ClientMiddleware } from "@rbxts/tether";
115
136
 
116
137
  export function logClient(): ClientMiddleware {
117
- return message => (player, ctx) => print(`[LOG]: Sent message '${message}' to player ${player} with data:`, ctx.data);
138
+ return (player, ctx) =>
139
+ print(
140
+ `[LOG]: Sent message '${ctx.message}' to player ${player} with data:`,
141
+ ctx.data
142
+ );
118
143
  }
119
144
  ```
120
145
 
121
146
  #### Server
147
+
122
148
  ```ts
123
149
  import type { ServerMiddleware } from "@rbxts/tether";
124
150
 
125
151
  export function logServer(): ServerMiddleware {
126
- return message => ctx => print(`[LOG]: Sent message '${message}' to server with data:`, ctx.data);
152
+ return (ctx) =>
153
+ print(
154
+ `[LOG]: Sent message '${ctx.message}' to server with data:`,
155
+ ctx.data
156
+ );
127
157
  }
128
158
  ```
129
159
 
130
160
  #### Shared
161
+
131
162
  ```ts
132
163
  import { type SharedMiddleware, DropRequest } from "@rbxts/tether";
133
164
 
134
165
  export function rateLimit(interval: number): SharedMiddleware {
135
166
  let lastRequest = 0;
136
167
 
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;
168
+ return () => {
169
+ if (os.clock() - lastRequest < interval) return DropRequest;
141
170
 
142
- lastRequest = os.clock();
143
- };
171
+ lastRequest = os.clock();
172
+ };
144
173
  }
145
174
  ```
146
175
 
147
176
  #### Transforming data
177
+
148
178
  ```ts
149
179
  import type { ServerMiddleware } from "@rbxts/tether";
150
180
 
151
181
  export function incrementNumberData(): ServerMiddleware<number> {
152
182
  // 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);
183
+ return ({ data, updateData }) => updateData(data + 1);
154
184
  }
155
185
  ```
156
186
 
157
187
  ### Using middleware
188
+
158
189
  ```ts
159
- import type { DataType } from "@rbxts/flamework-binary-serializer";
160
190
  import { MessageEmitter, BuiltinMiddlewares } from "@rbxts/tether";
191
+ import type { DataType } from "@rbxts/flamework-binary-serializer";
161
192
 
162
193
  export const messaging = MessageEmitter.create<MessageData>();
163
194
  messaging.middleware
164
195
  // only allows requests to the server every 5 seconds,
165
196
  // drops any requests that occur within 5 seconds of each other
166
- .useServer(Message.Test, BuiltinMiddlewares.rateLimit(5))
197
+ .useServer(Message.Test, BuiltinMiddlewares.rateLimit(5))
167
198
  // will be just one byte!
168
- .useShared(Message.Packed, () => ctx => print("Packed object size:", buffer.len(ctx.getRawData().buffer)));
199
+ .useShared(Message.Packed, ctx => print("Packed object size:", buffer.len(ctx.getRawData().buffer)));
169
200
  // logs every message fired
170
201
  .useServerGlobal(logServer())
171
202
  .useClientGlobal(logClient())
172
203
  .useSharedGlobal(BuiltinMiddlewares.debug()); // verbosely logs every packet sent
173
- .useServer(Message.Test, incrementNumberData()) // error! - data for Message.Test is not a number
204
+ .useServer(Message.Test, incrementNumberData()) // error! - data for Message.Test is not a number
174
205
  .useServerGlobal(incrementNumberData()); // error! - global data type is always 'unknown', we cannot guarantee a number
175
206
 
176
207
  export const enum Message {
@@ -194,4 +225,4 @@ export interface MessageData {
194
225
  readonly boolean8: boolean;
195
226
  }>;
196
227
  }
197
- ```
228
+ ```
@@ -18,10 +18,8 @@ do
18
18
  ]]
19
19
  local function simulatePing(pingInMs)
20
20
  return function()
21
- return function()
22
- task.wait(pingInMs / 1000)
23
- return nil
24
- end
21
+ task.wait(pingInMs / 1000)
22
+ return nil
25
23
  end
26
24
  end
27
25
  _container.simulatePing = simulatePing
@@ -38,13 +36,11 @@ do
38
36
  if throwError == nil then
39
37
  throwError = true
40
38
  end
41
- return function(message)
42
- return function(ctx)
43
- local rawData = ctx.getRawData()
44
- local totalSize = buffer.len(rawData.buffer) + #rawData.blobs * BLOB_SIZE
45
- if totalSize > maxBytes then
46
- return if throwError then error(`[@rbxts/tether]: Message '{message}' exceeded maximum packet size of {maxBytes} bytes`) else DropRequest
47
- end
39
+ return function(ctx)
40
+ local rawData = ctx.getRawData()
41
+ local totalSize = buffer.len(rawData.buffer) + #rawData.blobs * BLOB_SIZE
42
+ if totalSize > maxBytes then
43
+ return if throwError then error(`[@rbxts/tether]: Message '{ctx.message}' exceeded maximum packet size of {maxBytes} bytes`) else DropRequest
48
44
  end
49
45
  end
50
46
  end
@@ -60,12 +56,10 @@ do
60
56
  local function rateLimit(interval)
61
57
  local lastRequest = 0
62
58
  return function()
63
- return function()
64
- if os.clock() - lastRequest < interval then
65
- return DropRequest
66
- end
67
- lastRequest = os.clock()
59
+ if os.clock() - lastRequest < interval then
60
+ return DropRequest
68
61
  end
62
+ lastRequest = os.clock()
69
63
  end
70
64
  end
71
65
  _container.rateLimit = rateLimit
@@ -86,24 +80,23 @@ do
86
80
 
87
81
  ]]
88
82
  local function debug(schema)
89
- return function(message)
90
- return function(_param)
91
- local data = _param.data
92
- local getRawData = _param.getRawData
93
- local rawData = getRawData()
94
- local bufferSize = buffer.len(rawData.buffer)
95
- local blobsSize = #rawData.blobs * BLOB_SIZE
96
- local schemaString = if schema ~= nil then " " .. table.concat(string.split(repr(schema[1], {
97
- pretty = true,
98
- }), "\n"), "\n ") else "unknown"
99
- local text = { "\n", horizontalLine, "\n", "Packet sent to ", (if RunService:IsServer() then "client" else "server"), "!\n", " - Message: ", message, "\n", " - Data: ", repr(data, {
100
- pretty = true,
101
- }), "\n", " - Raw data:\n", " - Buffer: ", bufferToString(rawData.buffer), "\n", " - Blobs: ", repr(rawData.blobs, {
102
- pretty = false,
103
- robloxClassName = true,
104
- }), "\n", " - Packet size: ", bufferSize + blobsSize, " bytes\n", " - Buffer: ", bufferSize, " bytes\n", " - Blobs: ", blobsSize, " bytes\n", " - Schema: ", schemaString, "\n", horizontalLine, "\n" }
105
- print(table.concat(text, ""))
106
- end
83
+ return function(_param)
84
+ local message = _param.message
85
+ local data = _param.data
86
+ local getRawData = _param.getRawData
87
+ local rawData = getRawData()
88
+ local bufferSize = buffer.len(rawData.buffer)
89
+ local blobsSize = #rawData.blobs * BLOB_SIZE
90
+ local schemaString = if schema ~= nil then " " .. table.concat(string.split(repr(schema[1], {
91
+ pretty = true,
92
+ }), "\n"), "\n ") else "unknown"
93
+ local text = { "\n", horizontalLine, "\n", "Packet sent to ", (if RunService:IsServer() then "client" else "server"), "!\n", " - Message: ", message, "\n", " - Data: ", repr(data, {
94
+ pretty = true,
95
+ }), "\n", " - Raw data:\n", " - Buffer: ", bufferToString(rawData.buffer), "\n", " - Blobs: ", repr(rawData.blobs, {
96
+ pretty = false,
97
+ robloxClassName = true,
98
+ }), "\n", " - Packet size: ", bufferSize + blobsSize, " bytes\n", " - Buffer: ", bufferSize, " bytes\n", " - Blobs: ", blobsSize, " bytes\n", " - Schema: ", schemaString, "\n", horizontalLine, "\n" }
99
+ print(table.concat(text, ""))
107
100
  end
108
101
  end
109
102
  _container.debug = debug
@@ -2,7 +2,12 @@ import { Modding } from "@flamework/core";
2
2
  import Destroyable from "@rbxts/destroyable";
3
3
  import { MiddlewareProvider } from "./middleware";
4
4
  import type { ClientMessageCallback, ServerMessageCallback, BaseMessage, MessageEmitterMetadata, ClientMessageFunctionCallback, ServerMessageFunctionCallback } from "./structs";
5
+ interface MessageEmitterOptions {
6
+ readonly batchRemotes: boolean;
7
+ readonly batchRate: number;
8
+ }
5
9
  export declare class MessageEmitter<MessageData> extends Destroyable {
10
+ private readonly options;
6
11
  readonly middleware: MiddlewareProvider<MessageData>;
7
12
  private readonly clientCallbacks;
8
13
  private readonly clientFunctions;
@@ -10,10 +15,13 @@ export declare class MessageEmitter<MessageData> extends Destroyable {
10
15
  private readonly serverFunctions;
11
16
  private readonly guards;
12
17
  private readonly serializers;
18
+ private serverQueue;
19
+ private clientBroadcastQueue;
20
+ private clientQueue;
13
21
  private serverEvents;
14
22
  private clientEvents;
15
23
  /** @metadata macro */
16
- static create<MessageData>(meta?: Modding.Many<MessageEmitterMetadata<MessageData>>): MessageEmitter<MessageData>;
24
+ static create<MessageData>(options?: Partial<MessageEmitterOptions>, meta?: Modding.Many<MessageEmitterMetadata<MessageData>>): MessageEmitter<MessageData>;
17
25
  private constructor();
18
26
  readonly server: {
19
27
  /**
@@ -101,11 +109,15 @@ export declare class MessageEmitter<MessageData> extends Destroyable {
101
109
  */
102
110
  setCallback: <Kind extends keyof MessageData, ReturnKind extends keyof MessageData>(message: Kind & BaseMessage, returnMessage: ReturnKind & BaseMessage, callback: ClientMessageFunctionCallback<MessageData[Kind], MessageData[ReturnKind]>) => () => void;
103
111
  };
104
- private validateData;
105
112
  private initialize;
113
+ private update;
114
+ private runClientMiddlewares;
115
+ private runServerMiddlewares;
116
+ private validateData;
106
117
  private onRemoteFire;
107
118
  private executeFunctions;
108
119
  private executeEventCallbacks;
120
+ private deserializeAndValidate;
109
121
  private once;
110
122
  private on;
111
123
  private getPacket;
@@ -115,3 +127,4 @@ export declare class MessageEmitter<MessageData> extends Destroyable {
115
127
  private createMessageSerializer;
116
128
  private getSerializer;
117
129
  }
130
+ export {};
@@ -11,12 +11,18 @@ local repr = TS.import(script, TS.getModule(script, "@rbxts", "repr").out)
11
11
  local _middleware = TS.import(script, script.Parent, "middleware")
12
12
  local DropRequest = _middleware.DropRequest
13
13
  local MiddlewareProvider = _middleware.MiddlewareProvider
14
- -- TODO: error when trying to do something like server.emit() from the server
14
+ local Object = TS.import(script, TS.getModule(script, "@rbxts", "object-utils"))
15
15
  local remotes = Networking.createEvent("@rbxts/tether:message-emitter@remotes")
16
+ local noServerListen = "[@rbxts/tether]: Cannot listen to server message from client"
17
+ local noClientListen = "[@rbxts/tether]: Cannot listen to client message from server"
16
18
  local metaGenerationFailed = "[@rbxts/tether]: Failed to generate message metadata - make sure you have the Flamework transformer and are using Flamework macro-friendly types in your schemas"
17
19
  local guardFailed = function(message, data)
18
20
  return `[@rbxts/tether]: Type validation guard failed for message '{message}' - check your sent data\nSent data: {repr(data)}`
19
21
  end
22
+ local defaultMesssageEmitterOptions = {
23
+ batchRemotes = true,
24
+ batchRate = 1 / 24,
25
+ }
20
26
  local MessageEmitter
21
27
  do
22
28
  local super = Destroyable
@@ -31,8 +37,12 @@ do
31
37
  local self = setmetatable({}, MessageEmitter)
32
38
  return self:constructor(...) or self
33
39
  end
34
- function MessageEmitter:constructor()
40
+ function MessageEmitter:constructor(options)
41
+ if options == nil then
42
+ options = defaultMesssageEmitterOptions
43
+ end
35
44
  super.constructor(self)
45
+ self.options = options
36
46
  self.middleware = MiddlewareProvider.new()
37
47
  self.clientCallbacks = {}
38
48
  self.clientFunctions = {}
@@ -40,62 +50,51 @@ do
40
50
  self.serverFunctions = {}
41
51
  self.guards = {}
42
52
  self.serializers = {}
53
+ self.serverQueue = {}
54
+ self.clientBroadcastQueue = {}
55
+ self.clientQueue = {}
43
56
  self.server = {
44
57
  on = function(message, callback)
58
+ if RunService:IsClient() then
59
+ error(noServerListen)
60
+ end
45
61
  return self:on(message, callback, self.serverCallbacks)
46
62
  end,
47
63
  once = function(message, callback)
64
+ if RunService:IsClient() then
65
+ error(noServerListen)
66
+ end
48
67
  return self:once(message, callback, self.serverCallbacks)
49
68
  end,
50
69
  emit = function(message, data, unreliable)
51
70
  if unreliable == nil then
52
71
  unreliable = false
53
72
  end
54
- local updateData = function(newData)
55
- data = newData
56
- return nil
57
- end
58
- local getPacket = function()
59
- return self:getPacket(message, data)
60
- end
61
- if not self:validateData(message, data) then
62
- return nil
73
+ if RunService:IsServer() then
74
+ error("[@rbxts/tether]: Cannot emit message to server from server")
63
75
  end
64
76
  task.spawn(function()
65
- local ctx = {
66
- data = data,
67
- updateData = updateData,
68
- getRawData = getPacket,
69
- }
70
- for _, globalMiddleware in self.middleware:getServerGlobal() do
71
- if not self:validateData(message, data) then
72
- return nil
73
- end
74
- local result = globalMiddleware(message)(ctx)
75
- if result == DropRequest then
76
- return nil
77
- end
78
- end
79
- for _, middleware in self.middleware:getServer(message) do
80
- if not self:validateData(message, data) then
81
- return nil
82
- end
83
- local result = middleware(message)(ctx)
84
- if result == DropRequest then
85
- return nil
86
- end
87
- end
88
- if not self:validateData(message, data) then
77
+ local _binding = self:runServerMiddlewares(message, data)
78
+ local dropRequest = _binding[1]
79
+ local newData = _binding[2]
80
+ if dropRequest then
89
81
  return nil
90
82
  end
91
- local send = if unreliable then self.clientEvents.sendUnreliableServerMessage else self.clientEvents.sendServerMessage
92
- send(getPacket())
83
+ local _serverQueue = self.serverQueue
84
+ local _arg0 = { message, newData, unreliable }
85
+ table.insert(_serverQueue, _arg0)
86
+ if not self.options.batchRemotes then
87
+ self:update()
88
+ end
93
89
  end)
94
90
  end,
95
91
  invoke = TS.async(function(message, returnMessage, data, unreliable)
96
92
  if unreliable == nil then
97
93
  unreliable = false
98
94
  end
95
+ if RunService:IsServer() then
96
+ error("[@rbxts/tether]: Cannot invoke server function from server")
97
+ end
99
98
  local _clientFunctions = self.clientFunctions
100
99
  local _returnMessage = returnMessage
101
100
  if not (_clientFunctions[_returnMessage] ~= nil) then
@@ -118,6 +117,9 @@ do
118
117
  return returnValue
119
118
  end),
120
119
  setCallback = function(message, returnMessage, callback)
120
+ if RunService:IsClient() then
121
+ error(noServerListen)
122
+ end
121
123
  return self.server.on(message, function(player, data)
122
124
  local returnValue = callback(player, data)
123
125
  self.client.emit(player, returnMessage, returnValue)
@@ -126,54 +128,37 @@ do
126
128
  }
127
129
  self.client = {
128
130
  on = function(message, callback)
131
+ if RunService:IsServer() then
132
+ error(noClientListen)
133
+ end
129
134
  return self:on(message, callback, self.clientCallbacks)
130
135
  end,
131
136
  once = function(message, callback)
137
+ if RunService:IsServer() then
138
+ error(noClientListen)
139
+ end
132
140
  return self:once(message, callback, self.clientCallbacks)
133
141
  end,
134
142
  emit = function(player, message, data, unreliable)
135
143
  if unreliable == nil then
136
144
  unreliable = false
137
145
  end
138
- local updateData = function(newData)
139
- data = newData
140
- return nil
141
- end
142
- local getPacket = function()
143
- return self:getPacket(message, data)
144
- end
145
- if not self:validateData(message, data) then
146
- return nil
146
+ if RunService:IsClient() then
147
+ error("[@rbxts/tether]: Cannot emit message to client from client")
147
148
  end
148
149
  task.spawn(function()
149
- local ctx = {
150
- data = data,
151
- updateData = updateData,
152
- getRawData = getPacket,
153
- }
154
- for _, globalMiddleware in self.middleware:getClientGlobal() do
155
- if not self:validateData(message, data) then
156
- return nil
157
- end
158
- local result = globalMiddleware(message)(player, ctx)
159
- if result == DropRequest then
160
- return nil
161
- end
162
- end
163
- for _, middleware in self.middleware:getClient(message) do
164
- if not self:validateData(message, data) then
165
- return nil
166
- end
167
- local result = middleware(message)(player, ctx)
168
- if result == DropRequest then
169
- return nil
170
- end
171
- end
172
- if not self:validateData(message, data) then
150
+ local _binding = self:runClientMiddlewares(message, data)
151
+ local dropRequest = _binding[1]
152
+ local newData = _binding[2]
153
+ if dropRequest then
173
154
  return nil
174
155
  end
175
- local send = if unreliable then self.serverEvents.sendUnreliableClientMessage else self.serverEvents.sendClientMessage
176
- send(player, getPacket())
156
+ local _clientQueue = self.clientQueue
157
+ local _arg0 = { player, message, newData, unreliable }
158
+ table.insert(_clientQueue, _arg0)
159
+ if not self.options.batchRemotes then
160
+ self:update()
161
+ end
177
162
  end)
178
163
  end,
179
164
  emitExcept = function(player, message, data, unreliable)
@@ -210,56 +195,31 @@ do
210
195
  if unreliable == nil then
211
196
  unreliable = false
212
197
  end
213
- local updateData = function(newData)
214
- data = newData
215
- return nil
216
- end
217
- local getPacket = function()
218
- return self:getPacket(message, data)
219
- end
220
- if not self:validateData(message, data) then
221
- return nil
198
+ if RunService:IsClient() then
199
+ error("[@rbxts/tether]: Cannot emit message to all clients from client")
222
200
  end
223
201
  task.spawn(function()
224
- local ctx = {
225
- data = data,
226
- updateData = updateData,
227
- getRawData = getPacket,
228
- }
229
- local players = Players:GetPlayers()
230
- for _, globalMiddleware in self.middleware:getClientGlobal() do
231
- for _1, player in players do
232
- if not self:validateData(message, data) then
233
- return nil
234
- end
235
- local result = globalMiddleware(message)(player, ctx)
236
- if result == DropRequest then
237
- return nil
238
- end
239
- end
240
- end
241
- for _, middleware in self.middleware:getClient(message) do
242
- for _1, player in players do
243
- if not self:validateData(message, data) then
244
- return nil
245
- end
246
- local result = middleware(message)(player, ctx)
247
- if result == DropRequest then
248
- return nil
249
- end
250
- end
251
- end
252
- if not self:validateData(message, data) then
202
+ local _binding = self:runClientMiddlewares(message, data)
203
+ local dropRequest = _binding[1]
204
+ local newData = _binding[2]
205
+ if dropRequest then
253
206
  return nil
254
207
  end
255
- local send = if unreliable then self.serverEvents.sendUnreliableClientMessage else self.serverEvents.sendClientMessage
256
- send:broadcast(getPacket())
208
+ local _clientBroadcastQueue = self.clientBroadcastQueue
209
+ local _arg0 = { message, newData, unreliable }
210
+ table.insert(_clientBroadcastQueue, _arg0)
211
+ if not self.options.batchRemotes then
212
+ self:update()
213
+ end
257
214
  end)
258
215
  end,
259
216
  invoke = TS.async(function(message, returnMessage, player, data, unreliable)
260
217
  if unreliable == nil then
261
218
  unreliable = false
262
219
  end
220
+ if RunService:IsClient() then
221
+ error("[@rbxts/tether]: Cannot invoke client function from client")
222
+ end
263
223
  local _serverFunctions = self.serverFunctions
264
224
  local _returnMessage = returnMessage
265
225
  if not (_serverFunctions[_returnMessage] ~= nil) then
@@ -282,6 +242,9 @@ do
282
242
  return returnValue
283
243
  end),
284
244
  setCallback = function(message, returnMessage, callback)
245
+ if RunService:IsServer() then
246
+ error(noClientListen)
247
+ end
285
248
  return self.client.on(message, function(data)
286
249
  local returnValue = callback(data)
287
250
  self.server.emit(returnMessage, returnValue)
@@ -347,8 +310,8 @@ do
347
310
  })
348
311
  end
349
312
  end
350
- function MessageEmitter:create(meta)
351
- local emitter = MessageEmitter.new()
313
+ function MessageEmitter:create(options, meta)
314
+ local emitter = MessageEmitter.new(Object.assign({}, defaultMesssageEmitterOptions, options))
352
315
  if meta == nil then
353
316
  warn(metaGenerationFailed)
354
317
  return emitter:initialize()
@@ -365,16 +328,6 @@ do
365
328
  end
366
329
  return emitter:initialize()
367
330
  end
368
- function MessageEmitter:validateData(message, data)
369
- local _guards = self.guards
370
- local _message = message
371
- local guard = _guards[_message]
372
- local guardPassed = guard(data)
373
- if not guardPassed then
374
- warn(guardFailed(message, data))
375
- end
376
- return guardPassed
377
- end
378
331
  function MessageEmitter:initialize()
379
332
  if RunService:IsClient() then
380
333
  self.janitor:Add(self.clientEvents.sendClientMessage:connect(function(serializedPacket)
@@ -391,8 +344,144 @@ do
391
344
  return self:onRemoteFire(serializedPacket, player)
392
345
  end))
393
346
  end
347
+ local elapsed = 0
348
+ local _binding = self.options
349
+ local batchRemotes = _binding.batchRemotes
350
+ local batchRate = _binding.batchRate
351
+ if not batchRemotes then
352
+ return self
353
+ end
354
+ self.janitor:Add(RunService.Heartbeat:Connect(function(dt)
355
+ elapsed += dt
356
+ if elapsed >= batchRate then
357
+ elapsed -= batchRate
358
+ self:update()
359
+ end
360
+ end))
394
361
  return self
395
362
  end
363
+ function MessageEmitter:update()
364
+ for _, _binding in self.clientQueue do
365
+ local player = _binding[1]
366
+ local message = _binding[2]
367
+ local data = _binding[3]
368
+ local unreliable = _binding[4]
369
+ local remote = if unreliable then self.serverEvents.sendUnreliableClientMessage else self.serverEvents.sendClientMessage
370
+ remote(player, self:getPacket(message, data))
371
+ end
372
+ self.clientQueue = {}
373
+ for _, _binding in self.clientBroadcastQueue do
374
+ local message = _binding[1]
375
+ local data = _binding[2]
376
+ local unreliable = _binding[3]
377
+ local remote = if unreliable then self.serverEvents.sendUnreliableClientMessage else self.serverEvents.sendClientMessage
378
+ remote:broadcast(self:getPacket(message, data))
379
+ end
380
+ self.clientBroadcastQueue = {}
381
+ for _, _binding in self.serverQueue do
382
+ local message = _binding[1]
383
+ local data = _binding[2]
384
+ local unreliable = _binding[3]
385
+ local remote = if unreliable then self.clientEvents.sendUnreliableServerMessage else self.clientEvents.sendServerMessage
386
+ remote(self:getPacket(message, data))
387
+ end
388
+ self.serverQueue = {}
389
+ end
390
+ function MessageEmitter:runClientMiddlewares(message, data, player)
391
+ if not self:validateData(message, data) then
392
+ return { true, data }
393
+ end
394
+ local players = player or Players:GetPlayers()
395
+ local ctx = {
396
+ message = message,
397
+ data = data,
398
+ updateData = function(newData)
399
+ data = newData
400
+ return nil
401
+ end,
402
+ getRawData = function()
403
+ return self:getPacket(message, data)
404
+ end,
405
+ }
406
+ for _, globalMiddleware in self.middleware:getClientGlobal() do
407
+ local result = globalMiddleware(players, ctx)
408
+ if not self:validateData(message, data, "Invalid data after global client middleware") then
409
+ return { false, data }
410
+ end
411
+ if result == DropRequest then
412
+ self.middleware:notifyRequestDropped(message, "Global client middleware")
413
+ return { true, data }
414
+ end
415
+ end
416
+ for _, middleware in self.middleware:getClient(message) do
417
+ local result = middleware(players, ctx)
418
+ if not self:validateData(message, data, "Invalid data after client middleware") then
419
+ return { false, data }
420
+ end
421
+ if result == DropRequest then
422
+ self.middleware:notifyRequestDropped(message, "Client middleware")
423
+ return { true, data }
424
+ end
425
+ end
426
+ if not self:validateData(message, data) then
427
+ return { true, data }
428
+ end
429
+ return { false, data }
430
+ end
431
+ function MessageEmitter:runServerMiddlewares(message, data)
432
+ if not self:validateData(message, data) then
433
+ return { true, data }
434
+ end
435
+ local ctx = {
436
+ message = message,
437
+ data = data,
438
+ updateData = function(newData)
439
+ data = newData
440
+ return nil
441
+ end,
442
+ getRawData = function()
443
+ return self:getPacket(message, data)
444
+ end,
445
+ }
446
+ for _, globalMiddleware in self.middleware:getServerGlobal() do
447
+ if not self:validateData(message, data, "Invalid data after global server middleware") then
448
+ return { false, data }
449
+ end
450
+ local result = globalMiddleware(ctx)
451
+ if result == DropRequest then
452
+ self.middleware:notifyRequestDropped(message, "Global server middleware")
453
+ return { true, data }
454
+ end
455
+ end
456
+ for _, middleware in self.middleware:getServer(message) do
457
+ if not self:validateData(message, data, "Invalid data after server middleware") then
458
+ return { false, data }
459
+ end
460
+ local result = middleware(ctx)
461
+ if result == DropRequest then
462
+ self.middleware:notifyRequestDropped(message, "Server middleware")
463
+ return { true, data }
464
+ end
465
+ end
466
+ if not self:validateData(message, data) then
467
+ return { true, data }
468
+ end
469
+ return { false, data }
470
+ end
471
+ function MessageEmitter:validateData(message, data, requestDropReason)
472
+ if requestDropReason == nil then
473
+ requestDropReason = "Invalid data"
474
+ end
475
+ local _guards = self.guards
476
+ local _message = message
477
+ local guard = _guards[_message]
478
+ local guardPassed = guard(data)
479
+ if not guardPassed then
480
+ warn(guardFailed(message, data))
481
+ self.middleware:notifyRequestDropped(message, requestDropReason)
482
+ end
483
+ return guardPassed
484
+ end
396
485
  function MessageEmitter:onRemoteFire(serializedPacket, player)
397
486
  if buffer.len(serializedPacket.messageBuffer) > 1 then
398
487
  return warn("[@rbxts/tether]: Rejected message because message buffer was larger than one byte")
@@ -409,12 +498,7 @@ do
409
498
  if functions == nil then
410
499
  return nil
411
500
  end
412
- local serializer = self:getSerializer(message)
413
- local _packet = serializer
414
- if _packet ~= nil then
415
- _packet = _packet.deserialize(serializedPacket.buffer, serializedPacket.blobs)
416
- end
417
- local packet = _packet
501
+ local packet = self:deserializeAndValidate(message, serializedPacket)
418
502
  for callback in functions do
419
503
  callback(packet)
420
504
  end
@@ -427,12 +511,7 @@ do
427
511
  if callbacks == nil then
428
512
  return nil
429
513
  end
430
- local serializer = self:getSerializer(message)
431
- local _packet = serializer
432
- if _packet ~= nil then
433
- _packet = _packet.deserialize(serializedPacket.buffer, serializedPacket.blobs)
434
- end
435
- local packet = _packet
514
+ local packet = self:deserializeAndValidate(message, serializedPacket)
436
515
  for callback in callbacks do
437
516
  if isServer then
438
517
  callback(player, packet)
@@ -441,6 +520,16 @@ do
441
520
  end
442
521
  end
443
522
  end
523
+ function MessageEmitter:deserializeAndValidate(message, serializedPacket)
524
+ local serializer = self:getSerializer(message)
525
+ local _packet = serializer
526
+ if _packet ~= nil then
527
+ _packet = _packet.deserialize(serializedPacket.buffer, serializedPacket.blobs)
528
+ end
529
+ local packet = _packet
530
+ self:validateData(message, packet)
531
+ return packet
532
+ end
444
533
  function MessageEmitter:once(message, callback, callbacksMap)
445
534
  local destructor
446
535
  destructor = self:on(message, function(player, data)
@@ -5,22 +5,28 @@ type DropRequestSymbol = symbol & {
5
5
  export declare const DropRequest: DropRequestSymbol;
6
6
  export type ClientMiddleware<Data = unknown> = {
7
7
  _client?: void;
8
- } & ((message: BaseMessage) => (player: Player | Player[], ctx: MiddlewareContext<Data>) => DropRequestSymbol | void);
8
+ } & ((player: Player | Player[], ctx: MiddlewareContext<Data>) => DropRequestSymbol | void);
9
9
  export type ServerMiddleware<Data = unknown> = {
10
10
  _server?: void;
11
11
  } & SharedMiddleware<Data>;
12
- export type SharedMiddleware<Data = unknown> = (message: BaseMessage) => (ctx: MiddlewareContext<Data>) => DropRequestSymbol | void;
12
+ export type SharedMiddleware<Data = unknown> = (ctx: MiddlewareContext<Data>) => DropRequestSymbol | void;
13
13
  export type Middleware<Data = unknown> = ServerMiddleware<Data> & ClientMiddleware<Data> & SharedMiddleware<Data>;
14
- export interface MiddlewareContext<Data = unknown> {
14
+ export interface MiddlewareContext<Data = unknown, Message extends BaseMessage = BaseMessage> {
15
+ readonly message: Message;
15
16
  readonly data: Readonly<Data>;
16
17
  updateData: (newData: Data) => void;
17
18
  getRawData: () => SerializedPacket;
18
19
  }
20
+ type RequestDropCallback = (message: BaseMessage, reason?: string) => void;
19
21
  export declare class MiddlewareProvider<MessageData> {
20
22
  private readonly clientGlobalMiddlewares;
21
23
  private readonly serverGlobalMiddlewares;
22
24
  private readonly clientMiddlewares;
23
25
  private readonly serverMiddlewares;
26
+ private readonly requestDroppedCallbacks;
27
+ onRequestDropped(callback: RequestDropCallback): () => void;
28
+ /** @hidden */
29
+ notifyRequestDropped(message: BaseMessage, reason?: string): void;
24
30
  /** @hidden */
25
31
  getClient<Kind extends keyof MessageData>(message: Kind & BaseMessage): ClientMiddleware<MessageData[Kind]>[];
26
32
  /** @hidden */
@@ -1,5 +1,6 @@
1
1
  -- Compiled with roblox-ts v3.0.0
2
2
  local DropRequest = newproxy()
3
+ -- TODO: middlewares upon received message
3
4
  local MiddlewareProvider
4
5
  do
5
6
  MiddlewareProvider = setmetatable({}, {
@@ -17,6 +18,26 @@ do
17
18
  self.serverGlobalMiddlewares = {}
18
19
  self.clientMiddlewares = {}
19
20
  self.serverMiddlewares = {}
21
+ self.requestDroppedCallbacks = {}
22
+ end
23
+ function MiddlewareProvider:onRequestDropped(callback)
24
+ local _requestDroppedCallbacks = self.requestDroppedCallbacks
25
+ local _callback = callback
26
+ _requestDroppedCallbacks[_callback] = true
27
+ return function()
28
+ local _requestDroppedCallbacks_1 = self.requestDroppedCallbacks
29
+ local _callback_1 = callback
30
+ -- ▼ Set.delete ▼
31
+ local _valueExisted = _requestDroppedCallbacks_1[_callback_1] ~= nil
32
+ _requestDroppedCallbacks_1[_callback_1] = nil
33
+ -- ▲ Set.delete ▲
34
+ return _valueExisted
35
+ end
36
+ end
37
+ function MiddlewareProvider:notifyRequestDropped(message, reason)
38
+ for callback in self.requestDroppedCallbacks do
39
+ callback(message, reason)
40
+ end
20
41
  end
21
42
  function MiddlewareProvider:getClient(message)
22
43
  if self.clientMiddlewares[message] == nil then
@@ -77,10 +98,8 @@ do
77
98
  -- ▼ ReadonlyArray.map ▼
78
99
  local _newValue = table.create(#_exp)
79
100
  local _callback = function(middleware)
80
- return function(message)
81
- return function(_, ctx)
82
- return middleware(message)(ctx)
83
- end
101
+ return function(_, ctx)
102
+ return middleware(ctx)
84
103
  end
85
104
  end
86
105
  for _k, _v in _exp do
@@ -89,8 +108,7 @@ do
89
108
  -- ▲ ReadonlyArray.map ▲
90
109
  local client = _newValue
91
110
  self:useServer(message, server, order)
92
- self:useClient(message, client, order)
93
- return self
111
+ return self:useClient(message, client, order)
94
112
  end
95
113
  function MiddlewareProvider:useClientGlobal(middlewares, order)
96
114
  local globalMiddleware = self:getClientGlobal()
@@ -132,10 +150,8 @@ do
132
150
  -- ▼ ReadonlyArray.map ▼
133
151
  local _newValue = table.create(#_exp)
134
152
  local _callback = function(middleware)
135
- return function(message)
136
- return function(_, ctx)
137
- return middleware(message)(ctx)
138
- end
153
+ return function(_, ctx)
154
+ return middleware(ctx)
139
155
  end
140
156
  end
141
157
  for _k, _v in _exp do
@@ -144,8 +160,7 @@ do
144
160
  -- ▲ ReadonlyArray.map ▲
145
161
  local client = _newValue
146
162
  self:useClientGlobal(client, order)
147
- self:useServerGlobal(middlewares, order)
148
- return self
163
+ return self:useServerGlobal(middlewares, order)
149
164
  end
150
165
  end
151
166
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rbxts/tether",
3
- "version": "1.2.7",
3
+ "version": "1.2.9",
4
4
  "main": "out/init.lua",
5
5
  "scripts": {
6
6
  "build": "rbxtsc",
@@ -42,6 +42,7 @@
42
42
  "@rbxts/destroyable": "^1.0.1",
43
43
  "@rbxts/flamework-binary-serializer": "^0.6.0",
44
44
  "@rbxts/flamework-meta-utils": "^1.0.4",
45
+ "@rbxts/object-utils": "^1.0.4",
45
46
  "@rbxts/repr": "^1.0.1",
46
47
  "@rbxts/services": "^1.5.5",
47
48
  "ts-toolbelt": "^9.6.0"