@rbxts/tether 1.2.7 → 1.2.8

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,18 +2,26 @@ 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;
9
14
  private readonly serverCallbacks;
10
15
  private readonly serverFunctions;
16
+ private readonly clientQueue;
17
+ private readonly clientBroadcastQueue;
18
+ private readonly serverQueue;
11
19
  private readonly guards;
12
20
  private readonly serializers;
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,71 +37,61 @@ 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 = {}
39
49
  self.serverCallbacks = {}
40
50
  self.serverFunctions = {}
51
+ self.clientQueue = {}
52
+ self.clientBroadcastQueue = {}
53
+ self.serverQueue = {}
41
54
  self.guards = {}
42
55
  self.serializers = {}
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)
93
86
  end)
94
87
  end,
95
88
  invoke = TS.async(function(message, returnMessage, data, unreliable)
96
89
  if unreliable == nil then
97
90
  unreliable = false
98
91
  end
92
+ if RunService:IsServer() then
93
+ error("[@rbxts/tether]: Cannot invoke server function from server")
94
+ end
99
95
  local _clientFunctions = self.clientFunctions
100
96
  local _returnMessage = returnMessage
101
97
  if not (_clientFunctions[_returnMessage] ~= nil) then
@@ -118,6 +114,9 @@ do
118
114
  return returnValue
119
115
  end),
120
116
  setCallback = function(message, returnMessage, callback)
117
+ if RunService:IsClient() then
118
+ error(noServerListen)
119
+ end
121
120
  return self.server.on(message, function(player, data)
122
121
  local returnValue = callback(player, data)
123
122
  self.client.emit(player, returnMessage, returnValue)
@@ -126,54 +125,34 @@ do
126
125
  }
127
126
  self.client = {
128
127
  on = function(message, callback)
128
+ if RunService:IsServer() then
129
+ error(noClientListen)
130
+ end
129
131
  return self:on(message, callback, self.clientCallbacks)
130
132
  end,
131
133
  once = function(message, callback)
134
+ if RunService:IsServer() then
135
+ error(noClientListen)
136
+ end
132
137
  return self:once(message, callback, self.clientCallbacks)
133
138
  end,
134
139
  emit = function(player, message, data, unreliable)
135
140
  if unreliable == nil then
136
141
  unreliable = false
137
142
  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
143
+ if RunService:IsClient() then
144
+ error("[@rbxts/tether]: Cannot emit message to client from client")
147
145
  end
148
146
  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
147
+ local _binding = self:runClientMiddlewares(message, data)
148
+ local dropRequest = _binding[1]
149
+ local newData = _binding[2]
150
+ if dropRequest then
173
151
  return nil
174
152
  end
175
- local send = if unreliable then self.serverEvents.sendUnreliableClientMessage else self.serverEvents.sendClientMessage
176
- send(player, getPacket())
153
+ local _clientQueue = self.clientQueue
154
+ local _arg0 = { player, message, newData, unreliable }
155
+ table.insert(_clientQueue, _arg0)
177
156
  end)
178
157
  end,
179
158
  emitExcept = function(player, message, data, unreliable)
@@ -210,56 +189,28 @@ do
210
189
  if unreliable == nil then
211
190
  unreliable = false
212
191
  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
192
+ if RunService:IsClient() then
193
+ error("[@rbxts/tether]: Cannot emit message to all clients from client")
222
194
  end
223
195
  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
196
+ local _binding = self:runClientMiddlewares(message, data)
197
+ local dropRequest = _binding[1]
198
+ local newData = _binding[2]
199
+ if dropRequest then
253
200
  return nil
254
201
  end
255
- local send = if unreliable then self.serverEvents.sendUnreliableClientMessage else self.serverEvents.sendClientMessage
256
- send:broadcast(getPacket())
202
+ local _clientBroadcastQueue = self.clientBroadcastQueue
203
+ local _arg0 = { message, newData, unreliable }
204
+ table.insert(_clientBroadcastQueue, _arg0)
257
205
  end)
258
206
  end,
