@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.
Files changed (44) hide show
  1. package/README.md +35 -27
  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 +3 -4
  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
@@ -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 whose successful results are already packed as `[payload, blobs?]`. */
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 whose successful results are already packed as `[payload, blobs?]`. */
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
- ): RBXScriptConnection;
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
- outgoing: IntrinsicObfuscate<{ [k in keyof Functions<S>]: Modding.Target.Guard<ReturnType<S[k]>> }>;
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, the results its callbacks return (so `predict`
258
- * can unpack them) and the responses to requests it sends. Requests and results are packed inline
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 Signal from "@rbxts/signal";
2
- import { NetworkInfo } from "./types";
3
-
4
- interface BaseEvent {
5
- /**
6
- * The event or function that was fired.
7
- */
8
- networkInfo: NetworkInfo;
9
- }
10
-
11
- interface BadRequestData extends BaseEvent {
12
- /**
13
- * The index of the argument that was incorrect, or -1 when a serialized payload could not be decoded.
14
- */
15
- argIndex: number;
16
-
17
- /**
18
- * The value of the argument that was incorrect, or the decoding error for a malformed payload.
19
- */
20
- argValue: unknown;
21
- }
22
-
23
- interface BadResponseData extends BaseEvent {
24
- /**
25
- * The index of the argument that was incorrect.
26
- */
27
- value: unknown;
28
- }
29
-
30
- export interface EventNetworkingEvents {
31
- onBadRequest: (player: Player, data: BadRequestData) => void;
32
- }
33
-
34
- export interface FunctionNetworkingEvents extends EventNetworkingEvents {
35
- onBadResponse: (player: Player, data: BadResponseData) => void;
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
- export type MiddlewareProcessor<I extends readonly unknown[], O> = (player?: Player, ...args: I) => Promise<O>;
5
- export type Middleware<I extends readonly unknown[] = unknown[], O = void> = (
6
- player?: Player,
7
- ...args: I
8
- ) => O | Promise<O>;
9
- export type MiddlewareFactory<I extends readonly unknown[] = [], O = void> = (
10
- processNext: MiddlewareProcessor<I, O>,
11
- event: NetworkInfo,
12
- ) => Middleware<I, O>;
13
-
14
- export type EventMiddleware<I extends readonly unknown[] = unknown[]> = MiddlewareFactory<I, void>;
15
- export type FunctionMiddleware<I extends readonly unknown[] = unknown[], O = void> = MiddlewareFactory<I, O | Skip>;
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]): RBXScriptConnection;
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 Signal = TS.import(script, TS.getModule(script, "@rbxts", "signal"))
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 = Signal.new()
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;