@flamework-experimental/networking 2.0.0-alpha.1 → 2.0.0-alpha.3
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 +35 -27
- 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 +3 -4
- 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/out/functions/types.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "../types";
|
|
13
13
|
import { FunctionNetworkingEvents } from "../handlers";
|
|
14
14
|
import { FunctionMiddleware } from "../middleware/types";
|
|
15
|
+
import { SignalConnection } from "../util/signal";
|
|
15
16
|
import { Modding } from "@flamework-experimental/core";
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -68,8 +69,8 @@ export interface ServerReceiver<I extends unknown[], O, F = unknown> extends Raw
|
|
|
68
69
|
/** @hidden The declared function type; its return type is what the transformer packs. */
|
|
69
70
|
readonly _flamework_fn?: F;
|
|
70
71
|
|
|
71
|
-
/** @hidden Registers a callback
|
|
72
|
-
_setCallback(callback: (player: Player, ...args: never[]) => unknown): void;
|
|
72
|
+
/** @hidden Registers a callback with `pack`, which turns a successful result into `[payload, blobs?]`. */
|
|
73
|
+
_setCallback(callback: (player: Player, ...args: never[]) => unknown, pack: (value: unknown) => unknown): void;
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
export interface RawClientSender<I extends unknown[], O> {
|
|
@@ -119,8 +120,8 @@ export interface ClientReceiver<I extends unknown[], O, F = unknown> extends Raw
|
|
|
119
120
|
/** @hidden The declared function type; its return type is what the transformer packs. */
|
|
120
121
|
readonly _flamework_fn?: F;
|
|
121
122
|
|
|
122
|
-
/** @hidden Registers a callback
|
|
123
|
-
_setCallback(callback: (...args: never[]) => unknown): void;
|
|
123
|
+
/** @hidden Registers a callback with `pack`, which turns a successful result into `[payload, blobs?]`. */
|
|
124
|
+
_setCallback(callback: (...args: never[]) => unknown, pack: (value: unknown) => unknown): void;
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
export type ServerHandler<E, R> = NetworkingObfuscationMarker & {
|
|
@@ -199,7 +200,7 @@ export interface GlobalFunction<S, C> {
|
|
|
199
200
|
registerHandler<K extends keyof FunctionNetworkingEvents>(
|
|
200
201
|
key: K,
|
|
201
202
|
callback: FunctionNetworkingEvents[K],
|
|
202
|
-
):
|
|
203
|
+
): SignalConnection;
|
|
203
204
|
}
|
|
204
205
|
|
|
205
206
|
export interface FunctionConfiguration {
|
|
@@ -250,22 +251,19 @@ export type NamespaceMetadata<R, S> = Modding.Emit<{
|
|
|
250
251
|
incoming: IntrinsicObfuscate<{ [k in keyof Functions<R>]: IntrinsicTupleGuards<Parameters<R[k]>> }>;
|
|
251
252
|
|
|
252
253
|
outgoingIds: ObfuscateNames<keyof Functions<S>>;
|
|
253
|
-
|
|
254
|
+
// A response carries the resolved value: a `Promise<T>` result is checked as `T`.
|
|
255
|
+
outgoing: IntrinsicObfuscate<{ [k in keyof Functions<S>]: Modding.Target.Guard<Awaited<ReturnType<S[k]>>> }>;
|
|
254
256
|
|
|
255
257
|
/**
|
|
256
258
|
* Decoders, present only with `networking.serialization` on and absent for raw functions: the
|
|
257
|
-
* argument lists of requests this realm receives
|
|
258
|
-
*
|
|
259
|
-
* where they are produced.
|
|
259
|
+
* argument lists of requests this realm receives and the responses to requests it sends. Requests
|
|
260
|
+
* and results are packed inline where they are produced.
|
|
260
261
|
*/
|
|
261
262
|
incomingSerializers: IntrinsicObfuscate<{
|
|
262
263
|
[k in keyof Functions<R>]: R[k] extends NetworkRaw<unknown>
|
|
263
264
|
? undefined
|
|
264
265
|
: IntrinsicNetworkDecoder<Parameters<R[k]>>;
|
|
265
266
|
}>;
|
|
266
|
-
incomingResults: IntrinsicObfuscate<{
|
|
267
|
-
[k in keyof Functions<R>]: R[k] extends NetworkRaw<unknown> ? undefined : IntrinsicNetworkResultDecoder<R[k]>;
|
|
268
|
-
}>;
|
|
269
267
|
outgoingResults: IntrinsicObfuscate<{
|
|
270
268
|
[k in keyof Functions<S>]: S[k] extends NetworkRaw<unknown> ? undefined : IntrinsicNetworkResultDecoder<S[k]>;
|
|
271
269
|
}>;
|
package/out/handlers.d.ts
CHANGED
|
@@ -1,36 +1,35 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
1
|
+
import { NetworkInfo } from "./types";
|
|
2
|
+
|
|
3
|
+
interface BaseEvent {
|
|
4
|
+
/**
|
|
5
|
+
* The event or function that was fired.
|
|
6
|
+
*/
|
|
7
|
+
networkInfo: NetworkInfo;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface BadRequestData extends BaseEvent {
|
|
11
|
+
/**
|
|
12
|
+
* The index of the argument that was incorrect, or -1 when a serialized payload could not be decoded.
|
|
13
|
+
*/
|
|
14
|
+
argIndex: number;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The value of the argument that was incorrect, or the decoding error for a malformed payload.
|
|
18
|
+
*/
|
|
19
|
+
argValue: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface BadResponseData extends BaseEvent {
|
|
23
|
+
/**
|
|
24
|
+
* The index of the argument that was incorrect.
|
|
25
|
+
*/
|
|
26
|
+
value: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface EventNetworkingEvents {
|
|
30
|
+
onBadRequest: (player: Player, data: BadRequestData) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface FunctionNetworkingEvents extends EventNetworkingEvents {
|
|
34
|
+
onBadResponse: (player: Player, data: BadResponseData) => void;
|
|
35
|
+
}
|
package/out/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { GlobalFunction } from "./functions/types";
|
|
|
3
3
|
import { Skip as NetworkingSkip } from "./middleware/skip";
|
|
4
4
|
import { NetworkingFunctionError } from "./function/errors";
|
|
5
5
|
import { EventMiddleware as _EventMiddleware, FunctionMiddleware as _FunctionMiddleware } from "./middleware/types";
|
|
6
|
+
import { SignalConnection as _SignalConnection } from "./util/signal";
|
|
6
7
|
import { NetworkRaw, NetworkUnreliable } from "./types";
|
|
7
8
|
import type { Modding } from "@flamework-experimental/core";
|
|
8
9
|
export declare namespace Networking {
|
|
@@ -51,5 +52,10 @@ export declare namespace Networking {
|
|
|
51
52
|
* A function that generates an event middleware.
|
|
52
53
|
*/
|
|
53
54
|
type FunctionMiddleware<I extends readonly unknown[] = unknown[], O = void> = _FunctionMiddleware<I, O>;
|
|
55
|
+
/**
|
|
56
|
+
* What `connect` and `registerHandler` return: `Connected`, `Disconnect()`, and `Destroy()` for
|
|
57
|
+
* maids and janitors. It is networking's own, not an engine `RBXScriptConnection`.
|
|
58
|
+
*/
|
|
59
|
+
type Connection = _SignalConnection;
|
|
54
60
|
}
|
|
55
61
|
export { NetworkingFunctionError };
|
package/out/init.luau
CHANGED
|
@@ -71,6 +71,12 @@ do
|
|
|
71
71
|
* A function that generates an event middleware.
|
|
72
72
|
|
|
73
73
|
]]
|
|
74
|
+
--[[
|
|
75
|
+
*
|
|
76
|
+
* What `connect` and `registerHandler` return: `Connected`, `Disconnect()`, and `Destroy()` for
|
|
77
|
+
* maids and janitors. It is networking's own, not an engine `RBXScriptConnection`.
|
|
78
|
+
|
|
79
|
+
]]
|
|
74
80
|
end
|
|
75
81
|
return {
|
|
76
82
|
Networking = Networking,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { t } from "@rbxts/t";
|
|
2
|
+
import { SignalContainer } from "../util/createSignalContainer";
|
|
3
|
+
import { EventNetworkingEvents } from "../handlers";
|
|
4
|
+
import { NetworkInfo } from "../types";
|
|
5
|
+
import { Guards } from "./processor";
|
|
6
|
+
/**
|
|
7
|
+
* The generated argument checks of one event or function, for the receive pipeline, which runs them
|
|
8
|
+
* ahead of all user middleware. A failure warns (with `warnOnInvalid`) and fires `onBadRequest`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createGuards(name: string, fixedParameters: t.check<unknown>[], restParameter: t.check<unknown> | undefined, networkInfo: NetworkInfo, warnOnInvalid: boolean, signals: SignalContainer<EventNetworkingEvents>): Guards;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local Players = TS.import(script, TS.getModule(script, "@rbxts", "services")).Players
|
|
4
|
+
--[[
|
|
5
|
+
*
|
|
6
|
+
* The generated argument checks of one event or function, for the receive pipeline, which runs them
|
|
7
|
+
* ahead of all user middleware. A failure warns (with `warnOnInvalid`) and fires `onBadRequest`.
|
|
8
|
+
|
|
9
|
+
]]
|
|
10
|
+
local function createGuards(name, fixedParameters, restParameter, networkInfo, warnOnInvalid, signals)
|
|
11
|
+
return {
|
|
12
|
+
fixed = fixedParameters,
|
|
13
|
+
rest = restParameter,
|
|
14
|
+
reject = function(player, index, value)
|
|
15
|
+
if warnOnInvalid then
|
|
16
|
+
if player then
|
|
17
|
+
warn(`'{player}' sent invalid arguments for event '{name}' (arg #{index}):`, value)
|
|
18
|
+
else
|
|
19
|
+
warn(`Server sent invalid arguments for event '{name}' (arg #{index}):`, value)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
signals:fire("onBadRequest", player or Players.LocalPlayer, {
|
|
23
|
+
networkInfo = networkInfo,
|
|
24
|
+
argIndex = index,
|
|
25
|
+
argValue = value,
|
|
26
|
+
})
|
|
27
|
+
end,
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
return {
|
|
31
|
+
createGuards = createGuards,
|
|
32
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Serialization } from "@flamework-experimental/core";
|
|
2
|
+
import { t } from "@rbxts/t";
|
|
3
|
+
import { NetworkInfo } from "../types";
|
|
4
|
+
import { MiddlewareFactory } from "./types";
|
|
5
|
+
import { Signal } from "../util/signal";
|
|
6
|
+
|
|
7
|
+
/** One step of the receive pipeline: `(player, ...args) -> result`. The player is `undefined` on a client. */
|
|
8
|
+
export type Processor = (player: Player | undefined, ...args: unknown[]) => unknown;
|
|
9
|
+
|
|
10
|
+
/** The generated argument checks, run ahead of all user middleware. */
|
|
11
|
+
export interface Guards {
|
|
12
|
+
/** `fixed[i]` checks argument `i`. */
|
|
13
|
+
fixed: ReadonlyArray<t.check<unknown>>;
|
|
14
|
+
|
|
15
|
+
/** Checks every argument past the fixed ones, for a rest parameter. */
|
|
16
|
+
rest: t.check<unknown> | undefined;
|
|
17
|
+
|
|
18
|
+
/** Called with the first argument that failed, by its 0-based index. */
|
|
19
|
+
reject: (player: Player | undefined, index: number, value: unknown) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Folds the guards and the middleware into one processor that runs them as plain calls in the
|
|
24
|
+
* calling thread and returns what the last link did, a Promise already followed (see `processor.luau`).
|
|
25
|
+
* @param rejected What the processor returns when a guard fails.
|
|
26
|
+
* @param cancelled What a Promise that was cancelled reads as.
|
|
27
|
+
*/
|
|
28
|
+
export function createProcessor(
|
|
29
|
+
middleware: ReadonlyArray<MiddlewareFactory<any, any>> | undefined,
|
|
30
|
+
networkInfo: NetworkInfo,
|
|
31
|
+
final: Processor,
|
|
32
|
+
guards: Guards | undefined,
|
|
33
|
+
rejected: unknown,
|
|
34
|
+
cancelled: unknown,
|
|
35
|
+
): Processor;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The handler for a remote's `OnServerEvent` (`withPlayer`) or `OnClientEvent`: decodes the payload
|
|
39
|
+
* when there is a decoder, reporting and dropping one it cannot read, and runs `process`.
|
|
40
|
+
*/
|
|
41
|
+
export function createReceiver(
|
|
42
|
+
decoder: Serialization.Decoder | undefined,
|
|
43
|
+
onMalformed: ((player: Player | undefined, message: string) => void) | undefined,
|
|
44
|
+
process: Processor,
|
|
45
|
+
withPlayer: boolean,
|
|
46
|
+
): (...args: unknown[]) => void;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Unpacks a serialized argument list, `(payload, blobs?)` as the remote carried it, under `pcall`:
|
|
50
|
+
* `true` and the list, or `false` and why it could not be read.
|
|
51
|
+
*/
|
|
52
|
+
export function decode(
|
|
53
|
+
decoder: Serialization.Decoder,
|
|
54
|
+
payload: unknown,
|
|
55
|
+
blobs: unknown,
|
|
56
|
+
): LuaTuple<[ok: true, list: unknown[]] | [ok: false, message: string]>;
|
|
57
|
+
|
|
58
|
+
/** The last step of an event's chain: fires `signal`, with the sender first when `withPlayer`. */
|
|
59
|
+
export function deliverTo(signal: Signal, withPlayer: boolean): Processor;
|
|
60
|
+
|
|
61
|
+
/** `callback` called without the player in front, which is how a client's callback is written. */
|
|
62
|
+
export function withoutPlayer(callback: (...args: never[]) => unknown): Processor;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
--[[
|
|
2
|
+
The receive pipeline: what runs between a remote delivering a message (or `predict` standing in
|
|
3
|
+
for one) and the handler that receives it.
|
|
4
|
+
|
|
5
|
+
Plain calls in the thread that received the message: the payload is decoded, the generated guards
|
|
6
|
+
check the arguments, each middleware calls the next through `processNext`, and the last step
|
|
7
|
+
delivers (an event's signal, a function's callback). Nothing here makes a Promise or a thread. A
|
|
8
|
+
middleware may still yield, which holds up only the message it is processing, and one that
|
|
9
|
+
returns a Promise is waited for in place.
|
|
10
|
+
|
|
11
|
+
Plain Luau rather than TypeScript so that an argument list keeps its count: roblox-ts builds a
|
|
12
|
+
table from every `...args` and spreads it with `unpack`, which stops at `#list` and may lose the
|
|
13
|
+
values after a nil (see `util/trimArguments.ts`). Here the list travels as varargs, and is only cut
|
|
14
|
+
after its last value where it enters and where a middleware hands on a list of its own.
|
|
15
|
+
]]
|
|
16
|
+
|
|
17
|
+
local TS = _G[script]
|
|
18
|
+
|
|
19
|
+
--- Stands in for a missing blob list when decoding: a sender whose type has blob slots always sends one.
|
|
20
|
+
local NO_BLOBS = {}
|
|
21
|
+
|
|
22
|
+
--[[
|
|
23
|
+
`callback(player, ...)`, with the argument list cut after its last value.
|
|
24
|
+
|
|
25
|
+
A list that ends in nil would reach a TypeScript `(...args)` as a table whose `#` can stop at any
|
|
26
|
+
earlier gap: `{1, nil, 3, nil}` may read as one value. One that ends in a value spreads whole. A
|
|
27
|
+
middleware that names its parameters, `(player, id, name) => processNext(player, id, name)`,
|
|
28
|
+
passes on a list ending in nil when `name` was not sent, which is why every link cuts it again.
|
|
29
|
+
Nothing is lost: an argument that was not passed reads as nil too.
|
|
30
|
+
]]
|
|
31
|
+
local function callTrimmed(callback: (...any) -> ...any, player: any, ...: any): any
|
|
32
|
+
local count = select("#", ...)
|
|
33
|
+
if count == 0 or select(count, ...) ~= nil then
|
|
34
|
+
return callback(player, ...)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
repeat
|
|
38
|
+
count -= 1
|
|
39
|
+
until count == 0 or select(count, ...) ~= nil
|
|
40
|
+
|
|
41
|
+
return callback(player, table.unpack({ ... }, 1, count))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
--[[
|
|
45
|
+
A link's result, with a Promise it returned followed in this thread: its value once resolved,
|
|
46
|
+
`cancelled` if it was cancelled, and its rejection raised.
|
|
47
|
+
]]
|
|
48
|
+
local function follow(value: any, cancelled: any): any
|
|
49
|
+
if type(value) ~= "table" then
|
|
50
|
+
return value
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
local Promise = TS.Promise
|
|
54
|
+
if not Promise.is(value) then
|
|
55
|
+
return value
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
local status, result = value:awaitStatus()
|
|
59
|
+
if status == Promise.Status.Resolved then
|
|
60
|
+
return result
|
|
61
|
+
elseif status == Promise.Status.Cancelled then
|
|
62
|
+
return cancelled
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
error(result, 0)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
--[[
|
|
69
|
+
Folds the guards and the middleware into one function, `(player, ...args) -> result`, that runs
|
|
70
|
+
them in order in the calling thread and returns what the last one did.
|
|
71
|
+
|
|
72
|
+
`middleware` is a list of factories, the first the outermost; each is handed `processNext`, which
|
|
73
|
+
calls the next link and returns its result (a Promise the link returned already followed). A link
|
|
74
|
+
that does not call it drops the message there. `final` ends the chain.
|
|
75
|
+
|
|
76
|
+
`guards`, when given, check the arguments before any middleware runs: `fixed[i]` checks argument
|
|
77
|
+
`i`, `rest` every one past them. The first one that fails is reported through `reject` with its
|
|
78
|
+
0-based index, and the processor returns `rejected` without calling anything else.
|
|
79
|
+
|
|
80
|
+
`cancelled` is what a Promise that was cancelled reads as (`Networking.Skip` for a function).
|
|
81
|
+
]]
|
|
82
|
+
local function createProcessor(
|
|
83
|
+
middleware: { (processNext: any, networkInfo: any) -> any }?,
|
|
84
|
+
networkInfo: any,
|
|
85
|
+
final: (...any) -> ...any,
|
|
86
|
+
guards: any,
|
|
87
|
+
rejected: any,
|
|
88
|
+
cancelled: any
|
|
89
|
+
)
|
|
90
|
+
local head = final
|
|
91
|
+
if middleware ~= nil then
|
|
92
|
+
for index = #middleware, 1, -1 do
|
|
93
|
+
local after = head
|
|
94
|
+
head = middleware[index](function(player, ...)
|
|
95
|
+
return follow(callTrimmed(after, player, ...), cancelled)
|
|
96
|
+
end, networkInfo)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
if guards == nil then
|
|
101
|
+
return function(player, ...)
|
|
102
|
+
return follow(callTrimmed(head, player, ...), cancelled)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
local fixed = guards.fixed
|
|
107
|
+
local rest = guards.rest
|
|
108
|
+
local reject = guards.reject
|
|
109
|
+
local fixedCount = #fixed
|
|
110
|
+
|
|
111
|
+
return function(player, ...)
|
|
112
|
+
-- Checked up to the last value: a trailing nil is an argument that was not passed.
|
|
113
|
+
local count = select("#", ...)
|
|
114
|
+
while count > 0 and select(count, ...) == nil do
|
|
115
|
+
count -= 1
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
for index = 1, math.max(fixedCount, count) do
|
|
119
|
+
local guard = fixed[index] or rest
|
|
120
|
+
if guard ~= nil then
|
|
121
|
+
local value = select(index, ...)
|
|
122
|
+
if not guard(value) then
|
|
123
|
+
reject(player, index - 1, value)
|
|
124
|
+
return rejected
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
return follow(callTrimmed(head, player, ...), cancelled)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
--[[
|
|
134
|
+
Unpacks a serialized argument list, `(payload, blobs?)` as the remote carried it. Returns `true`
|
|
135
|
+
and the list, or `false` and why it could not be read. The decoder runs under `pcall`: a hostile
|
|
136
|
+
buffer raises instead of yielding garbage.
|
|
137
|
+
]]
|
|
138
|
+
local function decode(decoder: (buffer, { any }) -> { any }, payload: any, blobs: any): (boolean, any)
|
|
139
|
+
if type(payload) ~= "buffer" or (blobs ~= nil and type(blobs) ~= "table") then
|
|
140
|
+
return false, "payload is not a buffer with an optional blob list"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
local ok, result = pcall(decoder, payload, blobs or NO_BLOBS)
|
|
144
|
+
if not ok then
|
|
145
|
+
return false, tostring(result)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
return true, result
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
--[[
|
|
152
|
+
The handler to connect to a remote's `OnServerEvent` (`withPlayer`) or `OnClientEvent`: decodes
|
|
153
|
+
the payload when there is a `decoder`, reporting one it cannot read through `onMalformed` and
|
|
154
|
+
dropping it, and hands the arguments to `process`, in the thread the engine runs the handler on.
|
|
155
|
+
]]
|
|
156
|
+
local function createReceiver(
|
|
157
|
+
decoder: ((buffer, { any }) -> { any })?,
|
|
158
|
+
onMalformed: ((player: any, message: string) -> ())?,
|
|
159
|
+
process: (...any) -> ...any,
|
|
160
|
+
withPlayer: boolean
|
|
161
|
+
)
|
|
162
|
+
local receive = process
|
|
163
|
+
if decoder ~= nil then
|
|
164
|
+
receive = function(player, payload, blobs)
|
|
165
|
+
local ok, result = decode(decoder, payload, blobs)
|
|
166
|
+
if not ok then
|
|
167
|
+
if onMalformed ~= nil then
|
|
168
|
+
onMalformed(player, result)
|
|
169
|
+
end
|
|
170
|
+
return
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
-- A decoded list has a slot per declared parameter, so an absent trailing optional leaves
|
|
174
|
+
-- it ending in nil: unpacked up to its last value.
|
|
175
|
+
return process(player, table.unpack(result, 1, table.maxn(result)))
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
if withPlayer then
|
|
180
|
+
return receive
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
return function(...)
|
|
184
|
+
return receive(nil, ...)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
--- The last step of an event's chain: fires `signal`, with the sender first on the server.
|
|
189
|
+
local function deliverTo(signal: any, withPlayer: boolean)
|
|
190
|
+
if withPlayer then
|
|
191
|
+
return function(player, ...)
|
|
192
|
+
signal:Fire(player, ...)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
return function(_player, ...)
|
|
197
|
+
signal:Fire(...)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
--- `callback` called without the player in front, which is how a client's callback is written.
|
|
202
|
+
local function withoutPlayer(callback: (...any) -> ...any)
|
|
203
|
+
return function(_player, ...)
|
|
204
|
+
return callback(...)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
createProcessor = createProcessor,
|
|
210
|
+
createReceiver = createReceiver,
|
|
211
|
+
decode = decode,
|
|
212
|
+
deliverTo = deliverTo,
|
|
213
|
+
withoutPlayer = withoutPlayer,
|
|
214
|
+
}
|
|
@@ -1,15 +1,26 @@
|
|
|
1
|
-
import { NetworkInfo } from "../types";
|
|
2
|
-
import { Skip } from "./skip";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export type
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
export type
|
|
1
|
+
import { NetworkInfo } from "../types";
|
|
2
|
+
import { Skip } from "./skip";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Calls the next link of the chain and returns its result: nothing for an event, the value (or
|
|
6
|
+
* `Networking.Skip`) for a function. A Promise the next link returned has already been waited for,
|
|
7
|
+
* in this thread.
|
|
8
|
+
*/
|
|
9
|
+
export type MiddlewareProcessor<I extends readonly unknown[], O> = (player?: Player, ...args: I) => O;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* One link of the chain. It may yield, which holds up only the message it is processing, and it may
|
|
13
|
+
* return a Promise, which is waited for before the link ahead of it continues.
|
|
14
|
+
*/
|
|
15
|
+
export type Middleware<I extends readonly unknown[] = unknown[], O = void> = (
|
|
16
|
+
player?: Player,
|
|
17
|
+
...args: I
|
|
18
|
+
) => O | Promise<O>;
|
|
19
|
+
|
|
20
|
+
export type MiddlewareFactory<I extends readonly unknown[] = [], O = void> = (
|
|
21
|
+
processNext: MiddlewareProcessor<I, O>,
|
|
22
|
+
event: NetworkInfo,
|
|
23
|
+
) => Middleware<I, O>;
|
|
24
|
+
|
|
25
|
+
export type EventMiddleware<I extends readonly unknown[] = unknown[]> = MiddlewareFactory<I, void>;
|
|
26
|
+
export type FunctionMiddleware<I extends readonly unknown[] = unknown[], O = void> = MiddlewareFactory<I, O | Skip>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a function that builds a value on its first call and returns that value from then on.
|
|
3
|
+
*
|
|
4
|
+
* `build` may yield: a client handler waits for the server's remotes to replicate. A thread that
|
|
5
|
+
* calls while a build is under way waits for it rather than starting a second one, which would wire
|
|
6
|
+
* a second handler to the same remotes. If the build throws, the thread that started it gets the
|
|
7
|
+
* error and the next waiting thread builds again.
|
|
8
|
+
*
|
|
9
|
+
* The build runs on a thread of its own, and the thread that started it waits like the others. A
|
|
10
|
+
* caller can be killed while it waits (`task.cancel`, a cancelled Promise, a test's timeout), and a
|
|
11
|
+
* killed thread never reaches a `finally`: a build killed with its caller would leave every thread
|
|
12
|
+
* queued behind it, and every later caller, waiting for good.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createOnce<T extends defined>(): (build: () => T) => T;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
--[[
|
|
3
|
+
*
|
|
4
|
+
* Returns a function that builds a value on its first call and returns that value from then on.
|
|
5
|
+
*
|
|
6
|
+
* `build` may yield: a client handler waits for the server's remotes to replicate. A thread that
|
|
7
|
+
* calls while a build is under way waits for it rather than starting a second one, which would wire
|
|
8
|
+
* a second handler to the same remotes. If the build throws, the thread that started it gets the
|
|
9
|
+
* error and the next waiting thread builds again.
|
|
10
|
+
*
|
|
11
|
+
* The build runs on a thread of its own, and the thread that started it waits like the others. A
|
|
12
|
+
* caller can be killed while it waits (`task.cancel`, a cancelled Promise, a test's timeout), and a
|
|
13
|
+
* killed thread never reaches a `finally`: a build killed with its caller would leave every thread
|
|
14
|
+
* queued behind it, and every later caller, waiting for good.
|
|
15
|
+
|
|
16
|
+
]]
|
|
17
|
+
local function createOnce()
|
|
18
|
+
local value
|
|
19
|
+
local waiting
|
|
20
|
+
return function(build)
|
|
21
|
+
while value == nil do
|
|
22
|
+
if waiting ~= nil then
|
|
23
|
+
local _waiting = waiting
|
|
24
|
+
local _arg0 = coroutine.running()
|
|
25
|
+
table.insert(_waiting, _arg0)
|
|
26
|
+
coroutine.yield()
|
|
27
|
+
continue
|
|
28
|
+
end
|
|
29
|
+
local threads = {}
|
|
30
|
+
waiting = threads
|
|
31
|
+
-- Set by the build's thread, so widened: the checks below must not narrow them to their start.
|
|
32
|
+
local finished = false
|
|
33
|
+
local failure = nil
|
|
34
|
+
task.spawn(function()
|
|
35
|
+
local ok, result = pcall(build)
|
|
36
|
+
if ok then
|
|
37
|
+
value = result
|
|
38
|
+
else
|
|
39
|
+
failure = {
|
|
40
|
+
reason = result,
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
finished = true
|
|
44
|
+
waiting = nil
|
|
45
|
+
for _, thread in threads do
|
|
46
|
+
-- A waiter killed meanwhile, the starter included, is not resumed.
|
|
47
|
+
if coroutine.status(thread) == "suspended" then
|
|
48
|
+
task.spawn(thread)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end)
|
|
52
|
+
if not finished then
|
|
53
|
+
local _arg0 = coroutine.running()
|
|
54
|
+
table.insert(threads, _arg0)
|
|
55
|
+
coroutine.yield()
|
|
56
|
+
end
|
|
57
|
+
if failure then
|
|
58
|
+
error(failure.reason, 0)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
return value
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
return {
|
|
65
|
+
createOnce = createOnce,
|
|
66
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { SignalConnection } from "./signal";
|
|
1
2
|
export interface SignalContainer<T> {
|
|
2
3
|
fire<K extends keyof T>(name: K, ...args: Parameters<T[K]>): void;
|
|
3
|
-
connect<K extends keyof T>(name: K, callback: T[K]):
|
|
4
|
+
connect<K extends keyof T>(name: K, callback: T[K]): SignalConnection;
|
|
4
5
|
}
|
|
5
6
|
export declare function createSignalContainer<T>(): SignalContainer<T>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
-- Compiled with roblox-ts v3.0.0
|
|
2
2
|
local TS = _G[script]
|
|
3
|
-
local
|
|
3
|
+
local createSignal = TS.import(script, script.Parent, "signal").createSignal
|
|
4
4
|
local function createSignalContainer()
|
|
5
5
|
local signals = {}
|
|
6
6
|
return {
|
|
@@ -17,7 +17,7 @@ local function createSignalContainer()
|
|
|
17
17
|
local signal = signals[_name]
|
|
18
18
|
if not signal then
|
|
19
19
|
local _exp = name
|
|
20
|
-
signal =
|
|
20
|
+
signal = createSignal()
|
|
21
21
|
local _signal = signal
|
|
22
22
|
signals[_exp] = _signal
|
|
23
23
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A handler's connection, public as `Networking.Connection`. Shaped like an `RBXScriptConnection`
|
|
3
|
+
* (`Connected`, `Disconnect`), plus `Destroy` for maids and janitors; it is a table, not an engine
|
|
4
|
+
* connection.
|
|
5
|
+
*/
|
|
6
|
+
export interface SignalConnection {
|
|
7
|
+
readonly Connected: boolean;
|
|
8
|
+
Disconnect(): void;
|
|
9
|
+
Destroy(): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Networking's own signal (see `signal.luau`): arguments are passed by reference, each handler runs
|
|
14
|
+
* on a recycled thread of its own, newest connection first.
|
|
15
|
+
*/
|
|
16
|
+
export interface Signal<T extends Callback = Callback> {
|
|
17
|
+
Connect(callback: T): SignalConnection;
|
|
18
|
+
Fire(...args: Parameters<T>): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createSignal<T extends Callback = Callback>(): Signal<T>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Runs `callback(...args)` at once on a recycled thread: a yield inside it does not hold up the
|
|
25
|
+
* caller, and an error is printed rather than raised to it.
|
|
26
|
+
*/
|
|
27
|
+
export function spawn<A extends unknown[]>(callback: (...args: A) => unknown, ...args: A): void;
|