259
207
  invoke = TS.async(function(message, returnMessage, player, data, unreliable)
260
208
  if unreliable == nil then
261
209
  unreliable = false
262
210
  end
211
+ if RunService:IsClient() then
212
+ error("[@rbxts/tether]: Cannot invoke client function from client")
213
+ end
263
214
  local _serverFunctions = self.serverFunctions
264
215
  local _returnMessage = returnMessage
265
216
  if not (_serverFunctions[_returnMessage] ~= nil) then
@@ -282,6 +233,9 @@ do
282
233
  return returnValue
283
234
  end),
284
235
  setCallback = function(message, returnMessage, callback)
236
+ if RunService:IsServer() then
237
+ error(noClientListen)
238
+ end
285
239
  return self.client.on(message, function(data)
286
240
  local returnValue = callback(data)
287
241
  self.server.emit(returnMessage, returnValue)
@@ -347,8 +301,8 @@ do
347
301
  })
348
302
  end
349
303
  end
350
- function MessageEmitter:create(meta)
351
- local emitter = MessageEmitter.new()
304
+ function MessageEmitter:create(options, meta)
305
+ local emitter = MessageEmitter.new(Object.assign({}, options, defaultMesssageEmitterOptions))
352
306
  if meta == nil then
353
307
  warn(metaGenerationFailed)
354
308
  return emitter:initialize()
@@ -365,16 +319,6 @@ do
365
319
  end
366
320
  return emitter:initialize()
367
321
  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
322
  function MessageEmitter:initialize()
379
323
  if RunService:IsClient() then
380
324
  self.janitor:Add(self.clientEvents.sendClientMessage:connect(function(serializedPacket)
@@ -391,8 +335,138 @@ do
391
335
  return self:onRemoteFire(serializedPacket, player)
392
336
  end))
393
337
  end
338
+ local elapsed = 0
339
+ RunService.Heartbeat:Connect(function(dt)
340
+ elapsed += dt
341
+ if elapsed >= self.options.batchRate then
342
+ elapsed -= self.options.batchRate
343
+ self:update()
344
+ end
345
+ end)
394
346
  return self
395
347
  end
