@rbxts/tether 1.1.6 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # Tether
2
- A message-based networking solution for Roblox with automatic binary serialization.
2
+ A message-based networking solution for Roblox with automatic binary serialization and type validation.
3
3
 
4
4
  > [!CAUTION]
5
5
  > Depends on `rbxts-transformer-flamework`!
@@ -14,7 +14,8 @@ import { MessageEmitter } from "@rbxts/tether";
14
14
  export const messaging = MessageEmitter.create<MessageData>();
15
15
 
16
16
  export const enum Message {
17
- Test
17
+ Test,
18
+ Packed
18
19
  }
19
20
 
20
21
  export interface MessageData {
@@ -22,6 +23,16 @@ export interface MessageData {
22
23
  readonly foo: string;
23
24
  readonly n: DataType.u8;
24
25
  };
26
+ [Message.Packed]: DataType.Packed<{
27
+ boolean1: boolean;
28
+ boolean2: boolean;
29
+ boolean3: boolean;
30
+ boolean4: boolean;
31
+ boolean5: boolean;
32
+ boolean6: boolean;
33
+ boolean7: boolean;
34
+ boolean8: boolean;
35
+ }>;
25
36
  }
26
37
  ```
27
38
 
@@ -32,7 +43,7 @@ export interface MessageData {
32
43
  ```ts
33
44
  import { Message, messaging } from "shared/messaging";
34
45
 
