@axpecter/lync 2.3.3 → 3.0.0

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 (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +302 -299
  3. package/Types.luau +215 -0
  4. package/api/Define.luau +154 -0
  5. package/api/Lifecycle.luau +119 -0
  6. package/api/Packet.luau +108 -0
  7. package/api/Query.luau +149 -0
  8. package/api/Set.luau +189 -0
  9. package/channel/Blocks.luau +124 -0
  10. package/channel/Group.luau +118 -0
  11. package/channel/Link.luau +135 -0
  12. package/channel/Outbox.luau +261 -0
  13. package/channel/Recipients.luau +129 -0
  14. package/channel/Transport.luau +138 -0
  15. package/codec/Base.luau +236 -0
  16. package/codec/Composites.luau +128 -0
  17. package/codec/Datatypes.luau +162 -0
  18. package/codec/Legality.luau +61 -0
  19. package/codec/Scalars.luau +72 -0
  20. package/codec/Text.luau +89 -0
  21. package/codec/init.luau +39 -0
  22. package/index.d.ts +285 -0
  23. package/init.luau +91 -0
  24. package/kernel/Bits.luau +16 -0
  25. package/kernel/Catalog.luau +121 -0
  26. package/kernel/Heap.luau +80 -0
  27. package/kernel/Listeners.luau +95 -0
  28. package/kernel/Log.luau +197 -0
  29. package/kernel/Tally.luau +128 -0
  30. package/kernel/Threads.luau +44 -0
  31. package/kernel/Wide.luau +267 -0
  32. package/package.json +15 -32
  33. package/runtime/Boundary.luau +180 -0
  34. package/runtime/Dispatch.luau +91 -0
  35. package/runtime/Handles.luau +114 -0
  36. package/runtime/Handshake.luau +90 -0
  37. package/runtime/Pending.luau +196 -0
  38. package/runtime/Router.luau +379 -0
  39. package/runtime/Session.luau +731 -0
  40. package/shape/Compile.luau +340 -0
  41. package/shape/Folds.luau +332 -0
  42. package/shape/Frames.luau +170 -0
  43. package/shape/Grid.luau +169 -0
  44. package/shape/Ir.luau +540 -0
  45. package/shape/Joins.luau +125 -0
  46. package/shape/Legality.luau +273 -0
  47. package/shape/Lower.luau +419 -0
  48. package/shape/Order.luau +64 -0
  49. package/shape/Regions.luau +113 -0
  50. package/shape/Rotation.luau +472 -0
  51. package/shape/Serial.luau +258 -0
  52. package/shape/Words.luau +89 -0
  53. package/shape/emit/Block.luau +453 -0
  54. package/shape/emit/Column.luau +338 -0
  55. package/shape/emit/Leaf.luau +462 -0
  56. package/shape/emit/Word.luau +234 -0
  57. package/shape/emit/init.luau +222 -0
  58. package/shape/init.luau +78 -0
  59. package/state/Apply.luau +330 -0
  60. package/state/Audience.luau +126 -0
  61. package/state/Catchup.luau +218 -0
  62. package/state/Coalesce.luau +43 -0
  63. package/state/Fieldset.luau +177 -0
  64. package/state/Newest.luau +339 -0
  65. package/state/Plan.luau +441 -0
  66. package/state/Sections.luau +468 -0
  67. package/state/Store.luau +188 -0
  68. package/state/Sync.luau +762 -0
  69. package/wire/Bytes.luau +118 -0
  70. package/wire/Clock.luau +37 -0
  71. package/wire/Protocol.luau +110 -0
  72. package/wire/Sequence.luau +40 -0
  73. package/wire/Varint.luau +124 -0
  74. package/src/Types.luau +0 -180
  75. package/src/api/Group.luau +0 -156
  76. package/src/api/Packet.luau +0 -317
  77. package/src/api/Query.luau +0 -338
  78. package/src/api/Scope.luau +0 -90
  79. package/src/api/Signal.luau +0 -193
  80. package/src/codec/Base.luau +0 -136
  81. package/src/codec/composite/Array.luau +0 -473
  82. package/src/codec/composite/Map.luau +0 -485
  83. package/src/codec/composite/Optional.luau +0 -54
  84. package/src/codec/composite/Shared.luau +0 -311
  85. package/src/codec/composite/Struct.luau +0 -494
  86. package/src/codec/composite/Tagged.luau +0 -103
  87. package/src/codec/composite/Tuple.luau +0 -143
  88. package/src/codec/datatype/Buffer.luau +0 -66
  89. package/src/codec/datatype/CFrame.luau +0 -109
  90. package/src/codec/datatype/Color.luau +0 -32
  91. package/src/codec/datatype/Instance.luau +0 -52
  92. package/src/codec/datatype/IntVector.luau +0 -39
  93. package/src/codec/datatype/NumberRange.luau +0 -39
  94. package/src/codec/datatype/Ray.luau +0 -36
  95. package/src/codec/datatype/Rect.luau +0 -31
  96. package/src/codec/datatype/Region.luau +0 -62
  97. package/src/codec/datatype/Sequence.luau +0 -184
  98. package/src/codec/datatype/String.luau +0 -94
  99. package/src/codec/datatype/UDim.luau +0 -46
  100. package/src/codec/datatype/Vector.luau +0 -130
  101. package/src/codec/meta/Auto.luau +0 -318
  102. package/src/codec/meta/Bitfield.luau +0 -160
  103. package/src/codec/meta/Custom.luau +0 -33
  104. package/src/codec/meta/DeltaScalar.luau +0 -390
  105. package/src/codec/meta/Enum.luau +0 -50
  106. package/src/codec/meta/Float.luau +0 -38
  107. package/src/codec/meta/Nothing.luau +0 -13
  108. package/src/codec/meta/Unknown.luau +0 -41
  109. package/src/codec/primitive/Bool.luau +0 -26
  110. package/src/codec/primitive/Float16.luau +0 -99
  111. package/src/codec/primitive/Int.luau +0 -59
  112. package/src/codec/primitive/Number.luau +0 -24
  113. package/src/codec/primitive/Signed.luau +0 -32
  114. package/src/codec/primitive/Varint.luau +0 -165
  115. package/src/codec/primitive/Zint.luau +0 -99
  116. package/src/index.d.ts +0 -423
  117. package/src/init.luau +0 -362
  118. package/src/internal/Baseline.luau +0 -57
  119. package/src/internal/Channel.luau +0 -484
  120. package/src/internal/Middleware.luau +0 -100
  121. package/src/internal/Pool.luau +0 -66
  122. package/src/internal/Registry.luau +0 -119
  123. package/src/internal/Transport.luau +0 -28
  124. package/src/transport/Bridge.luau +0 -110
  125. package/src/transport/Client.luau +0 -97
  126. package/src/transport/Gate.luau +0 -488
  127. package/src/transport/Reader.luau +0 -331
  128. package/src/transport/Server.luau +0 -233
  129. package/src/util/Array.luau +0 -18
  130. package/src/util/Buffer.luau +0 -90
  131. package/src/util/Constants.luau +0 -30
  132. package/src/util/Log.luau +0 -68
  133. package/src/util/Player.luau +0 -14
  134. package/src/util/Quantize.luau +0 -84
  135. package/src/util/Quat.luau +0 -124
@@ -1,338 +0,0 @@
1
- --!strict
2
- --!optimize 2
3
- -- Query definition: handle, request with correlation IDs and timeout.
4
-
5
- local Players = game:GetService("Players")
6
- local RunService = game:GetService("RunService")
7
-
8
- local Channel = require(script.Parent.Parent.internal.Channel)
9
- local Log = require(script.Parent.Parent.util.Log)
10
- local Player = require(script.Parent.Parent.util.Player)
11
- local Registry = require(script.Parent.Parent.internal.Registry)
12
- local Transport = require(script.Parent.Parent.internal.Transport)
13
- local Types = require(script.Parent.Parent.Types)
14
-
15
- -- Constants --------------------------------------------------------------
16
-
17
- local IS_SERVER = RunService:IsServer()
18
- local DEFAULT_TIMEOUT = 5
19
-
20
- local EMPTY_OPTIONS: Types.QueryOptions = table.freeze({})
21
-
22
- -- State ------------------------------------------------------------------
23
-
24
- local _nextCorrId = 0
25
- -- Correlation -> waiting thread / completion closure. Cleared on resolve or timeout.
26
- local _pending: { [number]: thread } = {}
27
- local _pendingGather: { [number]: (any) -> () } = {}
28
-
29
- -- Private ----------------------------------------------------------------
30
-
31
- local isPlayer = Player.is
32
-
33
- local function nextCorr(): number
34
- _nextCorrId += 1
35
- return _nextCorrId
36
- end
37
-
38
- local function writeQueryTracked(
39
- ch: Types.ChannelState,
40
- reg: Types.Registration,
41
- corrId: number,
42
- codec: Types.InternalCodec<any>?,
43
- data: any
44
- ): ()
45
- if Channel.statsEnabled() then
46
- local before = ch.cursor
47
- Channel.writeQuery(ch, reg.id, corrId, codec, data, reg.name)
48
- reg.bytesSent += ch.cursor - before
49
- else
50
- Channel.writeQuery(ch, reg.id, corrId, codec, data, reg.name)
51
- end
52
- end
53
-
54
- local function collectTargets(target: any): { Player }
55
- local targets: { Player } = {}
56
- if typeof(target) ~= "table" then
57
- return targets
58
- end
59
-
60
- local kind = target._lyncKind
61
- if kind == "all" then
62
- for _, p in Players:GetPlayers() do
63
- table.insert(targets, p)
64
- end
65
- elseif kind == "group" then
66
- for p in target._members do
67
- table.insert(targets, p)
68
- end
69
- else
70
- for _, p in target do
71
- if isPlayer(p) then
72
- table.insert(targets, p :: Player)
73
- end
74
- end
75
- end
76
- return targets
77
- end
78
-
79
- -- Public types -----------------------------------------------------------
80
-
81
- --[[
82
- Server `request(data, target)` returns `Resp?` for a Player target and
83
- `{ [Player]: Resp? }` for a multi-target table; Luau lacks overloads on
84
- table fields, so the type advertises `Resp?` and multi-target callers cast.
85
- ]]
86
- export type QueryHandle<Req, Resp> = {
87
- handle: (
88
- self: QueryHandle<Req, Resp>,
89
- fn: (request: Req, player: Player?) -> Resp?
90
- ) -> Types.Connection,
91
- request: (self: QueryHandle<Req, Resp>, data: Req, target: any?) -> Resp?,
92
- name: (self: QueryHandle<Req, Resp>) -> string,
93
- stats: (self: QueryHandle<Req, Resp>) -> Types.PacketStats,
94
- }
95
-
96
- -- Public -----------------------------------------------------------------
97
-
98
- local Query = {}
99
-
100
- function Query.define<Req, Resp>(
101
- name: string,
102
- requestCodec: Types.InternalCodec<Req>,
103
- responseCodec: Types.InternalCodec<Resp>,
104
- options: Types.QueryOptions?
105
- ): QueryHandle<Req, Resp>
106
- local opts = options or EMPTY_OPTIONS
107
- local timeout = opts.timeout or DEFAULT_TIMEOUT
108
-
109
- local reqOpenFn = Channel.resolveOpenFn(Channel.TS_NONE, name)
110
- -- Embedded "\0resp" suffix stays uniquely paired with the request reg.
111
- local respName = `{name}\0resp`
112
- local respOpenFn = Channel.resolveOpenFn(Channel.TS_NONE, respName)
113
-
114
- local reqReg = Registry.register({
115
- name = name,
116
- kind = Registry.KIND_REQUEST,
117
- codec = requestCodec,
118
- isUnreliable = false,
119
- partner = nil,
120
- needsGate = IS_SERVER,
121
- headerSize = 1,
122
- timestampMode = Channel.TS_NONE,
123
- openFn = reqOpenFn,
124
- rateLimit = opts.rateLimit or { maxPerSecond = 30 },
125
- validate = opts.validate,
126
- maxPayloadBytes = nil,
127
- })
128
-
129
- local respReg = Registry.register({
130
- name = respName,
131
- kind = Registry.KIND_RESPONSE,
132
- codec = responseCodec,
133
- isUnreliable = false,
134
- partner = reqReg.id,
135
- needsGate = false,
136
- headerSize = 1,
137
- timestampMode = Channel.TS_NONE,
138
- openFn = respOpenFn,
139
- rateLimit = nil,
140
- validate = nil,
141
- maxPayloadBytes = nil,
142
- })
143
-
144
- reqReg.partner = respReg.id
145
-
146
- -- Single active handler; replacing it disconnects the prior one via handlerToken.
147
- local activeHandler: ((any, Player?) -> any)? = nil
148
- local handlerToken = 0
149
-
150
- respReg.signal:connect(function(value: any, _sender: Player?, corrId: number)
151
- local single = _pending[corrId]
152
- if single then
153
- _pending[corrId] = nil
154
- task.spawn(single, value)
155
- return
156
- end
157
- local gather = _pendingGather[corrId]
158
- if gather then
159
- _pendingGather[corrId] = nil
160
- gather(value)
161
- end
162
- end)
163
-
164
- reqReg.signal:connect(function(value: any, sender: Player?, corrId: number)
165
- local handler = activeHandler
166
- if not handler then
167
- return
168
- end
169
-
170
- local ok, response = pcall(handler, value, sender)
171
- if not ok then
172
- Log.warn(`query "{name}" handler errored: {response}`)
173
- end
174
- -- pcall failure path returns nil response (writeQuery encodes a nil-MSB header).
175
- local payloadCodec = if ok then responseCodec else nil
176
- local payloadData = if ok then response else nil
177
-
178
- local ch: Types.ChannelState? = if IS_SERVER
179
- then (if sender then Transport.server().getChannel(sender, false) else nil)
180
- else Transport.client().getChannel(false)
181
- if ch then
182
- writeQueryTracked(ch, respReg, corrId, payloadCodec, payloadData)
183
- end
184
- end)
185
-
186
- -- Resolve the waiting thread with nil after `timeout` seconds.
187
- local function scheduleSingleTimeout(corrId: number, running: thread): ()
188
- task.delay(timeout, function()
189
- if _pending[corrId] == running then
190
- _pending[corrId] = nil
191
- task.spawn(running, nil)
192
- end
193
- end)
194
- end
195
-
196
- -- Issue a single-target query on `ch` and yield until reply or timeout.
197
- local function requestOnChannel(ch: Types.ChannelState, data: any): any
198
- local corrId = nextCorr()
199
- writeQueryTracked(ch, reqReg, corrId, requestCodec, data)
200
-
201
- local running = coroutine.running()
202
- _pending[corrId] = running
203
- scheduleSingleTimeout(corrId, running)
204
- return coroutine.yield()
205
- end
206
-
207
- local function requestMulti(targets: { Player }, data: any): { [Player]: any }
208
- local targetCount = #targets
209
- if targetCount == 0 then
210
- return {}
211
- end
212
-
213
- local results: { [Player]: any } = {}
214
- local remaining = targetCount
215
- local running = coroutine.running()
216
- local resolved = false
217
- local corrIds = table.create(targetCount)
218
-
219
- --[[
220
- On timeout, surface the partial result table AND evict every
221
- still-pending closure. Without eviction each unanswered closure
222
- keeps `results`/`remaining`/`resolveOnce` alive: a slow leak.
223
- ]]
224
- local function resolveOnce(): ()
225
- if resolved then
226
- return
227
- end
228
- resolved = true
229
- for i = 1, targetCount do
230
- _pendingGather[corrIds[i]] = nil
231
- end
232
- task.spawn(running, results)
233
- end
234
-
235
- local Server = Transport.server()
236
- for i, p in targets do
237
- local pCorr = nextCorr()
238
- corrIds[i] = pCorr
239
- local ch = Server.getChannel(p, false)
240
- writeQueryTracked(ch, reqReg, pCorr, requestCodec, data)
241
-
242
- local pRef = p
243
- _pendingGather[pCorr] = function(response: any)
244
- results[pRef] = response
245
- remaining -= 1
246
- if remaining == 0 then
247
- resolveOnce()
248
- end
249
- end
250
- end
251
-
252
- task.delay(timeout, resolveOnce)
253
- return coroutine.yield()
254
- end
255
-
256
- local function requestFromServer(data: any, target: any): any
257
- if target == nil then
258
- Log.error(`"{name}" server request requires a target`)
259
- end
260
- if isPlayer(target) then
261
- return requestOnChannel(Transport.server().getChannel(target :: Player, false), data)
262
- end
263
- return requestMulti(collectTargets(target), data)
264
- end
265
-
266
- local function requestFromClient(data: any): any
267
- return requestOnChannel(Transport.client().getChannel(false), data)
268
- end
269
-
270
- type Self = QueryHandle<Req, Resp>
271
-
272
- local handle = {
273
- --[[
274
- Replace the active handler. The returned Connection only clears
275
- the active slot if no later :handle has overwritten it: token
276
- comparison guards against a stale disconnect wiping a fresh handler.
277
- ]]
278
- handle = function(
279
- _self: Self,
280
- fn: (request: Req, player: Player?) -> Resp?
281
- ): Types.Connection
282
- handlerToken += 1
283
- local myToken = handlerToken
284
- activeHandler = fn :: any
285
-
286
- local conn: Types.Connection
287
- conn = {
288
- connected = true,
289
- disconnect = function(self): ()
290
- if not self.connected then
291
- return
292
- end
293
- self.connected = false
294
- if handlerToken == myToken then
295
- activeHandler = nil
296
- end
297
- end,
298
- }
299
- return conn
300
- end,
301
-
302
- request = function(_self: Self, data: Req, target: any?): Resp?
303
- if IS_SERVER then
304
- return requestFromServer(data, target)
305
- end
306
- return requestFromClient(data)
307
- end,
308
-
309
- name = function(_self: Self): string
310
- return name
311
- end,
312
-
313
- stats = function(_self: Self): Types.PacketStats
314
- return {
315
- bytesSent = reqReg.bytesSent + respReg.bytesSent,
316
- bytesReceived = reqReg.bytesReceived + respReg.bytesReceived,
317
- fires = reqReg.fires + respReg.fires,
318
- recvFires = reqReg.recvFires + respReg.recvFires,
319
- drops = reqReg.drops + respReg.drops,
320
- }
321
- end,
322
- }
323
-
324
- return table.freeze(handle) :: QueryHandle<Req, Resp>
325
- end
326
-
327
- function Query.pendingCount(): number
328
- local count = 0
329
- for _ in _pending do
330
- count += 1
331
- end
332
- for _ in _pendingGather do
333
- count += 1
334
- end
335
- return count
336
- end
337
-
338
- return table.freeze(Query)
@@ -1,90 +0,0 @@
1
- --!strict
2
- --!optimize 2
3
- -- Batched connection lifecycle for Lync, RBXScriptConnection, and disconnect()-style objects.
4
-
5
- local Types = require(script.Parent.Parent.Types)
6
-
7
- -- Public types -----------------------------------------------------------
8
-
9
- export type ScopeHandle = {
10
- on: (self: ScopeHandle, source: any, fn: (...any) -> ()) -> Types.Connection,
11
- once: (self: ScopeHandle, source: any, fn: (...any) -> ()) -> Types.Connection,
12
- add: (self: ScopeHandle, connection: any) -> (),
13
- size: (self: ScopeHandle) -> number,
14
- destroy: (self: ScopeHandle) -> (),
15
- }
16
-
17
- type Scope = ScopeHandle & { _connections: { any }, _count: number }
18
-
19
- -- Private ----------------------------------------------------------------
20
-
21
- local function pushConnection(self: Scope, conn: any): ()
22
- local idx = self._count + 1
23
- self._count = idx
24
- self._connections[idx] = conn
25
- end
26
-
27
- -- Routes :on / :once through `methodName` so both call sites share insert + size bookkeeping.
28
- local function attach(
29
- self: Scope,
30
- source: any,
31
- fn: (...any) -> (),
32
- methodName: string
33
- ): Types.Connection
34
- local conn = source[methodName](source, fn)
35
- pushConnection(self, conn)
36
- return conn
37
- end
38
-
39
- -- Public -----------------------------------------------------------------
40
-
41
- local ScopeMeta = {}
42
- ScopeMeta.__index = ScopeMeta
43
-
44
- function ScopeMeta.on(self: Scope, source: any, fn: (...any) -> ()): Types.Connection
45
- return attach(self, source, fn, "on")
46
- end
47
-
48
- function ScopeMeta.once(self: Scope, source: any, fn: (...any) -> ()): Types.Connection
49
- return attach(self, source, fn, "once")
50
- end
51
-
52
- ScopeMeta.add = pushConnection
53
-
54
- function ScopeMeta.size(self: Scope): number
55
- return self._count
56
- end
57
-
58
- --[[
59
- Disconnect every tracked source. The shape sniff handles three kinds:
60
- Roblox RBXScriptConnection (:Disconnect), Lync Connection (:disconnect),
61
- and tables exposing either capitalization.
62
- ]]
63
- function ScopeMeta.destroy(self: Scope): ()
64
- local conns = self._connections
65
- local count = self._count
66
- for i = 1, count do
67
- local conn = conns[i]
68
- conns[i] = nil
69
- if typeof(conn) == "RBXScriptConnection" then
70
- (conn :: RBXScriptConnection):Disconnect()
71
- elseif typeof(conn) == "table" then
72
- if conn.disconnect then
73
- conn:disconnect()
74
- elseif conn.Disconnect then
75
- conn:Disconnect()
76
- end
77
- end
78
- end
79
- self._count = 0
80
- end
81
-
82
- table.freeze(ScopeMeta)
83
-
84
- local ScopeModule = {}
85
-
86
- function ScopeModule.create(): ScopeHandle
87
- return (setmetatable({ _connections = {}, _count = 0 }, ScopeMeta) :: any) :: ScopeHandle
88
- end
89
-
90
- return table.freeze(ScopeModule)
@@ -1,193 +0,0 @@
1
- --!strict
2
- --!optimize 2
3
- -- O(1) disconnect signal with thread reuse and reentrancy-safe fire.
4
-
5
- local Types = require(script.Parent.Parent.Types)
6
-
7
- -- Constants --------------------------------------------------------------
8
-
9
- local STATE_DISCONNECTED = 0
10
- local STATE_CONNECTED = 1
11
- local STATE_ONCE = 2
12
-
13
- -- State ------------------------------------------------------------------
14
-
15
- -- Single recyclable thread for async dispatch; saves coroutine.create per fire.
16
- local _freeThread: thread? = nil
17
-
18
- -- Private ----------------------------------------------------------------
19
-
20
- -- pcall wrapper: a handler error must not consume the recycled thread, otherwise
21
- -- every subsequent fire pays a coroutine.create until process restart.
22
- local function passer(fn: (...any) -> (), ...): ()
23
- local thread = _freeThread
24
- _freeThread = nil
25
- local ok, err = pcall(fn, ...)
26
- _freeThread = thread
27
- if not ok then
28
- task.spawn(error, err)
29
- end
30
- end
31
-
32
- local function yielder(): ()
33
- while true do
34
- passer(coroutine.yield())
35
- end
36
- end
37
-
38
- local function spawnCallback(fn: (...any) -> (), ...: any): ()
39
- if not _freeThread then
40
- _freeThread = coroutine.create(yielder)
41
- coroutine.resume(_freeThread :: thread)
42
- end
43
- task.spawn(_freeThread :: thread, fn, ...)
44
- end
45
-
46
- type Entry = {
47
- fn: (...any) -> (),
48
- _state: number,
49
- connected: boolean,
50
- _signal: SignalFields,
51
- _index: number,
52
- }
53
-
54
- type SignalFields = {
55
- _entries: { Entry },
56
- _count: number,
57
- }
58
-
59
- local EntryImpl = {}
60
- EntryImpl.__index = EntryImpl
61
-
62
- -- Swap-with-last for O(1) disconnect; the moved entry's _index is patched too.
63
- local function disconnectEntry(entry: Entry): ()
64
- if entry._state == STATE_DISCONNECTED then
65
- return
66
- end
67
-
68
- entry._state = STATE_DISCONNECTED
69
- entry.connected = false
70
-
71
- local signal = entry._signal
72
- local entries = signal._entries
73
- local idx = entry._index
74
- local last = signal._count
75
-
76
- if idx < last then
77
- local moved = entries[last]
78
- entries[idx] = moved
79
- moved._index = idx
80
- end
81
- entries[last] = nil
82
- signal._count = last - 1
83
- end
84
-
85
- EntryImpl.disconnect = disconnectEntry
86
- table.freeze(EntryImpl)
87
-
88
- -- Auto-disconnects ONCE entries before invoking; chooses sync vs spawned thread.
89
- local function invokeEntry(entry: Entry, sync: boolean, ...: any): ()
90
- local state = entry._state
91
- if state == STATE_DISCONNECTED then
92
- return
93
- end
94
- if state == STATE_ONCE then
95
- disconnectEntry(entry)
96
- end
97
- if sync then
98
- entry.fn(...)
99
- else
100
- spawnCallback(entry.fn, ...)
101
- end
102
- end
103
-
104
- local SignalImpl = {}
105
- SignalImpl.__index = SignalImpl
106
-
107
- local function newEntry(self: SignalFields, callback: (...any) -> (), state: number): Entry
108
- local idx = self._count + 1
109
- self._count = idx
110
-
111
- local entry: Entry = setmetatable({
112
- fn = callback,
113
- _state = state,
114
- connected = true,
115
- _signal = self,
116
- _index = idx,
117
- }, EntryImpl) :: any
118
-
119
- self._entries[idx] = entry
120
- return entry
121
- end
122
-
123
- function SignalImpl.connect(self: SignalFields, callback: (...any) -> ()): Types.Connection
124
- return newEntry(self, callback, STATE_CONNECTED) :: any
125
- end
126
-
127
- function SignalImpl.once(self: SignalFields, callback: (...any) -> ()): Types.Connection
128
- return newEntry(self, callback, STATE_ONCE) :: any
129
- end
130
-
131
- function SignalImpl.wait(self: SignalFields): ...any
132
- local running = coroutine.running()
133
- local conn: Types.Connection
134
- conn = SignalImpl.connect(self, function(...: any)
135
- conn:disconnect()
136
- task.spawn(running, ...)
137
- end)
138
- return coroutine.yield()
139
- end
140
-
141
- local function dispatch(self: SignalFields, sync: boolean, ...: any): ()
142
- local count = self._count
143
- if count == 0 then
144
- return
145
- end
146
-
147
- local entries = self._entries
148
-
149
- -- Single-listener fast path skips the snapshot allocation.
150
- if count == 1 then
151
- invokeEntry(entries[1], sync, ...)
152
- return
153
- end
154
-
155
- --[[
156
- Re-entrancy: a handler may disconnect or add others mid-fire.
157
- Snapshot the entry list so the iteration view is stable.
158
- ]]
159
- local snapshot = table.create(count)
160
- table.move(entries, 1, count, 1, snapshot)
161
-
162
- for i = 1, count do
163
- invokeEntry(snapshot[i], sync, ...)
164
- end
165
- end
166
-
167
- function SignalImpl.fire(self: SignalFields, ...: any): ()
168
- dispatch(self, false, ...)
169
- end
170
-
171
- function SignalImpl.fireSync(self: SignalFields, ...: any): ()
172
- dispatch(self, true, ...)
173
- end
174
-
175
- function SignalImpl.count(self: SignalFields): number
176
- return self._count
177
- end
178
-
179
- function SignalImpl.hasListeners(self: SignalFields): boolean
180
- return self._count > 0
181
- end
182
-
183
- table.freeze(SignalImpl)
184
-
185
- -- Public -----------------------------------------------------------------
186
-
187
- local SignalModule = {}
188
-
189
- function SignalModule.create(): Types.SignalLike
190
- return (setmetatable({ _entries = {}, _count = 0 }, SignalImpl) :: any) :: Types.SignalLike
191
- end
192
-
193
- return table.freeze(SignalModule)