@flamework-experimental/networking 2.0.0-alpha.1 → 2.0.0-alpha.2
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 +6 -0
- package/flamework.build +1 -1
- package/out/event/createEvent.d.ts +22 -4
- package/out/event/createEvent.luau +47 -49
- package/out/events/createClientMethod.luau +7 -3
- package/out/events/createGenericHandler.luau +6 -3
- package/out/events/createNetworkingEvent.luau +7 -5
- package/out/events/createServerMethod.d.ts +1 -1
- package/out/events/createServerMethod.luau +8 -3
- package/out/events/types.d.ts +4 -3
- package/out/function/createFunctionReceiver.d.ts +10 -9
- package/out/function/createFunctionReceiver.luau +53 -55
- package/out/function/createFunctionSender.d.ts +3 -2
- package/out/function/createFunctionSender.luau +118 -40
- package/out/functions/createClientMethod.d.ts +1 -1
- package/out/functions/createClientMethod.luau +9 -8
- package/out/functions/createGenericHandler.luau +8 -12
- package/out/functions/createNetworkingFunction.luau +7 -5
- package/out/functions/createServerMethod.d.ts +1 -1
- package/out/functions/createServerMethod.luau +9 -8
- package/out/functions/types.d.ts +10 -12
- package/out/handlers.d.ts +35 -36
- package/out/index.d.ts +6 -0
- package/out/init.luau +6 -0
- package/out/middleware/createGuards.d.ts +10 -0
- package/out/middleware/createGuards.luau +32 -0
- package/out/middleware/processor.d.ts +62 -0
- package/out/middleware/processor.luau +214 -0
- package/out/middleware/types.d.ts +26 -15
- package/out/util/createOnce.d.ts +14 -0
- package/out/util/createOnce.luau +66 -0
- package/out/util/createSignalContainer.d.ts +2 -1
- package/out/util/createSignalContainer.luau +2 -2
- package/out/util/signal.d.ts +27 -0
- package/out/util/signal.luau +135 -0
- package/out/util/trimArguments.d.ts +12 -0
- package/out/util/trimArguments.luau +48 -0
- package/package.json +2 -3
- package/out/middleware/createGuardMiddleware.d.ts +0 -6
- package/out/middleware/createGuardMiddleware.luau +0 -44
- package/out/middleware/createMiddlewareProcessor.d.ts +0 -3
- package/out/middleware/createMiddlewareProcessor.luau +0 -24
- package/out/util/timeoutPromise.d.ts +0 -1
- package/out/util/timeoutPromise.luau +0 -10
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@ bun install
|
|
|
27
27
|
bun run build # builds every package in dependency order
|
|
28
28
|
bun run test # build + transformer tests + runtime specs
|
|
29
29
|
bun run lint
|
|
30
|
+
bun run test:place # the in-place suite in Roblox Studio (tests/place); needs Studio, Rojo and Lune
|
|
30
31
|
```
|
|
31
32
|
|
|
32
33
|
### Packages
|
|
@@ -64,3 +65,8 @@ Two suites, both run by `bun run test`:
|
|
|
64
65
|
|
|
65
66
|
Specs live in [`packages/specs`](packages/specs) and are compiled by `rbxtsc` like any other
|
|
66
67
|
Flamework consumer, so they exercise the transformer and the runtime together.
|
|
68
|
+
|
|
69
|
+
A third suite runs against the real engine and is left out of `bun run test`: the
|
|
70
|
+
[test place](tests/place/README.md), a small game linked to the packages' builds, whose
|
|
71
|
+
`@flamework-experimental/testing` sections `bun run test:place` runs in Roblox Studio, both realms,
|
|
72
|
+
under four Rojo projects (see [Testing in Roblox Studio](docs/testing/studio.md)).
|
package/flamework.build
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Serialization } from "@flamework-experimental/core";
|
|
2
|
-
import { MiddlewareFactory
|
|
2
|
+
import { MiddlewareFactory } from "../middleware/types";
|
|
3
3
|
import { NetworkInfo } from "../types";
|
|
4
|
+
import { Guards } from "../middleware/processor";
|
|
5
|
+
import { SignalConnection } from "../util/signal";
|
|
4
6
|
export interface CreateEventOptions {
|
|
5
7
|
/**
|
|
6
8
|
* The namespace this event should be created in.
|
|
@@ -30,6 +32,11 @@ export interface CreateEventOptions {
|
|
|
30
32
|
* A list of middleware that this event uses when it receives an event.
|
|
31
33
|
*/
|
|
32
34
|
incomingMiddleware?: MiddlewareFactory<any[], void>[];
|
|
35
|
+
/**
|
|
36
|
+
* The generated argument checks, run on what arrives before any middleware. A message that fails
|
|
37
|
+
* them is dropped.
|
|
38
|
+
*/
|
|
39
|
+
incomingGuards?: Guards;
|
|
33
40
|
/**
|
|
34
41
|
* Unpacks the argument list of incoming events. Absent when the project does not enable
|
|
35
42
|
* serialization. Outgoing lists reach `fire*` already packed: the transformer generates the
|
|
@@ -46,14 +53,25 @@ export interface EventInterface {
|
|
|
46
53
|
fireServer(...args: unknown[]): void;
|
|
47
54
|
fireClient(player: Player, ...args: unknown[]): void;
|
|
48
55
|
fireAllClients(...args: unknown[]): void;
|
|
49
|
-
connectServer(callback: (player: Player, ...args: unknown[]) => void):
|
|
50
|
-
connectClient(callback: (...args: unknown[]) => void):
|
|
51
|
-
|
|
56
|
+
connectServer(callback: (player: Player, ...args: unknown[]) => void): SignalConnection;
|
|
57
|
+
connectClient(callback: (...args: unknown[]) => void): SignalConnection;
|
|
58
|
+
/**
|
|
59
|
+
* Runs the receiving half as if the remote had delivered `args` (plain values, never a payload), on
|
|
60
|
+
* a recycled thread of its own, so that a middleware that yields does not hold up the caller.
|
|
61
|
+
*/
|
|
62
|
+
predict(player: Player | undefined, ...args: unknown[]): void;
|
|
52
63
|
}
|
|
53
64
|
/**
|
|
54
65
|
* The argument list a remote delivered: `args` as they are without a decoder, otherwise the buffer
|
|
55
66
|
* and blob list unpacked. `undefined` when the payload was malformed, after reporting it through
|
|
56
67
|
* `onMalformed`. Decoding runs under `pcall`: a hostile buffer raises instead of yielding garbage.
|
|
68
|
+
*
|
|
69
|
+
* Either way the list is trimmed after its last value, so that no hop after it loses the values
|
|
70
|
+
* that follow a nil (see `trimArguments`). A decoded list keeps one slot per declared parameter,
|
|
71
|
+
* so an absent trailing optional would otherwise end it in nil.
|
|
72
|
+
*
|
|
73
|
+
* An event's own messages are decoded in the receive pipeline (`processor.luau`); this is for a
|
|
74
|
+
* function's requests and responses, which carry a plain request id ahead of the payload.
|
|
57
75
|
*/
|
|
58
76
|
export declare function decodeArguments(decoder: Serialization.Decoder | undefined, player: Player | undefined, args: unknown[], onMalformed?: (player: Player | undefined, message: string) => void): unknown[] | undefined;
|
|
59
77
|
export declare function createEvent(options: CreateEventOptions): EventInterface;
|
|
@@ -2,77 +2,70 @@
|
|
|
2
2
|
local TS = _G[script]
|
|
3
3
|
local RunService = TS.import(script, TS.getModule(script, "@rbxts", "services")).RunService
|
|
4
4
|
local createRemoteInstance = TS.import(script, script.Parent, "createRemoteInstance").createRemoteInstance
|
|
5
|
-
local
|
|
6
|
-
|
|
7
|
-
local
|
|
5
|
+
local _processor = TS.import(script, script.Parent.Parent, "middleware", "processor")
|
|
6
|
+
local createProcessor = _processor.createProcessor
|
|
7
|
+
local createReceiver = _processor.createReceiver
|
|
8
|
+
local decode = _processor.decode
|
|
9
|
+
local deliverTo = _processor.deliverTo
|
|
10
|
+
local _signal = TS.import(script, script.Parent.Parent, "util", "signal")
|
|
11
|
+
local createSignal = _signal.createSignal
|
|
12
|
+
local spawn = _signal.spawn
|
|
13
|
+
local trimArguments = TS.import(script, script.Parent.Parent, "util", "trimArguments").trimArguments
|
|
8
14
|
--[[
|
|
9
15
|
*
|
|
10
16
|
* The argument list a remote delivered: `args` as they are without a decoder, otherwise the buffer
|
|
11
17
|
* and blob list unpacked. `undefined` when the payload was malformed, after reporting it through
|
|
12
18
|
* `onMalformed`. Decoding runs under `pcall`: a hostile buffer raises instead of yielding garbage.
|
|
19
|
+
*
|
|
20
|
+
* Either way the list is trimmed after its last value, so that no hop after it loses the values
|
|
21
|
+
* that follow a nil (see `trimArguments`). A decoded list keeps one slot per declared parameter,
|
|
22
|
+
* so an absent trailing optional would otherwise end it in nil.
|
|
23
|
+
*
|
|
24
|
+
* An event's own messages are decoded in the receive pipeline (`processor.luau`); this is for a
|
|
25
|
+
* function's requests and responses, which carry a plain request id ahead of the payload.
|
|
13
26
|
|
|
14
27
|
]]
|
|
15
28
|
local function decodeArguments(decoder, player, args, onMalformed)
|
|
16
29
|
if not decoder then
|
|
17
|
-
return args
|
|
30
|
+
return trimArguments(args)
|
|
18
31
|
end
|
|
19
|
-
local
|
|
20
|
-
local payload = _binding[1]
|
|
21
|
-
local blobs = _binding[2]
|
|
22
|
-
if not (type(payload) == "buffer") or (blobs ~= nil and not (type(blobs) == "table")) then
|
|
23
|
-
local _result = onMalformed
|
|
24
|
-
if _result ~= nil then
|
|
25
|
-
_result(player, "payload is not a buffer with an optional blob list")
|
|
26
|
-
end
|
|
27
|
-
return nil
|
|
28
|
-
end
|
|
29
|
-
local ok, result = pcall(decoder, payload, (blobs or NO_BLOBS))
|
|
32
|
+
local ok, result = decode(decoder, args[1], args[2])
|
|
30
33
|
if not ok then
|
|
31
34
|
local _result = onMalformed
|
|
32
35
|
if _result ~= nil then
|
|
33
|
-
_result(player,
|
|
36
|
+
_result(player, result)
|
|
34
37
|
end
|
|
35
38
|
return nil
|
|
36
39
|
end
|
|
37
|
-
return result
|
|
40
|
+
return trimArguments(result)
|
|
38
41
|
end
|
|
39
42
|
local function createEvent(options)
|
|
40
43
|
local remote = createRemoteInstance(if options.reliability == "unreliable" then "UnreliableRemoteEvent" else "RemoteEvent", options.namespace, options.debugName, options.id)
|
|
41
|
-
|
|
42
|
-
local
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
-- Which half this event is, fixed when it is made: the server's handlers are given the sender.
|
|
45
|
+
local isServer = RunService:IsServer()
|
|
46
|
+
-- Passes the arguments by reference. A BindableEvent would copy them, turning a decoded
|
|
47
|
+
-- `Map<Instance, ...>` into one keyed by strings and raising on a `Set<boolean>`.
|
|
48
|
+
local signal = createSignal()
|
|
49
|
+
-- Guards, then middleware, then the signal: plain calls in the thread that received the message.
|
|
50
|
+
local process = createProcessor(options.incomingMiddleware, options.networkInfo, deliverTo(signal, isServer), options.incomingGuards, nil, nil)
|
|
51
|
+
-- Nothing to deliver to until something connects (a `predict` may come first), so the remote is
|
|
52
|
+
-- only listened to from the first connection on.
|
|
53
|
+
local listening = false
|
|
54
|
+
local listen = function()
|
|
55
|
+
if listening then
|
|
56
|
+
return nil
|
|
48
57
|
end
|
|
49
|
-
|
|
50
|
-
local receive = function(player, args)
|
|
51
|
-
local decoded = decodeArguments(options.incomingDecoder, player, args, options.onMalformed)
|
|
52
|
-
if decoded then
|
|
53
|
-
invoke(player, unpack(decoded))
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
local createConnection = function(callback)
|
|
57
|
-
if bindable then
|
|
58
|
-
return bindable.Event:Connect(callback)
|
|
59
|
-
end
|
|
60
|
-
bindable = Instance.new("BindableEvent")
|
|
58
|
+
listening = true
|
|
61
59
|
-- We defer to allow any other immediate connections to take place before unloading Roblox's queue.
|
|
62
60
|
task.defer(function()
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
end)
|
|
61
|
+
-- Runs in the thread the engine gives the handler, one per message.
|
|
62
|
+
local receive = createReceiver(options.incomingDecoder, options.onMalformed, process, isServer)
|
|
63
|
+
if isServer then
|
|
64
|
+
remote.OnServerEvent:Connect(receive)
|
|
68
65
|
else
|
|
69
|
-
remote.OnClientEvent:Connect(
|
|
70
|
-
local args = { ... }
|
|
71
|
-
return receive(nil, args)
|
|
72
|
-
end)
|
|
66
|
+
remote.OnClientEvent:Connect(receive)
|
|
73
67
|
end
|
|
74
68
|
end)
|
|
75
|
-
return bindable.Event:Connect(callback)
|
|
76
69
|
end
|
|
77
70
|
return {
|
|
78
71
|
fireEither = function(self, player, ...)
|
|
@@ -98,14 +91,19 @@ local function createEvent(options)
|
|
|
98
91
|
connectServer = function(self, callback)
|
|
99
92
|
local _arg0 = RunService:IsServer()
|
|
100
93
|
assert(_arg0)
|
|
101
|
-
|
|
94
|
+
listen()
|
|
95
|
+
return signal:Connect(callback)
|
|
102
96
|
end,
|
|
103
97
|
connectClient = function(self, callback)
|
|
104
98
|
local _arg0 = RunService:IsClient()
|
|
105
99
|
assert(_arg0)
|
|
106
|
-
|
|
100
|
+
listen()
|
|
101
|
+
return signal:Connect(callback)
|
|
102
|
+
end,
|
|
103
|
+
predict = function(self, player, ...)
|
|
104
|
+
local args = { ... }
|
|
105
|
+
spawn(process, player, unpack(args))
|
|
107
106
|
end,
|
|
108
|
-
invoke = invoke,
|
|
109
107
|
}
|
|
110
108
|
end
|
|
111
109
|
return {
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local trimArguments = TS.import(script, script.Parent.Parent, "util", "trimArguments").trimArguments
|
|
2
4
|
local function createClientMethod(receiver, sender)
|
|
5
|
+
-- A method that takes an argument list trims it before spreading it, or an explicit trailing
|
|
6
|
+
-- `undefined` would lose the arguments after a gap (see `trimArguments`).
|
|
3
7
|
local method = {
|
|
4
8
|
fire = function(self, ...)
|
|
5
9
|
local args = { ... }
|
|
6
|
-
sender:fireServer(unpack(args))
|
|
10
|
+
sender:fireServer(unpack(trimArguments(args)))
|
|
7
11
|
end,
|
|
8
12
|
_fire = function(self, ...)
|
|
9
13
|
local packed = { ... }
|
|
@@ -14,13 +18,13 @@ local function createClientMethod(receiver, sender)
|
|
|
14
18
|
end,
|
|
15
19
|
predict = function(self, ...)
|
|
16
20
|
local args = { ... }
|
|
17
|
-
|
|
21
|
+
receiver:predict(nil, unpack(trimArguments(args)))
|
|
18
22
|
end,
|
|
19
23
|
}
|
|
20
24
|
setmetatable(method, {
|
|
21
25
|
__call = function(method, ...)
|
|
22
26
|
local args = { ... }
|
|
23
|
-
method:fire(unpack(args))
|
|
27
|
+
method:fire(unpack((trimArguments(args))))
|
|
24
28
|
end,
|
|
25
29
|
})
|
|
26
30
|
return method
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
-- Compiled with roblox-ts v3.0.0
|
|
2
2
|
local TS = _G[script]
|
|
3
|
-
local
|
|
3
|
+
local createGuards = TS.import(script, script.Parent.Parent, "middleware", "createGuards").createGuards
|
|
4
4
|
local createEvent = TS.import(script, script.Parent.Parent, "event", "createEvent").createEvent
|
|
5
5
|
local getNamespaceConfig = TS.import(script, script.Parent.Parent, "util", "getNamespaceConfig").getNamespaceConfig
|
|
6
6
|
local Players = TS.import(script, TS.getModule(script, "@rbxts", "services")).Players
|
|
@@ -44,11 +44,13 @@ local function createGenericHandler(globalName, namespaceName, metadata, config,
|
|
|
44
44
|
name = effectiveName,
|
|
45
45
|
globalName = globalName,
|
|
46
46
|
}
|
|
47
|
+
-- The generated guards run ahead of all user middleware, so middleware never sees a payload that
|
|
48
|
+
-- failed them.
|
|
49
|
+
local incomingGuards
|
|
47
50
|
if not config.disableIncomingGuards and isIncoming then
|
|
48
51
|
local guards = metadata.incoming[name]
|
|
49
52
|
assert(guards)
|
|
50
|
-
|
|
51
|
-
table.insert(incomingMiddleware, 1, _arg0)
|
|
53
|
+
incomingGuards = createGuards(name, guards[1], guards[2], networkInfo, config.warnOnInvalidGuards, signals)
|
|
52
54
|
end
|
|
53
55
|
-- A malformed serialized payload is reported like a failed guard, with no argument index.
|
|
54
56
|
local onMalformed = function(player, message)
|
|
@@ -70,6 +72,7 @@ local function createGenericHandler(globalName, namespaceName, metadata, config,
|
|
|
70
72
|
debugName = name,
|
|
71
73
|
networkInfo = networkInfo,
|
|
72
74
|
incomingMiddleware = incomingMiddleware,
|
|
75
|
+
incomingGuards = incomingGuards,
|
|
73
76
|
}
|
|
74
77
|
local _left = "incomingDecoder"
|
|
75
78
|
local _result
|
|
@@ -5,6 +5,7 @@ local createClientMethod = TS.import(script, script.Parent, "createClientMethod"
|
|
|
5
5
|
local createServerMethod = TS.import(script, script.Parent, "createServerMethod").createServerMethod
|
|
6
6
|
local createSignalContainer = TS.import(script, script.Parent.Parent, "util", "createSignalContainer").createSignalContainer
|
|
7
7
|
local createGenericHandler = TS.import(script, script.Parent, "createGenericHandler").createGenericHandler
|
|
8
|
+
local createOnce = TS.import(script, script.Parent.Parent, "util", "createOnce").createOnce
|
|
8
9
|
local function getDefaultConfiguration(config)
|
|
9
10
|
local _object = {}
|
|
10
11
|
local _left = "middleware"
|
|
@@ -30,7 +31,9 @@ end
|
|
|
30
31
|
local function createNetworkingEvent(globalName)
|
|
31
32
|
local signals = createSignalContainer()
|
|
32
33
|
local server
|
|
33
|
-
|
|
34
|
+
-- Built once: building it waits for the server's remotes, and a thread that asks meanwhile gets the
|
|
35
|
+
-- same handler rather than a second one wired to the same remotes.
|
|
36
|
+
local client = createOnce()
|
|
34
37
|
return {
|
|
35
38
|
createServer = function(self, config, meta)
|
|
36
39
|
if RunService:IsRunning() and not RunService:IsServer() then
|
|
@@ -45,10 +48,9 @@ local function createNetworkingEvent(globalName)
|
|
|
45
48
|
if RunService:IsRunning() and not RunService:IsClient() then
|
|
46
49
|
return nil
|
|
47
50
|
end
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
end
|
|
51
|
-
return client
|
|
51
|
+
return client(function()
|
|
52
|
+
return createGenericHandler(globalName, nil, meta, getDefaultConfiguration(config), signals, createClientMethod)
|
|
53
|
+
end)
|
|
52
54
|
end,
|
|
53
55
|
registerHandler = function(self, key, callback)
|
|
54
56
|
return signals:connect(key, callback)
|
|
@@ -8,6 +8,6 @@ export declare function createServerMethod(receiver: EventInterface, sender: Eve
|
|
|
8
8
|
except: (players: Player | Player[], ...args: unknown[]) => void;
|
|
9
9
|
broadcast: (...args: unknown[]) => void;
|
|
10
10
|
readonly _flamework_receive?: unknown[] | undefined;
|
|
11
|
-
connect: (cb: (player: Player, ...args: unknown[]) => void) =>
|
|
11
|
+
connect: (cb: (player: Player, ...args: unknown[]) => void) => import("../util/signal").SignalConnection;
|
|
12
12
|
predict: (player: Player, ...args: unknown[]) => void;
|
|
13
13
|
};
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
-- Compiled with roblox-ts v3.0.0
|
|
2
2
|
local TS = _G[script]
|
|
3
3
|
local Players = TS.import(script, TS.getModule(script, "@rbxts", "services")).Players
|
|
4
|
+
local trimArguments = TS.import(script, script.Parent.Parent, "util", "trimArguments").trimArguments
|
|
4
5
|
local function createServerMethod(receiver, sender)
|
|
6
|
+
-- A method that takes an argument list trims it before spreading it, or an explicit trailing
|
|
7
|
+
-- `undefined` would lose the arguments after a gap (see `trimArguments`).
|
|
5
8
|
local method = {
|
|
6
9
|
fire = function(self, players, ...)
|
|
7
10
|
local args = { ... }
|
|
11
|
+
args = trimArguments(args)
|
|
8
12
|
local _players = players
|
|
9
13
|
if typeof(_players) == "Instance" then
|
|
10
14
|
sender:fireClient(players, unpack(args))
|
|
@@ -16,10 +20,11 @@ local function createServerMethod(receiver, sender)
|
|
|
16
20
|
end,
|
|
17
21
|
broadcast = function(self, ...)
|
|
18
22
|
local args = { ... }
|
|
19
|
-
sender:fireAllClients(unpack(args))
|
|
23
|
+
sender:fireAllClients(unpack(trimArguments(args)))
|
|
20
24
|
end,
|
|
21
25
|
except = function(self, players, ...)
|
|
22
26
|
local args = { ... }
|
|
27
|
+
args = trimArguments(args)
|
|
23
28
|
local _players = players
|
|
24
29
|
if typeof(_players) == "Instance" then
|
|
25
30
|
players = { players }
|
|
@@ -47,13 +52,13 @@ local function createServerMethod(receiver, sender)
|
|
|
47
52
|
end,
|
|
48
53
|
predict = function(self, player, ...)
|
|
49
54
|
local args = { ... }
|
|
50
|
-
receiver
|
|
55
|
+
receiver:predict(player, unpack(trimArguments(args)))
|
|
51
56
|
end,
|
|
52
57
|
}
|
|
53
58
|
setmetatable(method, {
|
|
54
59
|
__call = function(method, player, ...)
|
|
55
60
|
local args = { ... }
|
|
56
|
-
method:fire(player, unpack(args))
|
|
61
|
+
method:fire(player, unpack(trimArguments(args)))
|
|
57
62
|
end,
|
|
58
63
|
})
|
|
59
64
|
return method
|
package/out/events/types.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "../types";
|
|
11
11
|
import { EventNetworkingEvents } from "../handlers";
|
|
12
12
|
import { EventMiddleware } from "../middleware/types";
|
|
13
|
+
import { SignalConnection } from "../util/signal";
|
|
13
14
|
import { Modding } from "@flamework-experimental/core";
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -56,7 +57,7 @@ export interface RawServerReceiver<I extends unknown[]> {
|
|
|
56
57
|
* Connect to this networking event.
|
|
57
58
|
* @param callback The callback that will be fired
|
|
58
59
|
*/
|
|
59
|
-
connect(cb: (player: Player, ...args: I) => void):
|
|
60
|
+
connect(cb: (player: Player, ...args: I) => void): SignalConnection;
|
|
60
61
|
|
|
61
62
|
/**
|
|
62
63
|
* Fires a server event using player as the sender.
|
|
@@ -91,7 +92,7 @@ export interface RawClientReceiver<I extends unknown[]> {
|
|
|
91
92
|
* Connect to this networking event.
|
|
92
93
|
* @param callback The callback that will be fired
|
|
93
94
|
*/
|
|
94
|
-
connect(cb: (...args: I) => void):
|
|
95
|
+
connect(cb: (...args: I) => void): SignalConnection;
|
|
95
96
|
|
|
96
97
|
/**
|
|
97
98
|
* Fires a client event.
|
|
@@ -174,7 +175,7 @@ export interface GlobalEvent<S, C> {
|
|
|
174
175
|
registerHandler<K extends keyof EventNetworkingEvents>(
|
|
175
176
|
key: K,
|
|
176
177
|
callback: EventNetworkingEvents[K],
|
|
177
|
-
):
|
|
178
|
+
): SignalConnection;
|
|
178
179
|
}
|
|
179
180
|
|
|
180
181
|
export type EventNamespaces<T> = ExcludeMembers<T, Callback>;
|
|
@@ -2,6 +2,7 @@ import { Serialization } from "@flamework-experimental/core";
|
|
|
2
2
|
import { NetworkInfo } from "../types";
|
|
3
3
|
import { NetworkingFunctionError } from "./errors";
|
|
4
4
|
import { MiddlewareFactory } from "../middleware/types";
|
|
5
|
+
import { Guards } from "../middleware/processor";
|
|
5
6
|
export interface CreateFunctionReceiverOptions {
|
|
6
7
|
/**
|
|
7
8
|
* The namespace this event should be created in.
|
|
@@ -22,18 +23,18 @@ export interface CreateFunctionReceiverOptions {
|
|
|
22
23
|
*/
|
|
23
24
|
networkInfo: NetworkInfo;
|
|
24
25
|
/**
|
|
25
|
-
*
|
|
26
|
+
* The middleware a request goes through before it reaches the callback.
|
|
26
27
|
*/
|
|
27
28
|
incomingMiddleware?: MiddlewareFactory<any[], any>[];
|
|
28
29
|
/**
|
|
29
|
-
*
|
|
30
|
+
* The generated argument checks, run on a request before any middleware. A request that fails
|
|
31
|
+
* them is rejected with `BadRequest`.
|
|
30
32
|
*/
|
|
31
|
-
|
|
33
|
+
incomingGuards?: Guards;
|
|
32
34
|
/**
|
|
33
|
-
* Unpacks the
|
|
34
|
-
* Absent when the project does not enable serialization.
|
|
35
|
+
* Unpacks the request's argument list. Absent when the project does not enable serialization.
|
|
35
36
|
*/
|
|
36
|
-
|
|
37
|
+
argsDecoder?: Serialization.Decoder;
|
|
37
38
|
/**
|
|
38
39
|
* Called when a request cannot be decoded; the caller receives `BadRequest`.
|
|
39
40
|
*/
|
|
@@ -44,9 +45,9 @@ export interface RequestInfo {
|
|
|
44
45
|
requests: Map<number, (value: unknown, rejection?: NetworkingFunctionError) => void>;
|
|
45
46
|
}
|
|
46
47
|
export interface FunctionReceiverInterface {
|
|
47
|
-
/** `
|
|
48
|
-
setServerCallback(callback: (player: Player, ...args: unknown[]) => unknown,
|
|
49
|
-
setClientCallback(callback: (...args: unknown[]) => unknown,
|
|
48
|
+
/** `pack`: turns a successful result into `[payload, blobs?]`, generated by the transformer. */
|
|
49
|
+
setServerCallback(callback: (player: Player, ...args: unknown[]) => unknown, pack?: (value: unknown) => unknown): void;
|
|
50
|
+
setClientCallback(callback: (...args: unknown[]) => unknown, pack?: (value: unknown) => unknown): void;
|
|
50
51
|
invoke(player: Player | undefined, ...args: unknown[]): Promise<unknown>;
|
|
51
52
|
}
|
|
52
53
|
export declare function createFunctionReceiver(options: CreateFunctionReceiverOptions): FunctionReceiverInterface;
|
|
@@ -5,10 +5,12 @@ local _createEvent = TS.import(script, script.Parent.Parent, "event", "createEve
|
|
|
5
5
|
local createEvent = _createEvent.createEvent
|
|
6
6
|
local decodeArguments = _createEvent.decodeArguments
|
|
7
7
|
local NetworkingFunctionError = TS.import(script, script.Parent, "errors").NetworkingFunctionError
|
|
8
|
-
local createMiddlewareProcessor = TS.import(script, script.Parent.Parent, "middleware", "createMiddlewareProcessor").createMiddlewareProcessor
|
|
9
8
|
local _skip = TS.import(script, script.Parent.Parent, "middleware", "skip")
|
|
10
9
|
local Skip = _skip.Skip
|
|
11
10
|
local SkipBadRequest = _skip.SkipBadRequest
|
|
11
|
+
local _processor = TS.import(script, script.Parent.Parent, "middleware", "processor")
|
|
12
|
+
local createProcessor = _processor.createProcessor
|
|
13
|
+
local withoutPlayer = _processor.withoutPlayer
|
|
12
14
|
local getProcessResult
|
|
13
15
|
local function createFunctionReceiver(options)
|
|
14
16
|
local event = createEvent({
|
|
@@ -17,57 +19,63 @@ local function createFunctionReceiver(options)
|
|
|
17
19
|
id = options.id,
|
|
18
20
|
networkInfo = options.networkInfo,
|
|
19
21
|
})
|
|
20
|
-
|
|
21
|
-
local
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return newCallback(unpack(args))
|
|
30
|
-
end
|
|
31
|
-
end)
|
|
22
|
+
-- Which half this function is, fixed when it is made: the server's callback is given the sender.
|
|
23
|
+
local isServer = RunService:IsServer()
|
|
24
|
+
-- Guards, then middleware, then the callback: plain calls in the thread handling the request, a
|
|
25
|
+
-- Promise any of them returns waited for in place and a cancelled one read as `Networking.Skip`.
|
|
26
|
+
local process
|
|
27
|
+
local packResult
|
|
28
|
+
local setCallback = function(newCallback, pack)
|
|
29
|
+
packResult = pack
|
|
30
|
+
process = createProcessor(options.incomingMiddleware, options.networkInfo, if isServer then newCallback else withoutPlayer(newCallback), options.incomingGuards, SkipBadRequest, Skip)
|
|
32
31
|
end
|
|
33
32
|
--[[
|
|
34
33
|
*
|
|
35
|
-
*
|
|
36
|
-
* type carries nothing (`void`); errors always go back as they are.
|
|
34
|
+
* With serialization, a successful value leaves packed as `[payload, blobs?]`, or nothing at all
|
|
35
|
+
* when the result type carries nothing (`void`); errors always go back as they are. It is packed
|
|
36
|
+
* here, after the middleware, so a value a middleware returns is packed like the callback's own.
|
|
37
37
|
|
|
38
38
|
]]
|
|
39
39
|
local respond = function(player, id, processResult, value)
|
|
40
|
-
if processResult == true and
|
|
41
|
-
local
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
if processResult == true and packResult then
|
|
41
|
+
local packed = packResult(value)
|
|
42
|
+
if packed ~= nil then
|
|
43
|
+
event:fireEither(player, id, processResult, packed[1], packed[2])
|
|
44
|
+
else
|
|
45
|
+
event:fireEither(player, id, processResult)
|
|
46
|
+
end
|
|
45
47
|
else
|
|
46
48
|
event:fireEither(player, id, processResult, value)
|
|
47
49
|
end
|
|
48
50
|
end
|
|
51
|
+
--* A request that raised -- in a guard, a middleware, the callback, its Promise or the packing -- is answered `false`.
|
|
52
|
+
local fail = function(player, id, reason)
|
|
53
|
+
warn(`Failed to process request to '{options.debugName}'`)
|
|
54
|
+
warn(reason)
|
|
55
|
+
event:fireEither(player, id, false)
|
|
56
|
+
end
|
|
57
|
+
-- Runs on the thread the event's signal gives this handler, one per request.
|
|
49
58
|
local processRequest = function(player, id, ...)
|
|
50
59
|
local args = { ... }
|
|
51
|
-
|
|
60
|
+
local current = process
|
|
61
|
+
if not current then
|
|
52
62
|
return event:fireEither(player, id, NetworkingFunctionError.Unprocessed)
|
|
53
63
|
end
|
|
54
64
|
local decoded = decodeArguments(options.argsDecoder, player, args, options.onMalformed)
|
|
55
65
|
if not decoded then
|
|
56
66
|
return event:fireEither(player, id, NetworkingFunctionError.BadRequest)
|
|
57
67
|
end
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
local ok, value = pcall(current, player, unpack(decoded))
|
|
69
|
+
if not ok then
|
|
70
|
+
return fail(player, id, value)
|
|
71
|
+
end
|
|
72
|
+
local sent, reason = pcall(respond, player, id, getProcessResult(value), value)
|
|
73
|
+
if not sent then
|
|
74
|
+
fail(player, id, reason)
|
|
75
|
+
end
|
|
65
76
|
end
|
|
66
|
-
if
|
|
67
|
-
event:connectServer(
|
|
68
|
-
local args = { ... }
|
|
69
|
-
return processRequest(player, id, unpack(args))
|
|
70
|
-
end)
|
|
77
|
+
if isServer then
|
|
78
|
+
event:connectServer(processRequest)
|
|
71
79
|
else
|
|
72
80
|
event:connectClient(function(id, ...)
|
|
73
81
|
local args = { ... }
|
|
@@ -75,37 +83,27 @@ local function createFunctionReceiver(options)
|
|
|
75
83
|
end)
|
|
76
84
|
end
|
|
77
85
|
return {
|
|
78
|
-
setServerCallback = function(self, callback,
|
|
79
|
-
|
|
80
|
-
packed = false
|
|
81
|
-
end
|
|
82
|
-
setCallback(callback, packed)
|
|
86
|
+
setServerCallback = function(self, callback, pack)
|
|
87
|
+
setCallback(callback, pack)
|
|
83
88
|
end,
|
|
84
|
-
setClientCallback = function(self, callback,
|
|
85
|
-
|
|
86
|
-
packed = false
|
|
87
|
-
end
|
|
88
|
-
setCallback(callback, packed)
|
|
89
|
+
setClientCallback = function(self, callback, pack)
|
|
90
|
+
setCallback(callback, pack)
|
|
89
91
|
end,
|
|
90
92
|
invoke = function(self, player, ...)
|
|
91
93
|
local args = { ... }
|
|
92
|
-
|
|
94
|
+
local current = process
|
|
95
|
+
if not current then
|
|
93
96
|
return TS.Promise.reject(NetworkingFunctionError.Unprocessed)
|
|
94
97
|
end
|
|
95
|
-
|
|
98
|
+
-- The Promise `predict` returns is the only one: the chain runs in its executor's thread.
|
|
99
|
+
return TS.Promise.new(function(resolve, reject)
|
|
100
|
+
local value = current(player, unpack(args))
|
|
96
101
|
local processResult = getProcessResult(value)
|
|
97
102
|
if processResult ~= true then
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
local decoder = options.resultDecoder
|
|
102
|
-
if not packedResults or not decoder then
|
|
103
|
-
return value
|
|
103
|
+
reject(processResult)
|
|
104
|
+
else
|
|
105
|
+
resolve(value)
|
|
104
106
|
end
|
|
105
|
-
local _binding = value
|
|
106
|
-
local payload = _binding[1]
|
|
107
|
-
local blobs = _binding[2]
|
|
108
|
-
return decoder(payload, blobs or {})[1]
|
|
109
107
|
end)
|
|
110
108
|
end,
|
|
111
109
|
}
|
|
@@ -39,7 +39,8 @@ export interface RequestInfo {
|
|
|
39
39
|
requests: Map<number, (value: unknown, rejection?: NetworkingFunctionError) => void>;
|
|
40
40
|
}
|
|
41
41
|
export interface FunctionSenderInterface {
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
/** `timeout` in seconds; `math.huge` waits for good. */
|
|
43
|
+
invokeServer(timeout: number, ...args: unknown[]): Promise<unknown>;
|
|
44
|
+
invokeClient(player: Player, timeout: number, ...args: unknown[]): Promise<unknown>;
|
|
44
45
|
}
|
|
45
46
|
export declare function createFunctionSender(options: CreateFunctionSenderOptions): FunctionSenderInterface;
|