35
- messaging.onServerMessage(Message.Test, (player, data) => {
46
+ messaging.server.on(Message.Test, (player, data) => {
36
47
  print(player, "sent data:", data);
37
48
  });
38
49
  ```
@@ -41,12 +52,59 @@ messaging.onServerMessage(Message.Test, (player, data) => {
41
52
  ```ts
42
53
  import { Message, messaging } from "shared/messaging";
43
54
 
44
- messaging.emitServer(Message.Test, {
55
+ messaging.server.emit(Message.Test, {
45
56
  foo: "bar",
46
57
  n: 69
47
58
  });
48
59
  ```
49
60
 
61
+ ## Simulated Remote Functions
62
+ 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.
63
+
64
+ 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).
65
+
66
+ ### In `shared/messaging.ts`
67
+ ```ts
68
+ import type { DataType } from "@rbxts/flamework-binary-serializer";
69
+ import { MessageEmitter } from "@rbxts/tether";
70
+
71
+ export const messaging = MessageEmitter.create<MessageData>();
72
+
73
+ export const enum Message {
74
+ Increment,
75
+ IncrementReturn
76
+ }
77
+
78
+ export interface MessageData {
79
+ [Message.Increment]: DataType.u8;
80
+ [Message.IncrementReturn]: DataType.u8;
81
+ }
82
+ ```
83
+
84
+ ### Server
85
+ ```ts
86
+ import { Message, messaging } from "shared/messaging";
87
+
88
+ messaging.server.setCallback(Message.Increment, Message.IncrementReturn, (_, n) => n + 1);
89
+ ```
90
+
91
+ ### Client
92
+ ```ts
93
+ import { Message, messaging } from "shared/messaging";
94
+
95
+ messaging.server
96
+ .invoke(Message.Increment, Message.IncrementReturn, 69)
97
+ .then(print); // 70 - incremented by the server
98
+
99
+ // or use await style
100
+ async function main(): Promise<void> {
101
+ const value = await messaging.server.invoke(Message.Increment, Message.IncrementReturn, 69);
102
+ print(value) // 70
103
+ }
104
+
105
+ main();
106
+ ```
107
+
50
108
  ## Middleware
51
109
  Drop, delay, or modify requests
52
110
 
@@ -107,19 +165,18 @@ export const messaging = MessageEmitter.create<MessageData>();
107
165
  messaging.middleware
108
166
  // only allows requests to the server every 5 seconds,
109
167
  // drops any requests that occur within 5 seconds of each other
110
- .useServer(Message.Test, [BuiltinMiddlewares.rateLimit(5)])
111
- // automatically validates that the data sent through the remote matches
112
- // the data associated with the message at runtime using type guards
113
- .useServer(Message.Test, [BuiltinMiddlewares.validate()])
168
+ .useServer(Message.Test, BuiltinMiddlewares.rateLimit(5))
169
+ .useShared(Message.Packed, () => (_, __, getRawData) => print("Packed object size:", buffer.len(getRawData()))); // will be just one byte!
114
170
  // logs every message fired
115
- .useServerGlobal([logServer()])
116
- .useClientGlobal([logClient()])
117
- .useSharedGlobal([BuiltinMiddlewares.debug()]); // verbosely logs every packet sent
118
- .useServer(Message.Test, [incrementNumberData()]) // error! - data for Message.Test is not a number
119
- .useServerGlobal([incrementNumberData()]); // error! - global data type is always 'unknown', we cannot guarantee a number
171
+ .useServerGlobal(logServer())
172
+ .useClientGlobal(logClient())
173
+ .useSharedGlobal(BuiltinMiddlewares.debug()); // verbosely logs every packet sent
174
+ .useServer(Message.Test, incrementNumberData()) // error! - data for Message.Test is not a number
175
+ .useServerGlobal(incrementNumberData()); // error! - global data type is always 'unknown', we cannot guarantee a number
120
176
 
121
177
  export const enum Message {
122
- Test
178
+ Test,
179
+ Packed
123
180
  }
124
181
 
125
182
  export interface MessageData {
@@ -127,5 +184,15 @@ export interface MessageData {
127
184
  readonly foo: string;
128
185
  readonly n: DataType.u8;
129
186
  };
187
+ [Message.Packed]: DataType.Packed<{
188
+ boolean1: boolean;
189
+ boolean2: boolean;
190
+ boolean3: boolean;
191
+ boolean4: boolean;
192
+ boolean5: boolean;
193
+ boolean6: boolean;
194
+ boolean7: boolean;
195
+ boolean8: boolean;
196
+ }>;
130
197
  }
131
198
  ```
@@ -1,7 +1,16 @@
1
1
  import { Modding } from "@flamework/core";
2
+ import type { SerializerMetadata } from "@rbxts/flamework-binary-serializer";
2
3
  import { type SharedMiddleware } from "./middleware";
3
- type Guard<T> = (value: unknown) => value is T;
4
+ import type { TetherPacket } from "./structs";
5
+ import { Any } from "ts-toolbelt";
4
6
  export declare namespace BuiltinMiddlewares {
7
+ /**
8
+ * Creates a shared middleware that will simulate a ping of the given amount when a message is sent
9
+ *
10
+ * @param pingInMs The amount of time in milliseconds that the middleware should wait
11
+ * @returns A shared middleware that will simulate a ping
12
+ */
13
+ function simulatePing(pingInMs: number): SharedMiddleware;
5
14
  /**
6
15
  * Creates a shared middleware that will drop any message that occurs within the given interval of the previous message
7
16
  *
@@ -10,15 +19,17 @@ export declare namespace BuiltinMiddlewares {
10
19
  */
11
20
  function rateLimit(interval: number): SharedMiddleware;
12
21
  /**
13
- * Creates a shared middleware that validates the data with the given guard (or a generated guard if none was provided)
14
- *
15
- * If the guard fails, the middleware will drop the message
22
+ * Creates a shared middleware that will log a message whenever a message is sent, containing the following information:
23
+ * - The message kind
24
+ * - The data associated with the message
25
+ * - The raw data (buffer and blobs) associated with the message
26
+ * - The size of the packet (in bytes)
27
+ * - The size of the buffer (in bytes)
28
+ * - The size of the blobs (in bytes)
29
+ * - The schema string associated with the message (if it can be deduced)
16
30
  *
17
- * @param guard The guard to use to validate the data.
18
- * @returns A shared middleware that validates the data with the given guard.
31
+ * @returns A shared middleware that will log a message whenever a message is sent.
19
32
  * @metadata macro
20
33
  */
21
- function validate<T>(guard?: Guard<T> | Modding.Generic<T, "guard">): SharedMiddleware;
22
- function debug(): SharedMiddleware;
34
+ function debug<T>(schema?: Modding.Many<Any.Equals<T, unknown> extends 1 ? undefined : SerializerMetadata<TetherPacket<T>>>): SharedMiddleware<T>;
23
35
  }
24
- export {};
@@ -1,20 +1,29 @@
1
1
  -- Compiled with roblox-ts v3.0.0
2
2
  local TS = _G[script]
3
- local DropRequest = TS.import(script, script.Parent, "middleware").DropRequest
4
3
  local RunService = TS.import(script, TS.getModule(script, "@rbxts", "services")).RunService
5
- local noOp = function()
6
- return function() end
7
- end
8
- local validationGuardGenerationFailed = function()
9
- return `[@rbxts/tether]: Failed to generate guard for validate<T>() builtin middleware - skipping validation`
10
- end
11
- local guardFailed = function(message)
12
- return `[@rbxts/tether]: Type validation guard failed for message '{tostring(message)}' - check your sent data`
13
- end
4
+ local repr = TS.import(script, TS.getModule(script, "@rbxts", "repr").out)
5
+ local DropRequest = TS.import(script, script.Parent, "middleware").DropRequest
14
6
  local BLOB_SIZE = 5
15
7
  local BuiltinMiddlewares = {}
16
8
  do
17
9
  local _container = BuiltinMiddlewares
10
+ --[[
11
+ *
12
+ * Creates a shared middleware that will simulate a ping of the given amount when a message is sent
13
+ *
14
+ * @param pingInMs The amount of time in milliseconds that the middleware should wait
15
+ * @returns A shared middleware that will simulate a ping
16
+
17
+ ]]
18
+ local function simulatePing(pingInMs)
19
+ return function()
20
+ return function()
21
+ task.wait(pingInMs / 1000)
22
+ return nil
23
+ end
24
+ end
25
+ end
26
+ _container.simulatePing = simulatePing
18
27
  --[[
19
28
  *
20
29
  * Creates a shared middleware that will drop any message that occurs within the given interval of the previous message
@@ -35,34 +44,7 @@ do
35
44
  end
36
45
  end
37
46
  _container.rateLimit = rateLimit
38
- --[[
39
- *
40
- * Creates a shared middleware that validates the data with the given guard (or a generated guard if none was provided)
41
- *
42
- * If the guard fails, the middleware will drop the message
43
- *
44
- * @param guard The guard to use to validate the data.
45
- * @returns A shared middleware that validates the data with the given guard.
46
- * @metadata macro
47
-
48
- ]]
49
- local function validate(guard)
50
- if guard == nil then
51
- warn(validationGuardGenerationFailed())
52
- return noOp
53
- end
54
- return function(message)
55
- return function(data)
56
- if guard(data) then
57
- return nil
58
- end
59
- warn(guardFailed(message))
60
- return DropRequest
61
- end
62
- end
63
- end
64
- _container.validate = validate
65
- local function toStringBuffer(buf)
47
+ local function bufferToString(buf)
66
48
  local s = { "{ " }
67
49
  do
68
50
  local i = 0
@@ -87,24 +69,42 @@ do
87
69
  table.insert(s, " }")
88
70
  return table.concat(s, "")
89
71
  end
90
- local line = "------------------------------------"
91
- local function debug()
72
+ local horizontalLine = "------------------------------------"
73
+ --[[
74
+ *
75
+ * Creates a shared middleware that will log a message whenever a message is sent, containing the following information:
76
+ * - The message kind
77
+ * - The data associated with the message
78
+ * - The raw data (buffer and blobs) associated with the message
79
+ * - The size of the packet (in bytes)
80
+ * - The size of the buffer (in bytes)
81
+ * - The size of the blobs (in bytes)
82
+ * - The schema string associated with the message (if it can be deduced)
83
+ *
84
+ * @returns A shared middleware that will log a message whenever a message is sent.
85
+ * @metadata macro
86
+
87
+ ]]
88
+ local function debug(schema)
92
89
  return function(message)
93
90
  return function(data, _, getRawData)
94
91
  local rawData = getRawData()
95
92
  local bufferSize = buffer.len(rawData.buffer)
96
93
  local blobsSize = #rawData.blobs * BLOB_SIZE
97
- print(line)
98
- print("Packet sent to", (if RunService:IsServer() then "client" else "server") .. "!")
99
- print(" - Message:", message)
100
- print(" - Data:", data)
101
- print(" - Raw data:")
102
- print(" - Buffer:", toStringBuffer(rawData.buffer))
103
- print(" - Blobs:", rawData.blobs)
104
- print(" - Packet size:", bufferSize + blobsSize, "bytes")
105
- print(" - Buffer:", bufferSize, "bytes")
106
- print(" - Blobs:", blobsSize, "bytes")
107
- print(line)
94
+ local schemaString = if schema ~= nil then " " .. table.concat(string.split(repr(schema[2], {
95
+ pretty = true,
96
+ }), "\n"), "\n ") else "unknown"
97
+ local text = { "\n", horizontalLine, "\n", "Packet sent to ", (if RunService:IsServer() then "client" else "server"), "!\n", " - Message: ", message, "\n", " - Data: ", if data == nil then "undefined" else data, "\n", " - Raw data:\n", " - Buffer: ", bufferToString(rawData.buffer), "\n", " - Blobs: ", repr(rawData.blobs, {
98
+ pretty = false,
99
+ robloxClassName = true,
100
+ }), "\n", " - Packet size: ", bufferSize + blobsSize, " bytes\n", " - Buffer: ", bufferSize, " bytes\n", " - Blobs: ", blobsSize, " bytes\n", " - Schema: ", schemaString, "\n", horizontalLine, "\n" }
101
+ -- ReadonlyArray.join
102
+ local _result = table.create(#text)
103
+ for _k, _v in text do
104
+ _result[_k] = tostring(_v)
105
+ end
106
+ -- ▲ ReadonlyArray.join ▲
107
+ print(table.concat(_result, ""))
108
108
  end
109
109
  end
110
110
  end
@@ -1,54 +1,103 @@
1
1
  import { Modding } from "@flamework/core";
2
- import { type SerializerMetadata } from "@rbxts/flamework-binary-serializer";
3
2
  import Destroyable from "@rbxts/destroyable";
4
3
  import { MiddlewareProvider } from "./middleware";
5
- import type { TetherPacket, ClientMessageCallback, ServerMessageCallback, BaseMessage } from "./structs";
4
+ import type { ClientMessageCallback, ServerMessageCallback, BaseMessage, MessageEmitterMetadata, ClientMessageFunctionCallback, ServerMessageFunctionCallback } from "./structs";
6
5
  export declare class MessageEmitter<MessageData> extends Destroyable {
7
6
  readonly middleware: MiddlewareProvider<MessageData>;
8
7
  private readonly clientCallbacks;
8
+ private readonly clientFunctions;
9
9
  private readonly serverCallbacks;
10
+ private readonly serverFunctions;
11
+ private readonly guards;
10
12
  private serializers;
11
13
  private serverEvents;
12
14
  private clientEvents;
13
15
  /** @metadata macro */
14
- static create<MessageData>(metaForEachMessage?: Modding.Many<{
15
- [Kind in keyof MessageData]: MessageData[Kind] extends undefined ? undefined : Modding.Many<SerializerMetadata<TetherPacket<MessageData[Kind]>>>;
16
- }>): MessageEmitter<MessageData>;
16
+ static create<MessageData>(meta?: Modding.Many<MessageEmitterMetadata<MessageData>>): MessageEmitter<MessageData>;
17
17
  private constructor();
18
- /**.
19
- * @returns A destructor function that disconnects the callback from the message
20
- */
21
- onServerMessage<Kind extends keyof MessageData>(message: Kind & BaseMessage, callback: ServerMessageCallback<MessageData[Kind]>): () => void;
22
- /**.
23
- * @returns A destructor function that disconnects the callback from the message
24
- */
25
- onClientMessage<Kind extends keyof MessageData>(message: Kind & BaseMessage, callback: ClientMessageCallback<MessageData[Kind]>): () => void;
26
- /**
27
- * Emits a message to all connected clients
28
- *
29
- * @param message The message kind to be sent
30
- * @param data The data associated with the message
31
- * @param unreliable Whether the message should be sent unreliably
32
- */
33
- emitServer<Kind extends keyof MessageData>(message: Kind & BaseMessage, data?: MessageData[Kind], unreliable?: boolean): void;
34
- /**
35
- * Emits a message to a specific client
36
- *
37
- * @param player The player to whom the message is sent
38
- * @param message The message kind to be sent
39
- * @param data The data associated with the message
40
- * @param unreliable Whether the message should be sent unreliably
41
- */
42
- emitClient<Kind extends keyof MessageData>(player: Player | Player[], message: Kind & BaseMessage, data?: MessageData[Kind], unreliable?: boolean): void;
43
- /**
44
- * Emits a message to all connected clients
45
- *
46
- * @param message The message kind to be sent
47
- * @param data The data associated with the message
48
- * @param unreliable Whether the message should be sent unreliably
49
- */
50
- emitAllClients<Kind extends keyof MessageData>(message: Kind & BaseMessage, data?: MessageData[Kind], unreliable?: boolean): void;
18
+ readonly server: {
19
+ /**
20
+ * @returns A destructor function that disconnects the callback from the message
21
+ */
22
+ on: <Kind extends keyof MessageData>(message: Kind & BaseMessage, callback: ServerMessageCallback<MessageData[Kind]>) => () => void;
23
+ /**
24
+ * Disconnects the callback as soon as it is called for the first time
25
+ *
26
+ * @returns A destructor function that disconnects the callback from the message
27
+ */
28
+ once: <Kind extends keyof MessageData>(message: Kind & BaseMessage, callback: ServerMessageCallback<MessageData[Kind]>) => () => void;
29
+ /**
30
+ * Emits a message to the server
31
+ *
32
+ * @param message The message kind to be sent
33
+ * @param data The data associated with the message
34
+ * @param unreliable Whether the message should be sent unreliably
35
+ */
36
+ emit: <Kind extends keyof MessageData>(message: Kind & BaseMessage, data?: MessageData[Kind], unreliable?: boolean) => void;
37
+ /**
38
+ * Simulates a remote function invocation.
39
+ *
40
+ * @param message The message kind to be sent
41
+ * @param data The data associated with the message
42
+ * @param unreliable Whether the message should be sent unreliably
43
+ */
44
+ invoke: <Kind extends keyof MessageData, ReturnKind extends keyof MessageData>(message: Kind & BaseMessage, returnMessage: ReturnKind & BaseMessage, data?: MessageData[Kind], unreliable?: boolean) => Promise<MessageData[ReturnKind]>;
45
+ /**
46
+ * Sets a callback for a simulated remote function
47
+ *
48
+ * @returns A destructor function that disconnects the callback from the message
49
+ */
50
+ setCallback: <Kind extends keyof MessageData, ReturnKind extends keyof MessageData>(message: Kind & BaseMessage, returnMessage: ReturnKind & BaseMessage, callback: ServerMessageFunctionCallback<MessageData[Kind], MessageData[ReturnKind]>) => () => void;
51
+ };
52
+ readonly client: {
53
+ /**
54
+ * @returns A destructor function that disconnects the callback from the message
55
+ */
56
+ on: <Kind extends keyof MessageData>(message: Kind & BaseMessage, callback: ClientMessageCallback<MessageData[Kind]>) => () => void;
57
+ /**
58
+ * Disconnects the callback as soon as it is called for the first time
59
+ *
60
+ * @returns A destructor function that disconnects the callback from the message
61
+ */
62
+ once: <Kind extends keyof MessageData>(message: Kind & BaseMessage, callback: ClientMessageCallback<MessageData[Kind]>) => () => void;
63
+ /**
64
+ * Emits a message to a specific client
65
+ *
66
+ * @param player The player to whom the message is sent
67
+ * @param message The message kind to be sent
68
+ * @param data The data associated with the message
69
+ * @param unreliable Whether the message should be sent unreliably
70
+ */
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 connected clients
74
+ *
75
+ * @param message The message kind to be sent
76
+ * @param data The data associated with the message
77
+ * @param unreliable Whether the message should be sent unreliably
78
+ */
79
+ emitAll: <Kind extends keyof MessageData>(message: Kind & BaseMessage, data?: MessageData[Kind], unreliable?: boolean) => void;
80
+ /**
81
+ * Simulates a remote function invocation.
82
+ *
83
+ * @param message The message kind to be sent
84
+ * @param data The data associated with the message
85
+ * @param unreliable Whether the message should be sent unreliably
86
+ */
87
+ invoke: <Kind extends keyof MessageData, ReturnKind extends keyof MessageData>(message: Kind & BaseMessage, returnMessage: ReturnKind & BaseMessage, player: Player, data?: MessageData[Kind], unreliable?: boolean) => Promise<MessageData[ReturnKind]>;
88
+ /**
89
+ * Sets a callback for a simulated remote function
90
+ *
91
+ * @returns A destructor function that disconnects the callback from the message
92
+ */
93
+ setCallback: <Kind extends keyof MessageData, ReturnKind extends keyof MessageData>(message: Kind & BaseMessage, returnMessage: ReturnKind & BaseMessage, callback: ClientMessageFunctionCallback<MessageData[Kind], MessageData[ReturnKind]>) => () => void;
94
+ };
95
+ private validateData;
51
96
  private initialize;
97
+ private readMessageFromPacket;
98
+ private executeFunctions;
99
+ private executeEventCallbacks;
100
+ private once;
52
101
  private on;
53
102
  private getPacket;
54
103
  /** @metadata macro */
@@ -10,7 +10,12 @@ local Destroyable = TS.import(script, TS.getModule(script, "@rbxts", "destroyabl
10
10
  local _middleware = TS.import(script, script.Parent, "middleware")
11
11
  local DropRequest = _middleware.DropRequest
12
12
  local MiddlewareProvider = _middleware.MiddlewareProvider
13
- local GlobalEvents = Networking.createEvent("@rbxts/tether:message-emitter@GlobalEvents")
13
+ -- TODO: error when trying to do something like server.emit() from the server
14
+ local remotes = Networking.createEvent("@rbxts/tether:message-emitter@remotes")
15
+ local metaGenerationFailed = "[@rbxts/tether]: Failed to generate message metadata - make sure you are using Flamework macro-friendly types in your schemas"
16
+ local guardFailed = function(message)
17
+ return `[@rbxts/tether]: Type validation guard failed for message '{message}' - check your sent data`
18
+ end
14
19
  local MessageEmitter
15
20
  do
16
21
  local super = Destroyable
@@ -29,8 +34,213 @@ do
29
34
  super.constructor(self)
30
35
  self.middleware = MiddlewareProvider.new()
31
36
  self.clientCallbacks = {}
37
+ self.clientFunctions = {}
32
38
  self.serverCallbacks = {}
39
+ self.serverFunctions = {}
40
+ self.guards = {}
33
41
  self.serializers = {}
42
+ self.server = {
43
+ on = function(message, callback)
44
+ return self:on(message, callback, self.serverCallbacks)
45
+ end,
46
+ once = function(message, callback)
47
+ return self:once(message, callback, self.serverCallbacks)
48
+ end,
49
+ emit = function(message, data, unreliable)
50
+ if unreliable == nil then
51
+ unreliable = false
52
+ end
53
+ local updateData = function(newData)
54
+ data = newData
55
+ return nil
56
+ end
57
+ local getPacket = function()
58
+ return self:getPacket(message, data)
59
+ end
60
+ if not self:validateData(message, data) then
61
+ return nil
62
+ end
63
+ task.spawn(function()
64
+ for _, globalMiddleware in self.middleware:getServerGlobal() do
65
+ if not self:validateData(message, data) then
66
+ return nil
67
+ end
68
+ local result = globalMiddleware(message)(data, updateData, getPacket)
69
+ if result == DropRequest then
70
+ return nil
71
+ end
72
+ end
73
+ for _, middleware in self.middleware:getServer(message) do
74
+ if not self:validateData(message, data) then
75
+ return nil
76
+ end
77
+ local result = middleware(message)(data, updateData, getPacket)
78
+ if result == DropRequest then
79
+ return nil
80
+ end
81
+ end
82
+ if not self:validateData(message, data) then
83
+ return nil
84
+ end
85
+ local send = if unreliable then self.clientEvents.sendUnreliableServerMessage else self.clientEvents.sendServerMessage
86
+ send(getPacket())
87
+ end)
88
+ end,
89
+ invoke = TS.async(function(message, returnMessage, data, unreliable)
90
+ if unreliable == nil then
91
+ unreliable = false
92
+ end
93
+ local _clientFunctions = self.clientFunctions
94
+ local _returnMessage = returnMessage
95
+ if not (_clientFunctions[_returnMessage] ~= nil) then
96
+ local _clientFunctions_1 = self.clientFunctions
97
+ local _returnMessage_1 = returnMessage
98
+ _clientFunctions_1[_returnMessage_1] = {}
99
+ end
100
+ local _clientFunctions_1 = self.clientFunctions
101
+ local _returnMessage_1 = returnMessage
102
+ local functions = _clientFunctions_1[_returnMessage_1]
103
+ local returnValue
104
+ functions[function(data)
105
+ returnValue = data
106
+ return returnValue
107
+ end] = true
108
+ self.server.emit(message, data, unreliable)
109
+ while returnValue == nil do
110
+ RunService.Heartbeat:Wait()
111
+ end
112
+ return returnValue
113
+ end),
114
+ setCallback = function(message, returnMessage, callback)
115
+ return self.server.on(message, function(player, data)
116
+ local returnValue = callback(player, data)
117
+ self.client.emit(player, returnMessage, returnValue)
118
+ end)
119
+ end,
120
+ }
121
+ self.client = {
122
+ on = function(message, callback)
123
+ return self:on(message, callback, self.clientCallbacks)
124
+ end,
125
+ once = function(message, callback)
126
+ return self:once(message, callback, self.clientCallbacks)
127
+ end,
128
+ emit = function(player, message, data, unreliable)
129
+ if unreliable == nil then
130
+ unreliable = false
131
+ end
132
+ local updateData = function(newData)
133
+ data = newData
134
+ return nil
135
+ end
136
+ local getPacket = function()
137
+ return self:getPacket(message, data)
138
+ end
139
+ if not self:validateData(message, data) then
140
+ return nil
141
+ end
142
+ task.spawn(function()
143
+ for _, globalMiddleware in self.middleware:getClientGlobal() do
144
+ if not self:validateData(message, data) then
145
+ return nil
146
+ end
147
+ local result = globalMiddleware(message)(player, data, updateData, getPacket)
148
+ if result == DropRequest then
149
+ return nil
150
+ end
151
+ end
152
+ for _, middleware in self.middleware:getClient(message) do
153
+ if not self:validateData(message, data) then
154
+ return nil
155
+ end
156
+ local result = middleware(message)(player, data, updateData, getPacket)
157
+ if result == DropRequest then
158
+ return nil
159
+ end
160
+ end
161
+ if not self:validateData(message, data) then
162
+ return nil
163
+ end
164
+ local send = if unreliable then self.serverEvents.sendUnreliableClientMessage else self.serverEvents.sendClientMessage
165
+ send(player, getPacket())
166
+ end)
167
+ end,
168
+ emitAll = function(message, data, unreliable)
169
+ if unreliable == nil then
170
+ unreliable = false
171
+ end
172
+ local updateData = function(newData)
173
+ data = newData
174
+ return nil
175
+ end
176
+ local getPacket = function()
177
+ return self:getPacket(message, data)
178
+ end
179
+ if not self:validateData(message, data) then
180
+ return nil
181
+ end
182
+ task.spawn(function()
183
+ for _, globalMiddleware in self.middleware:getClientGlobal() do
184
+ for _1, player in Players:GetPlayers() do
185
+ if not self:validateData(message, data) then
186
+ return nil
187
+ end
188
+ local result = globalMiddleware(message)(player, data, updateData, getPacket)
189
+ if result == DropRequest then
190
+ return nil
191
+ end
192
+ end
193
+ end
194
+ for _, middleware in self.middleware:getClient(message) do
195
+ for _1, player in Players:GetPlayers() do
196
+ if not self:validateData(message, data) then
197
+ return nil
198
+ end
199
+ local result = middleware(message)(player, data, updateData, getPacket)
200
+ if result == DropRequest then
201
+ return nil
202
+ end
203
+ end
204
+ end
205
+ if not self:validateData(message, data) then
206
+ return nil
207
+ end
208
+ local send = if unreliable then self.serverEvents.sendUnreliableClientMessage else self.serverEvents.sendClientMessage
209
+ send:broadcast(getPacket())
210
+ end)
211
+ end,
212
+ invoke = TS.async(function(message, returnMessage, player, data, unreliable)
213
+ if unreliable == nil then
214
+ unreliable = false
215
+ end
216
+ local _serverFunctions = self.serverFunctions
217
+ local _returnMessage = returnMessage
218
+ if not (_serverFunctions[_returnMessage] ~= nil) then
219
+ local _serverFunctions_1 = self.serverFunctions
220
+ local _returnMessage_1 = returnMessage
221
+ _serverFunctions_1[_returnMessage_1] = {}
222
+ end
223
+ local _serverFunctions_1 = self.serverFunctions
224
+ local _returnMessage_1 = returnMessage
225
+ local functions = _serverFunctions_1[_returnMessage_1]
226
+ local returnValue
227
+ functions[function(data)
228
+ returnValue = data
229
+ return returnValue
230
+ end] = true
231
+ self.client.emit(player, message, data, unreliable)
232
+ while returnValue == nil do
233
+ RunService.Heartbeat:Wait()
234
+ end
235
+ return returnValue
236
+ end),
237
+ setCallback = function(message, returnMessage, callback)
238
+ return self.client.on(message, function(data)
239
+ local returnValue = callback(data)
240
+ self.server.emit(returnMessage, returnValue)
241
+ end)
242
+ end,
243
+ }
34
244
  self.janitor:Add(function()
35
245
  table.clear(self.clientCallbacks)
36
246
  table.clear(self.serverCallbacks)
@@ -39,7 +249,7 @@ do
39
249
  self.clientEvents = nil
40
250
  end)
41
251
  if RunService:IsServer() then
42
- self.serverEvents = GlobalEvents:createServer({}, {
252
+ self.serverEvents = remotes:createServer({}, {
43
253
  incomingIds = { "sendServerMessage", "sendUnreliableServerMessage" },
44
254
  incoming = {
45
255
  sendServerMessage = { { t.interface({
@@ -62,7 +272,7 @@ do
62
272
  namespaces = {},
63
273
  })
64
274
  else
65
- self.clientEvents = GlobalEvents:createClient({}, {
275
+ self.clientEvents = remotes:createClient({}, {
66
276
  incomingIds = { "sendClientMessage", "sendUnreliableClientMessage" },
67
277
  incoming = {
68
278
  sendClientMessage = { { t.interface({
@@ -86,180 +296,130 @@ do
86
296
  })
87
297
  end
88
298
  end
89
- function MessageEmitter:create(metaForEachMessage)
299
+ function MessageEmitter:create(meta)
90
300
  local emitter = MessageEmitter.new()
91
- if metaForEachMessage == nil then
92
- warn("[@rbxts/tether]: Failed to generate serializer metadata for MessageEmitter")
301
+ if meta == nil then
302
+ warn(metaGenerationFailed)
93
303
  return emitter:initialize()
94
304
  end
95
- for kind, meta in pairs(metaForEachMessage) do
96
- if meta == nil then
305
+ for kind, _binding in pairs(meta) do
306
+ local guard = _binding.guard
307
+ local serializerMetadata = _binding.serializerMetadata
308
+ local numberKind = tonumber(kind)
309
+ emitter.guards[numberKind] = guard
310
+ if serializerMetadata == nil then
97
311
  continue
98
312
  end
99
- emitter:addSerializer(tonumber(kind), meta)
313
+ emitter:addSerializer(numberKind, serializerMetadata)
100
314
  end
101
315
  return emitter:initialize()
102
316
  end
103
- function MessageEmitter:onServerMessage(message, callback)
104
- return self:on(message, callback)
105
- end
106
- function MessageEmitter:onClientMessage(message, callback)
107
- return self:on(message, callback)
108
- end
109
- function MessageEmitter:emitServer(message, data, unreliable)
110
- if unreliable == nil then
111
- unreliable = false
112
- end
113
- local updateData = function(newData)
114
- data = newData
115
- return nil
116
- end
117
- local getPacket = function()
118
- return self:getPacket(message, data)
119
- end
120
- for _, globalMiddleware in self.middleware:getServerGlobal() do
121
- local result = globalMiddleware(message)(data, updateData, getPacket)
122
- if result == DropRequest then
123
- return nil
124
- end
125
- end
126
- for _, middleware in self.middleware:getServer(message) do
127
- local result = middleware(message)(data, updateData, getPacket)
128
- if result == DropRequest then
129
- return nil
130
- end
317
+ function MessageEmitter:validateData(message, data)
318
+ local _guards = self.guards
319
+ local _message = message
320
+ local guard = _guards[_message]
321
+ local guardPassed = guard(data)
322
+ if not guardPassed then
323
+ warn(guardFailed(message))
131
324
  end
132
- local send = if unreliable then self.clientEvents.sendUnreliableServerMessage else self.clientEvents.sendServerMessage
133
- send(getPacket())
325
+ return guardPassed
134
326
  end
135
- function MessageEmitter:emitClient(player, message, data, unreliable)
136
- if unreliable == nil then
137
- unreliable = false
327
+ function MessageEmitter:initialize()
328
+ if RunService:IsClient() then
329
+ self.janitor:Add(self.clientEvents.sendClientMessage:connect(function(serializedPacket)
330
+ local sentMessage = self:readMessageFromPacket(serializedPacket)
331
+ self:executeEventCallbacks(sentMessage, serializedPacket)
332
+ self:executeFunctions(sentMessage, serializedPacket)
333
+ end))
334
+ else
335
+ self.janitor:Add(self.serverEvents.sendServerMessage:connect(function(player, serializedPacket)
336
+ local sentMessage = self:readMessageFromPacket(serializedPacket)
337
+ self:executeEventCallbacks(sentMessage, serializedPacket, player)
338
+ self:executeFunctions(sentMessage, serializedPacket)
339
+ end))
138
340
  end
139
- local updateData = function(newData)
140
- data = newData
341
+ return self
342
+ end
343
+ function MessageEmitter:readMessageFromPacket(serializedPacket)
344
+ return buffer.readu8(serializedPacket.buffer, 0)
345
+ end
346
+ function MessageEmitter:executeFunctions(message, serializedPacket)
347
+ local isServer = RunService:IsServer()
348
+ local functionsMap = if isServer then self.serverFunctions else self.clientFunctions
349
+ local _message = message
350
+ local functions = functionsMap[_message]
351
+ if functions == nil then
141
352
  return nil
142
353
  end
143
- local getPacket = function()
144
- return self:getPacket(message, data)
145
- end
146
- for _, globalMiddleware in self.middleware:getClientGlobal() do
147
- local result = globalMiddleware(message)(player, data, updateData, getPacket)
148
- if result == DropRequest then
149
- return nil
150
- end
354
+ local serializer = self:getSerializer(message)
355
+ local _packet = serializer
356
+ if _packet ~= nil then
357
+ _packet = _packet.deserialize(serializedPacket.buffer, serializedPacket.blobs)
151
358
  end
152
- for _, middleware in self.middleware:getClient(message) do
153
- local result = middleware(message)(player, data, updateData, getPacket)
154
- if result == DropRequest then
155
- return nil
359
+ local packet = _packet
360
+ for callback in functions do
361
+ local _result = packet
362
+ if _result ~= nil then
363
+ _result = _result.data
156
364
  end
365
+ callback(_result)
157
366
  end
158
- local send = if unreliable then self.serverEvents.sendUnreliableClientMessage else self.serverEvents.sendClientMessage
159
- send(player, getPacket())
160
367
  end
161
- function MessageEmitter:emitAllClients(message, data, unreliable)
162
- if unreliable == nil then
163
- unreliable = false
164
- end
165
- local updateData = function(newData)
166
- data = newData
368
+ function MessageEmitter:executeEventCallbacks(message, serializedPacket, player)
369
+ local isServer = RunService:IsServer()
370
+ local callbacksMap = if isServer then self.serverCallbacks else self.clientCallbacks
371
+ local _message = message
372
+ local callbacks = callbacksMap[_message]
373
+ if callbacks == nil then
167
374
  return nil
168
375
  end
169
- local getPacket = function()
170
- return self:getPacket(message, data)
376
+ local serializer = self:getSerializer(message)
377
+ local _packet = serializer
378
+ if _packet ~= nil then
379
+ _packet = _packet.deserialize(serializedPacket.buffer, serializedPacket.blobs)
171
380
  end
172
- for _, globalMiddleware in self.middleware:getClientGlobal() do
173
- for _1, player in Players:GetPlayers() do
174
- local result = globalMiddleware(message)(player, data, updateData, getPacket)
175
- if result == DropRequest then
176
- return nil
381
+ local packet = _packet
382
+ for callback in callbacks do
383
+ if isServer then
384
+ local _exp = player
385
+ local _result = packet
386
+ if _result ~= nil then
387
+ _result = _result.data
177
388
  end
178
- end
179
- end
180
- for _, middleware in self.middleware:getClient(message) do
181
- for _1, player in Players:GetPlayers() do
182
- local result = middleware(message)(player, data, updateData, getPacket)
183
- if result == DropRequest then
184
- return nil
389
+ callback(_exp, _result)
390
+ else
391
+ local _result = packet
392
+ if _result ~= nil then
393
+ _result = _result.data
185
394
  end
395
+ callback(_result)
186
396
  end
187
397
  end
188
- local send = if unreliable then self.serverEvents.sendUnreliableClientMessage else self.serverEvents.sendClientMessage
189
- send:broadcast(self:getPacket(message, data))
190
398
  end
191
- function MessageEmitter:initialize()
192
- if RunService:IsClient() then
193
- self.janitor:Add(self.clientEvents.sendClientMessage:connect(function(serializedPacket)
194
- local sentMessage = buffer.readu8(serializedPacket.buffer, 0)
195
- local messageCallbacks = self.clientCallbacks[sentMessage] or {}
196
- -- ▼ ReadonlySet.size ▼
197
- local _size = 0
198
- for _ in messageCallbacks do
199
- _size += 1
200
- end
201
- -- ▲ ReadonlySet.size ▲
202
- if _size == 0 then
203
- return nil
204
- end
205
- local serializer = self:getSerializer(sentMessage)
206
- local _packet = serializer
207
- if _packet ~= nil then
208
- _packet = _packet.deserialize(serializedPacket.buffer, serializedPacket.blobs)
209
- end
210
- local packet = _packet
211
- for callback in messageCallbacks do
212
- local _result = packet
213
- if _result ~= nil then
214
- _result = _result.data
215
- end
216
- callback(_result)
217
- end
218
- end))
219
- else
220
- self.janitor:Add(self.serverEvents.sendServerMessage:connect(function(player, serializedPacket)
221
- local sentMessage = buffer.readu8(serializedPacket.buffer, 0)
222
- local messageCallbacks = self.serverCallbacks[sentMessage] or {}
223
- -- ▼ ReadonlySet.size ▼
224
- local _size = 0
225
- for _ in messageCallbacks do
226
- _size += 1
227
- end
228
- -- ▲ ReadonlySet.size ▲
229
- if _size == 0 then
230
- return nil
231
- end
232
- local serializer = self:getSerializer(sentMessage)
233
- local _packet = serializer
234
- if _packet ~= nil then
235
- _packet = _packet.deserialize(serializedPacket.buffer, serializedPacket.blobs)
236
- end
237
- local packet = _packet
238
- for callback in messageCallbacks do
239
- local _exp = player
240
- local _result = packet
241
- if _result ~= nil then
242
- _result = _result.data
243
- end
244
- callback(_exp, _result)
245
- end
246
- end))
247
- end
248
- return self
399
+ function MessageEmitter:once(message, callback, callbacksMap)
400
+ local destructor
401
+ destructor = self:on(message, function(player, data)
402
+ destructor()
403
+ callback(player, data)
404
+ end, callbacksMap)
405
+ return destructor
249
406
  end
250
- function MessageEmitter:on(message, callback)
251
- local callbacksMap = if RunService:IsServer() then self.serverCallbacks else self.clientCallbacks
407
+ function MessageEmitter:on(message, callback, callbacksMap)
408
+ local _callbacksMap = callbacksMap
252
409
  local _message = message
253
- if not (callbacksMap[_message] ~= nil) then
410
+ if not (_callbacksMap[_message] ~= nil) then
411
+ local _callbacksMap_1 = callbacksMap
254
412
  local _message_1 = message
255
- callbacksMap[_message_1] = {}
413
+ _callbacksMap_1[_message_1] = {}
256
414
  end
415
+ local _callbacksMap_1 = callbacksMap
257
416
  local _message_1 = message
258
- local callbacks = callbacksMap[_message_1]
417
+ local callbacks = _callbacksMap_1[_message_1]
259
418
  local _callback = callback
260
419
  callbacks[_callback] = true
420
+ local _callbacksMap_2 = callbacksMap
261
421
  local _message_2 = message
262
- callbacksMap[_message_2] = callbacks
422
+ _callbacksMap_2[_message_2] = callbacks
263
423
  return function()
264
424
  local _callback_1 = callback
265
425
  -- ▼ Set.delete ▼
@@ -4,10 +4,15 @@ type DropRequestSymbol = symbol & {
4
4
  };
5
5
  export declare const DropRequest: DropRequestSymbol;
6
6
  type UpdateDataFn<T> = (newData: T) => void;
7
- export type ClientMiddleware<Data = unknown> = (message: BaseMessage) => (player: Player | Player[], data: Readonly<Data>, updateData: UpdateDataFn<Data>, getRawData: () => SerializedPacket) => DropRequestSymbol | void;
8
- export type ServerMiddleware<Data = unknown> = (message: BaseMessage) => (data: Readonly<Data>, updateData: UpdateDataFn<Data>, getRawData: () => SerializedPacket) => DropRequestSymbol | void;
9
- export type SharedMiddleware = (message: BaseMessage) => (data: unknown, updateData: UpdateDataFn<unknown>, getRawData: () => SerializedPacket) => DropRequestSymbol | void;
10
- export type Middleware<Data = unknown> = ServerMiddleware<Data> & ClientMiddleware<Data>;
7
+ type GetRawDataFn = () => SerializedPacket;
8
+ export type ClientMiddleware<Data = unknown> = {
9
+ _client?: void;
10
+ } & ((message: BaseMessage) => (player: Player | Player[], data: Readonly<Data>, updateData: UpdateDataFn<Data>, getRawData: GetRawDataFn) => DropRequestSymbol | void);
11
+ export type ServerMiddleware<Data = unknown> = SharedMiddleware<Data> & {
12
+ _server?: void;
13
+ };
14
+ export type SharedMiddleware<Data = unknown> = (message: BaseMessage) => (data: Readonly<Data>, updateData: UpdateDataFn<Data>, getRawData: GetRawDataFn) => DropRequestSymbol | void;
15
+ export type Middleware<Data = unknown> = ServerMiddleware<Data> & ClientMiddleware<Data> & SharedMiddleware<Data>;
11
16
  export declare class MiddlewareProvider<MessageData> {
12
17
  private readonly clientGlobalMiddlewares;
13
18
  private readonly serverGlobalMiddlewares;
@@ -21,9 +26,9 @@ export declare class MiddlewareProvider<MessageData> {
21
26
  getClientGlobal<Data>(): ClientMiddleware<Data>[];
22
27
  /** @hidden */
23
28
  getServerGlobal<Data>(): ServerMiddleware<Data>[];
24
- useClient<Kind extends keyof MessageData>(message: Kind & BaseMessage, middlewares: ClientMiddleware<MessageData[Kind]> | readonly ClientMiddleware<MessageData[Kind]>[], order?: number): this;
25
- useServer<Kind extends keyof MessageData>(message: Kind & BaseMessage, middlewares: ServerMiddleware<MessageData[Kind]> | readonly ServerMiddleware<MessageData[Kind]>[], order?: number): this;
26
- useShared<Kind extends keyof MessageData>(message: Kind & BaseMessage, middlewares: SharedMiddleware | readonly SharedMiddleware[], order?: number): this;
29
+ useClient<Kind extends keyof MessageData>(message: Kind & BaseMessage, middlewares: ClientMiddleware<MessageData[Kind]> | readonly ClientMiddleware<MessageData[Kind]>[] | ClientMiddleware | readonly ClientMiddleware[], order?: number): this;
30
+ useServer<Kind extends keyof MessageData>(message: Kind & BaseMessage, middlewares: ServerMiddleware<MessageData[Kind]> | readonly ServerMiddleware<MessageData[Kind]>[] | ServerMiddleware | readonly ServerMiddleware[], order?: number): this;
31
+ useShared<Kind extends keyof MessageData>(message: Kind & BaseMessage, middlewares: SharedMiddleware<MessageData[Kind]> | readonly SharedMiddleware<MessageData[Kind]>[] | SharedMiddleware | readonly SharedMiddleware[], order?: number): this;
27
32
  useClientGlobal(middlewares: ClientMiddleware | readonly ClientMiddleware[], order?: number): this;
28
33
  useServerGlobal(middlewares: ServerMiddleware | readonly ServerMiddleware[], order?: number): this;
29
34
  useSharedGlobal(middlewares: SharedMiddleware | readonly SharedMiddleware[], order?: number): this;
package/out/structs.d.ts CHANGED
@@ -1,8 +1,11 @@
1
+ import { Modding } from "@flamework/core";
1
2
  import type { Networking } from "@flamework/networking";
2
- import type { DataType } from "@rbxts/flamework-binary-serializer";
3
+ import type { DataType, SerializerMetadata } from "@rbxts/flamework-binary-serializer";
3
4
  export type MessageCallback<T = unknown> = ServerMessageCallback<T> | ClientMessageCallback<T>;
4
5
  export type ClientMessageCallback<T = unknown> = (data: T) => void;
6
+ export type ClientMessageFunctionCallback<T = unknown, R = unknown> = (data: T) => R;
5
7
  export type ServerMessageCallback<T = unknown> = (player: Player, data: T) => void;
8
+ export type ServerMessageFunctionCallback<T = unknown, R = unknown> = (player: Player, data: T) => R;
6
9
  export type BaseMessage = number;
7
10
  export interface SerializedPacket {
8
11
  readonly buffer: buffer;
@@ -22,3 +25,11 @@ export interface ClientEvents {
22
25
  sendClientMessage: MessageEvent;
23
26
  sendUnreliableClientMessage: UnreliableMessageEvent;
24
27
  }
28
+ export interface MessageMetadata<MessageData, Kind extends keyof MessageData> {
29
+ readonly guard: Modding.Generic<MessageData[Kind], "guard">;
30
+ readonly serializerMetadata: MessageData[Kind] extends undefined ? undefined : Modding.Many<SerializerMetadata<TetherPacket<MessageData[Kind]>>>;
31
+ }
32
+ export type Guard<T = unknown> = (value: unknown) => value is T;
33
+ export type MessageEmitterMetadata<MessageData> = {
34
+ [Kind in keyof MessageData]: MessageMetadata<MessageData, Kind>;
35
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rbxts/tether",
3
- "version": "1.1.6",
3
+ "version": "1.2.0",
4
4
  "main": "out/init.lua",
5
5
  "scripts": {
6
6
  "build": "rbxtsc",
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "author": "runicly",
22
22
  "license": "ISC",
23
- "description": "A message-based networking solution for Roblox with automatic binary serialization",
23
+ "description": "A message-based networking solution for Roblox with automatic binary serialization and type validation",
24
24
  "types": "out/index.d.ts",
25
25
  "files": [
26
26
  "out",
@@ -43,6 +43,7 @@
43
43
  "@rbxts/destroyable": "^1.0.1",
44
44
  "@rbxts/flamework-binary-serializer": "^0.6.0",
45
45
  "@rbxts/flamework-meta-utils": "^1.0.4",
46
+ "@rbxts/repr": "^1.0.1",
46
47
  "@rbxts/services": "^1.5.5"
47
48
  }
48
49
  }