348
+ function MessageEmitter:update()
349
+ for _, _binding in self.clientQueue do
350
+ local player = _binding[1]
351
+ local message = _binding[2]
352
+ local data = _binding[3]
353
+ local unreliable = _binding[4]
354
+ local remote = if unreliable then self.serverEvents.sendUnreliableClientMessage else self.serverEvents.sendClientMessage
355
+ remote(player, self:getPacket(message, data))
356
+ end
357
+ table.clear(self.clientQueue)
358
+ for _, _binding in self.clientBroadcastQueue do
359
+ local message = _binding[1]
360
+ local data = _binding[2]
361
+ local unreliable = _binding[3]
362
+ local remote = if unreliable then self.serverEvents.sendUnreliableClientMessage else self.serverEvents.sendClientMessage
363
+ remote:broadcast(self:getPacket(message, data))
364
+ end
365
+ table.clear(self.clientBroadcastQueue)
366
+ for _, _binding in self.serverQueue do
367
+ local message = _binding[1]
368
+ local data = _binding[2]
369
+ local unreliable = _binding[3]
370
+ local remote = if unreliable then self.clientEvents.sendUnreliableServerMessage else self.clientEvents.sendServerMessage
371
+ remote(self:getPacket(message, data))
372
+ end
373
+ table.clear(self.serverQueue)
374
+ end
375
+ function MessageEmitter:runClientMiddlewares(message, data, player)
376
+ if not self:validateData(message, data) then
377
+ return { true, data }
378
+ end
379
+ local players = player or Players:GetPlayers()
380
+ local ctx = {
381
+ message = message,
382
+ data = data,
383
+ updateData = function(newData)
384
+ data = newData
385
+ return nil
386
+ end,
387
+ getRawData = function()
388
+ return self:getPacket(message, data)
389
+ end,
390
+ }
391
+ for _, globalMiddleware in self.middleware:getClientGlobal() do
392
+ local result = globalMiddleware(players, ctx)
393
+ if not self:validateData(message, data, "Invalid data after global client middleware") then
394
+ return { false, data }
395
+ end
396
+ if result == DropRequest then
397
+ self.middleware:notifyRequestDropped(message, "Global client middleware")
398
+ return { true, data }
399
+ end
400
+ end
401
+ for _, middleware in self.middleware:getClient(message) do
402
+ local result = middleware(players, ctx)
403
+ if not self:validateData(message, data, "Invalid data after client middleware") then
404
+ return { false, data }
405
+ end
406
+ if result == DropRequest then
407
+ self.middleware:notifyRequestDropped(message, "Client middleware")
408
+ return { true, data }
409
+ end
410
+ end
411
+ if not self:validateData(message, data) then
412
+ return { true, data }
413
+ end
414
+ return { false, data }
415
+ end
416
+ function MessageEmitter:runServerMiddlewares(message, data)
417
+ if not self:validateData(message, data) then
418
+ return { true, data }
419
+ end
420
+ local ctx = {
421
+ message = message,
422
+ data = data,
423
+ updateData = function(newData)
424
+ data = newData
425
+ return nil
426
+ end,
427
+ getRawData = function()
428
+ return self:getPacket(message, data)
429
+ end,
430
+ }
431
+ for _, globalMiddleware in self.middleware:getServerGlobal() do
432
+ if not self:validateData(message, data, "Invalid data after global server middleware") then
433
+ return { false, data }
434
+ end
435
+ local result = globalMiddleware(ctx)
436
+ if result == DropRequest then
437
+ self.middleware:notifyRequestDropped(message, "Global server middleware")
438
+ return { true, data }
439
+ end
440
+ end
441
+ for _, middleware in self.middleware:getServer(message) do
442
+ if not self:validateData(message, data, "Invalid data after server middleware") then
443
+ return { false, data }
444
+ end
445
+ local result = middleware(ctx)
446
+ if result == DropRequest then
447
+ self.middleware:notifyRequestDropped(message, "Server middleware")
448
+ return { true, data }
449
+ end
450
+ end
451
+ if not self:validateData(message, data) then
452
+ return { true, data }
453
+ end
454
+ return { false, data }
455
+ end
456
+ function MessageEmitter:validateData(message, data, requestDropReason)
457
+ if requestDropReason == nil then
458
+ requestDropReason = "Invalid data"
459
+ end
460
+ local _guards = self.guards
461
+ local _message = message
462
+ local guard = _guards[_message]
463
+ local guardPassed = guard(data)
464
+ if not guardPassed then
465
+ warn(guardFailed(message, data))
466
+ self.middleware:notifyRequestDropped(message, requestDropReason)
467
+ end
468
+ return guardPassed
469
+ end
396
470
  function MessageEmitter:onRemoteFire(serializedPacket, player)
397
471
  if buffer.len(serializedPacket.messageBuffer) > 1 then
398
472
  return warn("[@rbxts/tether]: Rejected message because message buffer was larger than one byte")
@@ -409,12 +483,7 @@ do
409
483
  if functions == nil then
410
484
  return nil
411
485
  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
486
+ local packet = self:deserializeAndValidate(message, serializedPacket)
418
487
  for callback in functions do
419
488
  callback(packet)
420
489
  end
@@ -427,12 +496,7 @@ do
427
496
  if callbacks == nil then
428
497
  return nil
429
498
  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
499
+ local packet = self:deserializeAndValidate(message, serializedPacket)
436
500
  for callback in callbacks do
437
501
  if isServer then
438
502
  callback(player, packet)
@@ -441,6 +505,16 @@ do
441
505
  end
442
506
  end
443
507
  end
508
+ function MessageEmitter:deserializeAndValidate(message, serializedPacket)
509
+ local serializer = self:getSerializer(message)
510
+ local _packet = serializer
511
+ if _packet ~= nil then
512
+ _packet = _packet.deserialize(serializedPacket.buffer, serializedPacket.blobs)
513
+ end
514
+ local packet = _packet
515
+ self:validateData(message, packet)
516
+ return packet
517
+ end
444
518
  function MessageEmitter:once(message, callback, callbacksMap)
445
519
  local destructor
446
520
  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.8",
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"