@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.
Files changed (44) hide show
  1. package/README.md +6 -0
  2. package/flamework.build +1 -1
  3. package/out/event/createEvent.d.ts +22 -4
  4. package/out/event/createEvent.luau +47 -49
  5. package/out/events/createClientMethod.luau +7 -3
  6. package/out/events/createGenericHandler.luau +6 -3
  7. package/out/events/createNetworkingEvent.luau +7 -5
  8. package/out/events/createServerMethod.d.ts +1 -1
  9. package/out/events/createServerMethod.luau +8 -3
  10. package/out/events/types.d.ts +4 -3
  11. package/out/function/createFunctionReceiver.d.ts +10 -9
  12. package/out/function/createFunctionReceiver.luau +53 -55
  13. package/out/function/createFunctionSender.d.ts +3 -2
  14. package/out/function/createFunctionSender.luau +118 -40
  15. package/out/functions/createClientMethod.d.ts +1 -1
  16. package/out/functions/createClientMethod.luau +9 -8
  17. package/out/functions/createGenericHandler.luau +8 -12
  18. package/out/functions/createNetworkingFunction.luau +7 -5
  19. package/out/functions/createServerMethod.d.ts +1 -1
  20. package/out/functions/createServerMethod.luau +9 -8
  21. package/out/functions/types.d.ts +10 -12
  22. package/out/handlers.d.ts +35 -36
  23. package/out/index.d.ts +6 -0
  24. package/out/init.luau +6 -0
  25. package/out/middleware/createGuards.d.ts +10 -0
  26. package/out/middleware/createGuards.luau +32 -0
  27. package/out/middleware/processor.d.ts +62 -0
  28. package/out/middleware/processor.luau +214 -0
  29. package/out/middleware/types.d.ts +26 -15
  30. package/out/util/createOnce.d.ts +14 -0
  31. package/out/util/createOnce.luau +66 -0
  32. package/out/util/createSignalContainer.d.ts +2 -1
  33. package/out/util/createSignalContainer.luau +2 -2
  34. package/out/util/signal.d.ts +27 -0
  35. package/out/util/signal.luau +135 -0
  36. package/out/util/trimArguments.d.ts +12 -0
  37. package/out/util/trimArguments.luau +48 -0
  38. package/package.json +2 -3
  39. package/out/middleware/createGuardMiddleware.d.ts +0 -6
  40. package/out/middleware/createGuardMiddleware.luau +0 -44
  41. package/out/middleware/createMiddlewareProcessor.d.ts +0 -3
  42. package/out/middleware/createMiddlewareProcessor.luau +0 -24
  43. package/out/util/timeoutPromise.d.ts +0 -1
  44. 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,6 @@
1
1
  {
2
2
  "version": 1,
3
- "flameworkVersion": "2.0.0-alpha.1",
3
+ "flameworkVersion": "2.0.0-alpha.3",
4
4
  "identifiers": {},
5
5
  "idGenerationMode": "full",
6
6
  "identifierPrefix": "$n"
@@ -1,6 +1,8 @@
1
1
  import { Serialization } from "@flamework-experimental/core";
2
- import { MiddlewareFactory, MiddlewareProcessor } from "../middleware/types";
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): RBXScriptConnection;
50
- connectClient(callback: (...args: unknown[]) => void): RBXScriptConnection;
51
- invoke: MiddlewareProcessor<any[], void>;
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 createMiddlewareProcessor = TS.import(script, script.Parent.Parent, "middleware", "createMiddlewareProcessor").createMiddlewareProcessor
6
- --* A sender whose type has blob slots always sends a table; one without never does.
7
- local NO_BLOBS = {}
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 _binding = args
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, tostring(result))
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
- local bindable
42
- local invoke = createMiddlewareProcessor(options.incomingMiddleware, options.networkInfo, function(player, ...)
43
- local args = { ... }
44
- if RunService:IsServer() then
45
- bindable:Fire(player, unpack(args))
46
- else
47
- bindable:Fire(unpack(args))
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
- end)
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
- if RunService:IsServer() then
64
- remote.OnServerEvent:Connect(function(player, ...)
65
- local args = { ... }
66
- return receive(player, args)
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(function(...)
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
- return createConnection(callback)
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
- return createConnection(callback)
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
- return receiver.invoke(nil, unpack(args))
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 createGuardMiddleware = TS.import(script, script.Parent.Parent, "middleware", "createGuardMiddleware").createGuardMiddleware
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
- local _arg0 = createGuardMiddleware(name, guards[1], guards[2], networkInfo, config.warnOnInvalidGuards, signals)
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
- local client
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
- if client == nil then
49
- client = createGenericHandler(globalName, nil, meta, getDefaultConfiguration(config), signals, createClientMethod)
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) => RBXScriptConnection;
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.invoke(player, unpack(args))
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
@@ -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): RBXScriptConnection;
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): RBXScriptConnection;
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
- ): RBXScriptConnection;
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
- * This function will be called when we receive a response, and can be used to resolve or reject values.
26
+ * The middleware a request goes through before it reaches the callback.
26
27
  */
27
28
  incomingMiddleware?: MiddlewareFactory<any[], any>[];
28
29
  /**
29
- * Unpacks the request's argument list. Absent when the project does not enable serialization.
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
- argsDecoder?: Serialization.Decoder;
33
+ incomingGuards?: Guards;
32
34
  /**
33
- * Unpacks the results a packed callback returns, so that `predict` resolves with the value itself.
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
- resultDecoder?: Serialization.Decoder;
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
- /** `packed`: the callback returns successful results as `[payload, blobs?]`, packed by the transformer. */
48
- setServerCallback(callback: (player: Player, ...args: unknown[]) => unknown, packed?: boolean): void;
49
- setClientCallback(callback: (...args: unknown[]) => unknown, packed?: boolean): void;
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
- local callback
21
- local packedResults = false
22
- local setCallback = function(newCallback, packed)
23
- packedResults = packed
24
- callback = createMiddlewareProcessor(options.incomingMiddleware, options.networkInfo, function(player, ...)
25
- local args = { ... }
26
- if RunService:IsServer() then
27
- return newCallback(player, unpack(args))
28
- else
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
- * A packed callback's successful value is `[payload, blobs?]`, or nothing at all when the result
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 packedResults and value ~= nil then
41
- local _binding = value
42
- local payload = _binding[1]
43
- local blobs = _binding[2]
44
- event:fireEither(player, id, processResult, payload, blobs)
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
- if not callback then
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
- callback(player, unpack(decoded)):andThen(function(value)
59
- return respond(player, id, getProcessResult(value), value)
60
- end):catch(function(reason)
61
- warn(`Failed to process request to '{options.debugName}'`)
62
- warn(reason)
63
- event:fireEither(player, id, false)
64
- end)
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 RunService:IsServer() then
67
- event:connectServer(function(player, id, ...)
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, packed)
79
- if packed == nil then
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, packed)
85
- if packed == nil then
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
- if not callback then
94
+ local current = process
95
+ if not current then
93
96
  return TS.Promise.reject(NetworkingFunctionError.Unprocessed)
94
97
  end
95
- return callback(player, unpack(args)):andThen(function(value)
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
- return TS.Promise.reject(processResult)
99
- end
100
- -- A packed callback answered with bytes; a local caller wants the value.
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
- invokeServer(...args: unknown[]): Promise<unknown>;
43
- invokeClient(player: Player, ...args: unknown[]): Promise<unknown>;
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;