@flamework-experimental/networking 2.0.0-alpha.0 → 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
|
@@ -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;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
--[[
|
|
2
|
+
A signal of networking's own, and the recycled threads it and the receive pipeline run on.
|
|
3
|
+
|
|
4
|
+
Plain Luau rather than TypeScript: roblox-ts turns every `...args` into a table and spreads it with
|
|
5
|
+
`unpack`, which stops at the first nil it happens to land on. Here a vararg list keeps its count
|
|
6
|
+
from the remote to the handler, and nothing is allocated on the way.
|
|
7
|
+
|
|
8
|
+
Arguments are handed over by reference. A BindableEvent would copy them, turning a decoded
|
|
9
|
+
`Map<Instance, ...>` into one keyed by strings and raising on a `Set<boolean>`.
|
|
10
|
+
|
|
11
|
+
Like an engine signal, each handler runs on a thread of its own: one that yields does not hold up
|
|
12
|
+
the others, and one that raises has its error printed while the others still run. The newest
|
|
13
|
+
connection runs first, which is the order the engine runs a BindableEvent's handlers in; one made
|
|
14
|
+
during a fire does not receive it, and one disconnected before its turn is skipped.
|
|
15
|
+
]]
|
|
16
|
+
|
|
17
|
+
--- Threads that finished their last job, waiting in `coroutine.yield()` for the next.
|
|
18
|
+
local idle: { thread } = {}
|
|
19
|
+
|
|
20
|
+
--- How many finished threads are kept. More than a few are only ever needed while handlers yield;
|
|
21
|
+
--- past this, a thread that finishes simply ends.
|
|
22
|
+
local MAX_IDLE = 16
|
|
23
|
+
|
|
24
|
+
local function call(callback: (...any) -> ...any, ...: any)
|
|
25
|
+
callback(...)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
--[[
|
|
29
|
+
The body of a recycled thread: waits for a job, runs it, parks itself, and runs whatever it is
|
|
30
|
+
resumed with next. A job that raises ends the thread, and the error is printed by the scheduler
|
|
31
|
+
that resumed it; a job that yields keeps the thread until it returns, so it is only ever parked
|
|
32
|
+
when idle.
|
|
33
|
+
|
|
34
|
+
It takes no arguments, and every job arrives through `coroutine.yield()`: a job passed as the
|
|
35
|
+
thread's own arguments would stay in this frame's varargs for as long as the thread lives,
|
|
36
|
+
keeping its handler, sender and arguments reachable long after it returned.
|
|
37
|
+
]]
|
|
38
|
+
local function runner()
|
|
39
|
+
local thread = coroutine.running()
|
|
40
|
+
|
|
41
|
+
while true do
|
|
42
|
+
call(coroutine.yield())
|
|
43
|
+
|
|
44
|
+
if #idle >= MAX_IDLE then
|
|
45
|
+
return
|
|
46
|
+
end
|
|
47
|
+
table.insert(idle, thread)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
--[[
|
|
52
|
+
Runs `callback(...)` at once on a thread of its own, recycling one that finished its last job. An
|
|
53
|
+
idle thread that was killed meanwhile (`task.cancel`) is dropped rather than resumed.
|
|
54
|
+
]]
|
|
55
|
+
local function spawn(callback: (...any) -> ...any, ...: any)
|
|
56
|
+
local count = #idle
|
|
57
|
+
while count > 0 do
|
|
58
|
+
local thread = idle[count]
|
|
59
|
+
idle[count] = nil
|
|
60
|
+
count -= 1
|
|
61
|
+
|
|
62
|
+
if coroutine.status(thread) == "suspended" then
|
|
63
|
+
task.spawn(thread, callback, ...)
|
|
64
|
+
return
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
-- A new thread, started up to its first `coroutine.yield()`, gets its first job the same way.
|
|
69
|
+
local thread = coroutine.create(runner)
|
|
70
|
+
coroutine.resume(thread)
|
|
71
|
+
task.spawn(thread, callback, ...)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
local Connection = {}
|
|
75
|
+
Connection.__index = Connection
|
|
76
|
+
|
|
77
|
+
--- Disconnects the handler. Takes effect at once, even for a fire under way.
|
|
78
|
+
function Connection:Disconnect()
|
|
79
|
+
if not self.Connected then
|
|
80
|
+
return
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
self.Connected = false
|
|
84
|
+
self._callback = nil
|
|
85
|
+
|
|
86
|
+
-- Copied rather than edited in place: a fire under way keeps walking the list it started with.
|
|
87
|
+
local signal = self._signal
|
|
88
|
+
local connections = signal._connections
|
|
89
|
+
local index = table.find(connections, self)
|
|
90
|
+
if index ~= nil then
|
|
91
|
+
local remaining = table.clone(connections)
|
|
92
|
+
table.remove(remaining, index)
|
|
93
|
+
signal._connections = remaining
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
--- The name maids and janitors call.
|
|
98
|
+
Connection.Destroy = Connection.Disconnect
|
|
99
|
+
|
|
100
|
+
local Signal = {}
|
|
101
|
+
Signal.__index = Signal
|
|
102
|
+
|
|
103
|
+
function Signal:Connect(callback: (...any) -> ...any)
|
|
104
|
+
local connection = setmetatable({
|
|
105
|
+
Connected = true,
|
|
106
|
+
_callback = callback,
|
|
107
|
+
_signal = self,
|
|
108
|
+
}, Connection)
|
|
109
|
+
|
|
110
|
+
-- Newest first, copied so that a fire under way does not reach it.
|
|
111
|
+
local connections = self._connections
|
|
112
|
+
local updated = table.create(#connections + 1)
|
|
113
|
+
updated[1] = connection
|
|
114
|
+
table.move(connections, 1, #connections, 2, updated)
|
|
115
|
+
self._connections = updated
|
|
116
|
+
|
|
117
|
+
return connection
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
function Signal:Fire(...: any)
|
|
121
|
+
for _, connection in self._connections do
|
|
122
|
+
if connection.Connected then
|
|
123
|
+
spawn(connection._callback, ...)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
local function createSignal()
|
|
129
|
+
return setmetatable({ _connections = {} }, Signal)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
createSignal = createSignal,
|
|
134
|
+
spawn = spawn,
|
|
135
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `args` cut after its last value, so that spreading it passes every argument.
|
|
3
|
+
*
|
|
4
|
+
* roblox-ts spreads a list as `unpack(list)`, which stops at `#list`. When the last slot is nil, `#`
|
|
5
|
+
* may stop at any earlier gap: `#{1, nil, nil, 4, nil}` can be 1, which drops the 4. A list that ends
|
|
6
|
+
* in a value always spreads whole. The `{ ... }` that roblox-ts rebuilds from that spread at the next
|
|
7
|
+
* hop also ends in a value, and so does every hop after it that passes the list on. So a list is
|
|
8
|
+
* trimmed where it enters, and again wherever code may build its own: a middleware that names its
|
|
9
|
+
* parameters passes on a list that can end in nil. Nothing is lost by trimming: an argument that was
|
|
10
|
+
* not passed reads as nil too.
|
|
11
|
+
*/
|
|
12
|
+
export declare function trimArguments<T extends unknown[]>(args: T): T;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
--[[
|
|
3
|
+
*
|
|
4
|
+
* `args` cut after its last value, so that spreading it passes every argument.
|
|
5
|
+
*
|
|
6
|
+
* roblox-ts spreads a list as `unpack(list)`, which stops at `#list`. When the last slot is nil, `#`
|
|
7
|
+
* may stop at any earlier gap: `#{1, nil, nil, 4, nil}` can be 1, which drops the 4. A list that ends
|
|
8
|
+
* in a value always spreads whole. The `{ ... }` that roblox-ts rebuilds from that spread at the next
|
|
9
|
+
* hop also ends in a value, and so does every hop after it that passes the list on. So a list is
|
|
10
|
+
* trimmed where it enters, and again wherever code may build its own: a middleware that names its
|
|
11
|
+
* parameters passes on a list that can end in nil. Nothing is lost by trimming: an argument that was
|
|
12
|
+
* not passed reads as nil too.
|
|
13
|
+
|
|
14
|
+
]]
|
|
15
|
+
local function trimArguments(args)
|
|
16
|
+
-- `pairs` visits every value, holes or not. Read as a map, the list gives the raw Luau (1-based)
|
|
17
|
+
-- index, which is the length up to that value.
|
|
18
|
+
local length = 0
|
|
19
|
+
for index in pairs(args) do
|
|
20
|
+
if index > length then
|
|
21
|
+
length = index
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
if #args == length then
|
|
25
|
+
return args
|
|
26
|
+
end
|
|
27
|
+
-- Sized up front, so that the last slot is the last value and `#` is exact.
|
|
28
|
+
local trimmed = table.create(length)
|
|
29
|
+
do
|
|
30
|
+
local i = 0
|
|
31
|
+
local _shouldIncrement = false
|
|
32
|
+
while true do
|
|
33
|
+
if _shouldIncrement then
|
|
34
|
+
i += 1
|
|
35
|
+
else
|
|
36
|
+
_shouldIncrement = true
|
|
37
|
+
end
|
|
38
|
+
if not (i < length) then
|
|
39
|
+
break
|
|
40
|
+
end
|
|
41
|
+
trimmed[i + 1] = args[i + 1]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
return trimmed
|
|
45
|
+
end
|
|
46
|
+
return {
|
|
47
|
+
trimArguments = trimArguments,
|
|
48
|
+
}
|