@axpecter/lync 2.2.1 → 2.3.1
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 +95 -143
- package/package.json +1 -1
- package/src/Types.luau +22 -16
- package/src/api/Group.luau +40 -33
- package/src/api/Packet.luau +68 -54
- package/src/api/Query.luau +103 -65
- package/src/api/Scope.luau +26 -10
- package/src/api/Signal.luau +43 -52
- package/src/codec/Base.luau +21 -14
- package/src/codec/composite/Array.luau +56 -134
- package/src/codec/composite/Map.luau +103 -72
- package/src/codec/composite/Optional.luau +6 -2
- package/src/codec/composite/Shared.luau +160 -69
- package/src/codec/composite/Struct.luau +283 -42
- package/src/codec/composite/Tagged.luau +13 -16
- package/src/codec/composite/Tuple.luau +21 -15
- package/src/codec/datatype/Buffer.luau +6 -4
- package/src/codec/datatype/CFrame.luau +56 -44
- package/src/codec/datatype/Color.luau +10 -10
- package/src/codec/datatype/Instance.luau +15 -12
- package/src/codec/datatype/IntVector.luau +2 -2
- package/src/codec/datatype/NumberRange.luau +15 -8
- package/src/codec/datatype/Ray.luau +13 -14
- package/src/codec/datatype/Rect.luau +12 -12
- package/src/codec/datatype/Region.luau +13 -14
- package/src/codec/datatype/Sequence.luau +87 -65
- package/src/codec/datatype/String.luau +17 -10
- package/src/codec/datatype/UDim.luau +10 -8
- package/src/codec/datatype/Vector.luau +20 -59
- package/src/codec/meta/Auto.luau +94 -126
- package/src/codec/meta/Bitfield.luau +12 -14
- package/src/codec/meta/Custom.luau +3 -1
- package/src/codec/meta/Enum.luau +9 -9
- package/src/codec/meta/Float.luau +6 -28
- package/src/codec/meta/Nothing.luau +1 -1
- package/src/codec/meta/Unknown.luau +10 -7
- package/src/codec/primitive/Bool.luau +6 -4
- package/src/codec/primitive/Float16.luau +5 -2
- package/src/codec/primitive/Int.luau +5 -6
- package/src/codec/primitive/Number.luau +6 -4
- package/src/codec/primitive/Signed.luau +2 -2
- package/src/codec/primitive/Varint.luau +69 -38
- package/src/index.d.ts +161 -53
- package/src/init.luau +109 -103
- package/src/internal/Baseline.luau +9 -1
- package/src/internal/Channel.luau +153 -154
- package/src/internal/Middleware.luau +22 -6
- package/src/internal/Pool.luau +12 -4
- package/src/internal/Registry.luau +25 -16
- package/src/internal/Transport.luau +1 -1
- package/src/transport/Bridge.luau +47 -33
- package/src/transport/Client.luau +25 -21
- package/src/transport/Gate.luau +227 -172
- package/src/transport/Reader.luau +162 -105
- package/src/transport/Server.luau +46 -57
- package/src/util/Array.luau +18 -0
- package/src/util/Buffer.luau +92 -0
- package/src/util/Constants.luau +30 -0
- package/src/util/Log.luau +68 -0
- package/src/util/Player.luau +14 -0
- package/src/util/Quantize.luau +60 -0
- package/src/internal/Util.luau +0 -26
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
--!native
|
|
3
3
|
-- Per-channel buffer state with MSB batch framing, XOR delta, timestamps.
|
|
4
4
|
|
|
5
|
+
local Base = require(script.Parent.Parent.codec.Base)
|
|
6
|
+
local Buf = require(script.Parent.Parent.util.Buffer)
|
|
7
|
+
local Constants = require(script.Parent.Parent.util.Constants)
|
|
8
|
+
local Log = require(script.Parent.Parent.util.Log)
|
|
5
9
|
local Types = require(script.Parent.Parent.Types)
|
|
6
10
|
local Varint = require(script.Parent.Parent.codec.primitive.Varint)
|
|
7
11
|
|
|
@@ -12,16 +16,29 @@ local TS_FRAME = 1
|
|
|
12
16
|
local TS_OFFSET = 2
|
|
13
17
|
local TS_FULL = 3
|
|
14
18
|
|
|
15
|
-
local DEFAULT_MAX = 262144
|
|
16
19
|
local INITIAL_BUF = 1024
|
|
17
|
-
local
|
|
18
|
-
local
|
|
20
|
+
local HEADER_BYTE = 1
|
|
21
|
+
local FRAME_MSB = Constants.FRAME_MSB
|
|
22
|
+
local U8_MAX = Constants.U8_MAX
|
|
23
|
+
local U16_MAX = Constants.U16_MAX
|
|
24
|
+
local MAX_BATCH_ITEMS = Constants.MAX_BATCH_ITEMS
|
|
25
|
+
|
|
26
|
+
-- 65.535s window so round((clock % win) * 1000) cannot overflow u16.
|
|
27
|
+
local OFFSET_WIN = 65.535
|
|
28
|
+
|
|
29
|
+
local TS_BYTES = table.freeze({
|
|
30
|
+
[TS_NONE] = 0,
|
|
31
|
+
[TS_FRAME] = 1,
|
|
32
|
+
[TS_OFFSET] = 2,
|
|
33
|
+
[TS_FULL] = 8,
|
|
34
|
+
})
|
|
19
35
|
|
|
20
36
|
-- State ------------------------------------------------------------------
|
|
21
37
|
|
|
22
|
-
local
|
|
23
|
-
local _anyTimestamp = false
|
|
38
|
+
local _timestampsEnabled = false
|
|
24
39
|
local _statsEnabled = false
|
|
40
|
+
|
|
41
|
+
-- Refreshed by updateTimestamp once per flush.
|
|
25
42
|
local _frameCounter = 0
|
|
26
43
|
local _offsetMs = 0
|
|
27
44
|
local _clock = 0.0
|
|
@@ -30,50 +47,20 @@ local EMPTY_REFS = table.freeze({}) :: { Instance }
|
|
|
30
47
|
|
|
31
48
|
-- Private ----------------------------------------------------------------
|
|
32
49
|
|
|
33
|
-
local
|
|
34
|
-
local lshift = bit32.lshift
|
|
50
|
+
local alloc = Base.alloc
|
|
35
51
|
local band = bit32.band
|
|
36
|
-
local bxor = bit32.bxor
|
|
37
52
|
local bor = bit32.bor
|
|
53
|
+
local bufCopy = buffer.copy
|
|
38
54
|
local writeu8 = buffer.writeu8
|
|
39
55
|
local writeu16 = buffer.writeu16
|
|
40
|
-
local writeu32 = buffer.writeu32
|
|
41
56
|
local writef64 = buffer.writef64
|
|
42
|
-
local readu8 = buffer.readu8
|
|
43
|
-
local readu32 = buffer.readu32
|
|
44
57
|
local round = math.round
|
|
45
|
-
local
|
|
58
|
+
local osClock = os.clock
|
|
46
59
|
|
|
47
|
-
local function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return
|
|
51
|
-
end
|
|
52
|
-
if needed > _maxSize then
|
|
53
|
-
local name = ch.currentPacket
|
|
54
|
-
if name then
|
|
55
|
-
error(`[Lync] Channel.alloc("{name}"): buffer overflow ({needed}B, max {_maxSize}B)`)
|
|
56
|
-
end
|
|
57
|
-
error(`[Lync] Channel.alloc: buffer overflow ({needed}B, max {_maxSize}B)`)
|
|
60
|
+
local function ensure(ch: Types.ChannelState, n: number): ()
|
|
61
|
+
if ch.cursor + n > ch.size then
|
|
62
|
+
alloc(ch, n)
|
|
58
63
|
end
|
|
59
|
-
local newSize = lshift(1, 32 - countlz(needed - 1))
|
|
60
|
-
local newBuff = buffer.create(newSize)
|
|
61
|
-
buffer.copy(newBuff, 0, ch.buff, 0, ch.cursor)
|
|
62
|
-
ch.buff = newBuff
|
|
63
|
-
ch.size = newSize
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
local function tsBytes(mode: number): number
|
|
67
|
-
if mode == TS_NONE then
|
|
68
|
-
return 0
|
|
69
|
-
end
|
|
70
|
-
if mode == TS_FRAME then
|
|
71
|
-
return 1
|
|
72
|
-
end
|
|
73
|
-
if mode == TS_OFFSET then
|
|
74
|
-
return 2
|
|
75
|
-
end
|
|
76
|
-
return 8
|
|
77
64
|
end
|
|
78
65
|
|
|
79
66
|
local function writeTimestamp(b: buffer, off: number, mode: number): ()
|
|
@@ -86,8 +73,66 @@ local function writeTimestamp(b: buffer, off: number, mode: number): ()
|
|
|
86
73
|
end
|
|
87
74
|
end
|
|
88
75
|
|
|
76
|
+
--[[
|
|
77
|
+
Single -> multi upgrade. Patches the previously-emitted single-item
|
|
78
|
+
header, splices a 2-byte count cell after the timestamp, and shifts the
|
|
79
|
+
payload right. Hot only when a packet fires twice in a row to the same
|
|
80
|
+
channel without an intervening flush.
|
|
81
|
+
]]
|
|
82
|
+
local function upgradeSingleToMulti(ch: Types.ChannelState, regId: number, headerSize: number): ()
|
|
83
|
+
local headerPos = ch.singlePos
|
|
84
|
+
writeu8(ch.buff, headerPos, regId)
|
|
85
|
+
|
|
86
|
+
local afterTs = headerPos + headerSize
|
|
87
|
+
local cursor = ch.cursor
|
|
88
|
+
local payloadLen = cursor - afterTs
|
|
89
|
+
ensure(ch, 2)
|
|
90
|
+
-- Re-bind: ensure() may have realloced the buffer.
|
|
91
|
+
local b = ch.buff
|
|
92
|
+
|
|
93
|
+
if payloadLen > 0 then
|
|
94
|
+
bufCopy(b, afterTs + 2, b, afterTs, payloadLen)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
ch.countPos = afterTs
|
|
98
|
+
writeu16(b, afterTs, 0)
|
|
99
|
+
ch.cursor = cursor + 2
|
|
100
|
+
ch.singleMode = false
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
--[[
|
|
104
|
+
Two writeBatch implementations, swapped at startup by enableStats(). For
|
|
105
|
+
high-rate channels this saves ~1 register read + 1 compare + 1 jump per
|
|
106
|
+
item versus branching on `_statsEnabled` inside the loop.
|
|
107
|
+
]]
|
|
108
|
+
local writeBatchPlain: (Types.ChannelState, Types.Registration, any) -> ()
|
|
109
|
+
local writeBatchStats: (Types.ChannelState, Types.Registration, any) -> ()
|
|
110
|
+
|
|
111
|
+
writeBatchPlain = function(ch: Types.ChannelState, reg: Types.Registration, data: any): ()
|
|
112
|
+
if reg.id ~= ch.lastId or ch.itemCount >= MAX_BATCH_ITEMS then
|
|
113
|
+
reg._openFn(ch, reg.id)
|
|
114
|
+
elseif ch.singleMode then
|
|
115
|
+
upgradeSingleToMulti(ch, reg.id, reg.headerSize)
|
|
116
|
+
end
|
|
117
|
+
ch.itemCount += 1
|
|
118
|
+
reg.codec.write(ch, data)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
writeBatchStats = function(ch: Types.ChannelState, reg: Types.Registration, data: any): ()
|
|
122
|
+
if reg.id ~= ch.lastId or ch.itemCount >= MAX_BATCH_ITEMS then
|
|
123
|
+
reg._openFn(ch, reg.id)
|
|
124
|
+
elseif ch.singleMode then
|
|
125
|
+
upgradeSingleToMulti(ch, reg.id, reg.headerSize)
|
|
126
|
+
end
|
|
127
|
+
ch.itemCount += 1
|
|
128
|
+
local before = ch.cursor
|
|
129
|
+
reg.codec.write(ch, data)
|
|
130
|
+
reg.bytesSent += ch.cursor - before
|
|
131
|
+
end
|
|
132
|
+
|
|
89
133
|
-- Public -----------------------------------------------------------------
|
|
90
134
|
|
|
135
|
+
-- Module table is intentionally unfrozen: enableStats() hot-swaps writeBatch.
|
|
91
136
|
local Channel = {}
|
|
92
137
|
|
|
93
138
|
Channel.TS_NONE = TS_NONE
|
|
@@ -114,36 +159,19 @@ function Channel.create(): Types.ChannelState
|
|
|
114
159
|
}
|
|
115
160
|
end
|
|
116
161
|
|
|
117
|
-
function Channel.alloc(ch: Types.ChannelState, bytes: number): ()
|
|
118
|
-
if bytes > 0 then
|
|
119
|
-
alloc(ch, bytes)
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
--[[
|
|
124
|
-
Write a single byte at the cursor and advance. Centralizes the
|
|
125
|
-
ubiquitous `if cursor + 1 > size then alloc(ch, 1) end; writeu8(...);
|
|
126
|
-
cursor += 1` pattern.
|
|
127
|
-
]]
|
|
128
162
|
function Channel.writeByte(ch: Types.ChannelState, byte: number): ()
|
|
129
163
|
local cursor = ch.cursor
|
|
130
|
-
if cursor +
|
|
131
|
-
alloc(ch,
|
|
164
|
+
if cursor + HEADER_BYTE > ch.size then
|
|
165
|
+
alloc(ch, HEADER_BYTE)
|
|
132
166
|
end
|
|
133
167
|
writeu8(ch.buff, cursor, byte)
|
|
134
|
-
ch.cursor = cursor +
|
|
168
|
+
ch.cursor = cursor + HEADER_BYTE
|
|
135
169
|
end
|
|
136
170
|
|
|
137
|
-
--
|
|
138
|
-
|
|
139
|
-
1-based index as a u16 to the buffer, advance cursor. Returns the
|
|
140
|
-
index. Used by the Instance and Unknown codecs.
|
|
141
|
-
]]
|
|
142
|
-
function Channel.pushRef(ch: Types.ChannelState, value: any): number
|
|
171
|
+
-- Append `value` to the sidecar refs, write its 1-based u16 index.
|
|
172
|
+
function Channel.pushRef(ch: Types.ChannelState, value: any): ()
|
|
143
173
|
local idx = ch.refCount + 1
|
|
144
|
-
|
|
145
|
-
error("[Lync] Channel.pushRef: ref overflow (>65535)")
|
|
146
|
-
end
|
|
174
|
+
Log.assert(idx <= U16_MAX, "ref overflow, max 65535")
|
|
147
175
|
ch.refs[idx] = value
|
|
148
176
|
ch.refCount = idx
|
|
149
177
|
|
|
@@ -153,9 +181,12 @@ function Channel.pushRef(ch: Types.ChannelState, value: any): number
|
|
|
153
181
|
end
|
|
154
182
|
writeu16(ch.buff, cursor, idx)
|
|
155
183
|
ch.cursor = cursor + 2
|
|
156
|
-
return idx
|
|
157
184
|
end
|
|
158
185
|
|
|
186
|
+
--[[
|
|
187
|
+
Per-flush reset. Preserves prevDump/prevDumpLen (XOR baseline) and
|
|
188
|
+
ch.deltas (delta caches): both must outlive flushes for the same owner.
|
|
189
|
+
]]
|
|
159
190
|
function Channel.reset(ch: Types.ChannelState): ()
|
|
160
191
|
ch.cursor = 0
|
|
161
192
|
ch.lastId = -1
|
|
@@ -170,20 +201,34 @@ function Channel.reset(ch: Types.ChannelState): ()
|
|
|
170
201
|
end
|
|
171
202
|
end
|
|
172
203
|
|
|
204
|
+
--[[
|
|
205
|
+
Full reset including XOR baseline and delta caches. Pool.acquire calls
|
|
206
|
+
this so a recycled state cannot leak the previous owner's baseline.
|
|
207
|
+
]]
|
|
208
|
+
function Channel.fullReset(ch: Types.ChannelState): ()
|
|
209
|
+
Channel.reset(ch)
|
|
210
|
+
ch.prevDump = nil
|
|
211
|
+
ch.prevDumpLen = 0
|
|
212
|
+
table.clear(ch.deltas)
|
|
213
|
+
end
|
|
214
|
+
|
|
173
215
|
--[[
|
|
174
216
|
Resolve the batch-open closure for a given timestamp mode and packet
|
|
175
|
-
name.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
- records cursor position so writeBatch can upgrade single->multi later.
|
|
217
|
+
name. Returned function seals the prior batch, writes the single-mode
|
|
218
|
+
header [0x80 | id] + optional timestamp, and records the cursor so
|
|
219
|
+
writeBatch can upgrade single -> multi later.
|
|
179
220
|
]]
|
|
180
221
|
function Channel.resolveOpenFn(
|
|
181
222
|
timestampMode: number,
|
|
182
223
|
name: string
|
|
183
224
|
): (ch: Types.ChannelState, id: number) -> ()
|
|
184
|
-
local headerLen =
|
|
225
|
+
local headerLen = HEADER_BYTE + TS_BYTES[timestampMode]
|
|
185
226
|
|
|
186
227
|
return function(ch: Types.ChannelState, id: number): ()
|
|
228
|
+
--[[
|
|
229
|
+
Inline sealCount: this is the only call site that can take the
|
|
230
|
+
multi-mode branch on a non-fresh channel.
|
|
231
|
+
]]
|
|
187
232
|
if ch.lastId >= 0 and not ch.singleMode then
|
|
188
233
|
writeu16(ch.buff, ch.countPos, ch.itemCount)
|
|
189
234
|
end
|
|
@@ -197,7 +242,7 @@ function Channel.resolveOpenFn(
|
|
|
197
242
|
alloc(ch, headerLen)
|
|
198
243
|
end
|
|
199
244
|
local b = ch.buff
|
|
200
|
-
writeu8(b, cursor, bor(
|
|
245
|
+
writeu8(b, cursor, bor(FRAME_MSB, id))
|
|
201
246
|
writeTimestamp(b, cursor + 1, timestampMode)
|
|
202
247
|
|
|
203
248
|
ch.singleMode = true
|
|
@@ -206,95 +251,31 @@ function Channel.resolveOpenFn(
|
|
|
206
251
|
end
|
|
207
252
|
end
|
|
208
253
|
|
|
254
|
+
Channel.writeBatch = writeBatchPlain
|
|
255
|
+
|
|
209
256
|
--[[
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
header byte's MSB, then memmoves payload right by 2 to insert the
|
|
213
|
-
deferred u16 count between the timestamp and first payload.
|
|
257
|
+
Every caller (Server.flushReliable / flushUnreliable, Client.flush)
|
|
258
|
+
pre-checks `ch.cursor > 0`, so cursor here is always positive.
|
|
214
259
|
]]
|
|
215
|
-
function Channel.writeBatch(ch: Types.ChannelState, reg: Types.Registration, data: any): ()
|
|
216
|
-
if reg.id ~= ch.lastId or ch.itemCount >= 0xFFFF then
|
|
217
|
-
reg._openFn(ch, reg.id)
|
|
218
|
-
elseif ch.singleMode then
|
|
219
|
-
local headerPos = ch.singlePos
|
|
220
|
-
local b = ch.buff
|
|
221
|
-
writeu8(b, headerPos, reg.id)
|
|
222
|
-
|
|
223
|
-
local afterTs = headerPos + reg.headerSize
|
|
224
|
-
local cursor = ch.cursor
|
|
225
|
-
local payloadLen = cursor - afterTs
|
|
226
|
-
if cursor + 2 > ch.size then
|
|
227
|
-
alloc(ch, 2)
|
|
228
|
-
end
|
|
229
|
-
local b2 = ch.buff
|
|
230
|
-
|
|
231
|
-
if payloadLen > 0 then
|
|
232
|
-
buffer.copy(b2, afterTs + 2, b2, afterTs, payloadLen)
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
ch.countPos = afterTs
|
|
236
|
-
writeu16(b2, afterTs, 0)
|
|
237
|
-
ch.cursor = cursor + 2
|
|
238
|
-
ch.singleMode = false
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
ch.itemCount += 1
|
|
242
|
-
reg.codec.write(ch, data)
|
|
243
|
-
end
|
|
244
|
-
|
|
245
260
|
function Channel.sealAndDump(ch: Types.ChannelState): (buffer, { Instance }, number, number)
|
|
246
|
-
if ch.lastId >= 0 and not ch.singleMode
|
|
261
|
+
if ch.lastId >= 0 and not ch.singleMode then
|
|
247
262
|
writeu16(ch.buff, ch.countPos, ch.itemCount)
|
|
248
263
|
end
|
|
249
264
|
|
|
250
265
|
local cursor = ch.cursor
|
|
251
266
|
local snapshot = buffer.create(cursor)
|
|
252
|
-
|
|
253
|
-
buffer.copy(snapshot, 0, ch.buff, 0, cursor)
|
|
254
|
-
end
|
|
267
|
+
bufCopy(snapshot, 0, ch.buff, 0, cursor)
|
|
255
268
|
|
|
256
269
|
local refCount = ch.refCount
|
|
257
270
|
local refs = if refCount > 0 then table.clone(ch.refs) else EMPTY_REFS
|
|
258
271
|
return snapshot, refs, cursor, refCount
|
|
259
272
|
end
|
|
260
273
|
|
|
261
|
-
|
|
262
|
-
XOR `current` against `previous`, returning a fresh buffer of curLen
|
|
263
|
-
bytes. When `previous` is nil, returns `current` unchanged.
|
|
264
|
-
]]
|
|
265
|
-
function Channel.xorApply(
|
|
266
|
-
current: buffer,
|
|
267
|
-
curLen: number,
|
|
268
|
-
previous: buffer?,
|
|
269
|
-
prevLen: number
|
|
270
|
-
): buffer
|
|
271
|
-
if not previous then
|
|
272
|
-
return current
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
local result = buffer.create(curLen)
|
|
276
|
-
local overlap = minN(curLen, prevLen)
|
|
277
|
-
local prev = previous
|
|
278
|
-
|
|
279
|
-
local aligned = band(overlap, -4)
|
|
280
|
-
local i = 0
|
|
281
|
-
while i < aligned do
|
|
282
|
-
writeu32(result, i, bxor(readu32(current, i), readu32(prev, i)))
|
|
283
|
-
i += 4
|
|
284
|
-
end
|
|
285
|
-
while i < overlap do
|
|
286
|
-
writeu8(result, i, bxor(readu8(current, i), readu8(prev, i)))
|
|
287
|
-
i += 1
|
|
288
|
-
end
|
|
289
|
-
if curLen > overlap then
|
|
290
|
-
buffer.copy(result, overlap, current, overlap, curLen - overlap)
|
|
291
|
-
end
|
|
292
|
-
return result
|
|
293
|
-
end
|
|
274
|
+
Channel.xorApply = Buf.xorApply
|
|
294
275
|
|
|
295
276
|
--[[
|
|
296
|
-
|
|
297
|
-
|
|
277
|
+
Query frame. MSB on the id byte = nil response; cleared = payload follows.
|
|
278
|
+
Correlation IDs are varint-encoded.
|
|
298
279
|
]]
|
|
299
280
|
function Channel.writeQuery(
|
|
300
281
|
ch: Types.ChannelState,
|
|
@@ -303,14 +284,19 @@ function Channel.writeQuery(
|
|
|
303
284
|
codec: Types.InternalCodec<any>?,
|
|
304
285
|
data: any
|
|
305
286
|
): ()
|
|
306
|
-
if ch.lastId >= 0 and not ch.singleMode
|
|
287
|
+
if ch.lastId >= 0 and not ch.singleMode then
|
|
307
288
|
writeu16(ch.buff, ch.countPos, ch.itemCount)
|
|
308
289
|
end
|
|
290
|
+
--[[
|
|
291
|
+
lastId = -1 forces the next writeBatch to call _openFn (which resets
|
|
292
|
+
singleMode). Every other reader of singleMode (writeQuery,
|
|
293
|
+
sealAndDump) is gated by `lastId >= 0` first, so stamping singleMode
|
|
294
|
+
here would never be observed before the next reset overwrites it.
|
|
295
|
+
]]
|
|
309
296
|
ch.lastId = -1
|
|
310
|
-
ch.singleMode = false
|
|
311
297
|
|
|
312
298
|
local hasPayload = codec ~= nil and data ~= nil
|
|
313
|
-
local headerByte = if hasPayload then id else bor(
|
|
299
|
+
local headerByte = if hasPayload then id else bor(FRAME_MSB, id)
|
|
314
300
|
|
|
315
301
|
local cursor = ch.cursor
|
|
316
302
|
if cursor + 1 > ch.size then
|
|
@@ -328,29 +314,42 @@ end
|
|
|
328
314
|
-- Configuration ----------------------------------------------------------
|
|
329
315
|
|
|
330
316
|
function Channel.setMaxSize(bytes: number): ()
|
|
331
|
-
|
|
317
|
+
Base.setMaxSize(bytes)
|
|
332
318
|
end
|
|
333
319
|
|
|
334
320
|
function Channel.updateTimestamp(): ()
|
|
335
|
-
_frameCounter = band(_frameCounter + 1,
|
|
336
|
-
|
|
337
|
-
|
|
321
|
+
_frameCounter = band(_frameCounter + 1, U8_MAX)
|
|
322
|
+
local now = osClock()
|
|
323
|
+
_offsetMs = round((now % OFFSET_WIN) * 1000)
|
|
324
|
+
_clock = now
|
|
338
325
|
end
|
|
339
326
|
|
|
340
327
|
function Channel.enableTimestamps(): ()
|
|
341
|
-
|
|
328
|
+
_timestampsEnabled = true
|
|
342
329
|
end
|
|
343
330
|
|
|
344
331
|
function Channel.hasTimestamps(): boolean
|
|
345
|
-
return
|
|
332
|
+
return _timestampsEnabled
|
|
346
333
|
end
|
|
347
334
|
|
|
348
335
|
function Channel.enableStats(): ()
|
|
349
336
|
_statsEnabled = true
|
|
337
|
+
Channel.writeBatch = writeBatchStats
|
|
350
338
|
end
|
|
351
339
|
|
|
352
340
|
function Channel.statsEnabled(): boolean
|
|
353
341
|
return _statsEnabled
|
|
354
342
|
end
|
|
355
343
|
|
|
356
|
-
|
|
344
|
+
-- Module-level config + timestamp counters. Distinct from Channel.reset(ch).
|
|
345
|
+
function Channel.resetGlobals(): ()
|
|
346
|
+
Base.reset()
|
|
347
|
+
_timestampsEnabled = false
|
|
348
|
+
_statsEnabled = false
|
|
349
|
+
_frameCounter = 0
|
|
350
|
+
_offsetMs = 0
|
|
351
|
+
_clock = 0.0
|
|
352
|
+
Channel.writeBatch = writeBatchPlain
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
return Channel
|
|
@@ -13,13 +13,21 @@ local _dropSignal = Signal.create()
|
|
|
13
13
|
|
|
14
14
|
-- Private ----------------------------------------------------------------
|
|
15
15
|
|
|
16
|
+
-- Mirrors Signal's STATE_DISCONNECTED. Entries in this state are mid-fire disconnects.
|
|
17
|
+
local STATE_DISCONNECTED = 0
|
|
18
|
+
|
|
16
19
|
--[[
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
Snapshot before iterating: a handler can disconnect itself or others,
|
|
21
|
+
and a fresh chain pull would skip or double-fire entries depending on
|
|
22
|
+
swap direction. Snapshotting locks the per-frame view.
|
|
20
23
|
]]
|
|
21
|
-
local function runChain(
|
|
22
|
-
|
|
24
|
+
local function runChain(
|
|
25
|
+
signal: { _count: number, _entries: { any } },
|
|
26
|
+
value: any,
|
|
27
|
+
name: string,
|
|
28
|
+
player: Player?
|
|
29
|
+
): any
|
|
30
|
+
local count = signal._count
|
|
23
31
|
if count == 0 then
|
|
24
32
|
return value
|
|
25
33
|
end
|
|
@@ -31,8 +39,9 @@ local function runChain(signal: any, value: any, name: string, player: Player?):
|
|
|
31
39
|
local result = value
|
|
32
40
|
for i = 1, count do
|
|
33
41
|
local entry = snapshot[i]
|
|
34
|
-
if entry._state ~=
|
|
42
|
+
if entry._state ~= STATE_DISCONNECTED then
|
|
35
43
|
local returned = entry.fn(result, name, player)
|
|
44
|
+
-- Hooks return non-nil to replace the value; nil keeps the prior result.
|
|
36
45
|
if returned ~= nil then
|
|
37
46
|
result = returned
|
|
38
47
|
end
|
|
@@ -73,6 +82,7 @@ function Middleware.fireDrop(player: Player, reason: string, name: string, data:
|
|
|
73
82
|
_dropSignal:fireSync(player, reason, name, data)
|
|
74
83
|
end
|
|
75
84
|
|
|
85
|
+
-- Hot-path callers gate on these to skip runChain's snapshot allocation.
|
|
76
86
|
function Middleware.hasSendHooks(): boolean
|
|
77
87
|
return _sendSignal:hasListeners()
|
|
78
88
|
end
|
|
@@ -81,4 +91,10 @@ function Middleware.hasReceiveHooks(): boolean
|
|
|
81
91
|
return _receiveSignal:hasListeners()
|
|
82
92
|
end
|
|
83
93
|
|
|
94
|
+
function Middleware.reset(): ()
|
|
95
|
+
_sendSignal = Signal.create()
|
|
96
|
+
_receiveSignal = Signal.create()
|
|
97
|
+
_dropSignal = Signal.create()
|
|
98
|
+
end
|
|
99
|
+
|
|
84
100
|
return table.freeze(Middleware)
|
package/src/internal/Pool.luau
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
--!strict
|
|
2
2
|
--!optimize 2
|
|
3
|
-
--
|
|
4
|
-
-- channels reusable without per-release reset cost.
|
|
3
|
+
-- ChannelState pool. Acquire fullResets to wipe prior owner state.
|
|
5
4
|
|
|
6
5
|
local Channel = require(script.Parent.Channel)
|
|
6
|
+
local Log = require(script.Parent.Parent.util.Log)
|
|
7
7
|
local Types = require(script.Parent.Parent.Types)
|
|
8
8
|
|
|
9
9
|
-- Constants --------------------------------------------------------------
|
|
@@ -20,17 +20,19 @@ local _maxSize = DEFAULT_SIZE
|
|
|
20
20
|
|
|
21
21
|
local Pool = {}
|
|
22
22
|
|
|
23
|
+
-- fullReset wipes the prior owner's XOR baseline and delta caches.
|
|
23
24
|
function Pool.acquire(): Types.ChannelState
|
|
24
25
|
if _depth > 0 then
|
|
25
26
|
local ch = _stack[_depth]
|
|
26
27
|
_stack[_depth] = nil
|
|
27
28
|
_depth -= 1
|
|
28
|
-
Channel.
|
|
29
|
+
Channel.fullReset(ch)
|
|
29
30
|
return ch
|
|
30
31
|
end
|
|
31
32
|
return Channel.create()
|
|
32
33
|
end
|
|
33
34
|
|
|
35
|
+
-- Above the cap, drop on the floor; the GC reclaims the buffer.
|
|
34
36
|
function Pool.release(ch: Types.ChannelState): ()
|
|
35
37
|
if _depth >= _maxSize then
|
|
36
38
|
return
|
|
@@ -41,7 +43,7 @@ end
|
|
|
41
43
|
|
|
42
44
|
function Pool.setMaxSize(size: number): ()
|
|
43
45
|
if size < 0 then
|
|
44
|
-
error(`
|
|
46
|
+
Log.error(`size must be non-negative, got {size}`)
|
|
45
47
|
end
|
|
46
48
|
_maxSize = size
|
|
47
49
|
while _depth > _maxSize do
|
|
@@ -54,4 +56,10 @@ function Pool.count(): number
|
|
|
54
56
|
return _depth
|
|
55
57
|
end
|
|
56
58
|
|
|
59
|
+
function Pool.reset(): ()
|
|
60
|
+
table.clear(_stack)
|
|
61
|
+
_depth = 0
|
|
62
|
+
_maxSize = DEFAULT_SIZE
|
|
63
|
+
end
|
|
64
|
+
|
|
57
65
|
return table.freeze(Pool)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
--!strict
|
|
2
2
|
--!optimize 2
|
|
3
|
-
--
|
|
3
|
+
-- Sequential 7-bit packet/query ID assignment.
|
|
4
4
|
|
|
5
|
+
local Log = require(script.Parent.Parent.util.Log)
|
|
5
6
|
local Signal = require(script.Parent.Parent.api.Signal)
|
|
6
7
|
local Types = require(script.Parent.Parent.Types)
|
|
7
8
|
|
|
@@ -10,13 +11,15 @@ local Types = require(script.Parent.Parent.Types)
|
|
|
10
11
|
local KIND_PACKET = 0
|
|
11
12
|
local KIND_REQUEST = 1
|
|
12
13
|
local KIND_RESPONSE = 2
|
|
14
|
+
|
|
15
|
+
-- 7-bit id leaves the MSB free for the frame single/multi flag (see Constants.FRAME_MSB).
|
|
13
16
|
local MAX_ID = 127
|
|
14
17
|
|
|
15
18
|
-- State ------------------------------------------------------------------
|
|
16
19
|
|
|
17
20
|
local _nextId = 0
|
|
18
|
-
local
|
|
19
|
-
local
|
|
21
|
+
local _registrationsById: { [number]: Types.Registration } = {}
|
|
22
|
+
local _registrationsByName: { [string]: Types.Registration } = {}
|
|
20
23
|
|
|
21
24
|
-- Public -----------------------------------------------------------------
|
|
22
25
|
|
|
@@ -45,10 +48,14 @@ export type RegisterOptions = {
|
|
|
45
48
|
function Registry.register(opts: RegisterOptions): Types.Registration
|
|
46
49
|
local nextCandidate = _nextId + 1
|
|
47
50
|
if nextCandidate > MAX_ID then
|
|
48
|
-
error(`
|
|
51
|
+
Log.error(`ID limit ({MAX_ID}) exceeded; cannot register "{opts.name}"`)
|
|
49
52
|
end
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
--[[
|
|
54
|
+
RESPONSE entries share their request's name with a "\0resp" suffix; only
|
|
55
|
+
request/packet names need uniqueness in the by-name index.
|
|
56
|
+
]]
|
|
57
|
+
if opts.kind ~= KIND_RESPONSE and _registrationsByName[opts.name] ~= nil then
|
|
58
|
+
Log.error(`duplicate name "{opts.name}"`)
|
|
52
59
|
end
|
|
53
60
|
|
|
54
61
|
_nextId = nextCandidate
|
|
@@ -75,36 +82,38 @@ function Registry.register(opts: RegisterOptions): Types.Registration
|
|
|
75
82
|
drops = 0,
|
|
76
83
|
}
|
|
77
84
|
|
|
78
|
-
|
|
85
|
+
_registrationsById[nextCandidate] = reg
|
|
79
86
|
if opts.kind ~= KIND_RESPONSE then
|
|
80
|
-
|
|
87
|
+
_registrationsByName[opts.name] = reg
|
|
81
88
|
end
|
|
82
89
|
return reg
|
|
83
90
|
end
|
|
84
91
|
|
|
85
92
|
function Registry.get(id: number): Types.Registration?
|
|
86
|
-
return
|
|
93
|
+
return _registrationsById[id]
|
|
87
94
|
end
|
|
88
95
|
|
|
89
96
|
function Registry.getByName(name: string): Types.Registration?
|
|
90
|
-
return
|
|
97
|
+
return _registrationsByName[name]
|
|
91
98
|
end
|
|
92
99
|
|
|
93
100
|
function Registry.count(): number
|
|
94
101
|
return _nextId
|
|
95
102
|
end
|
|
96
103
|
|
|
104
|
+
-- IDs are sequential and never freed, so the table has no holes.
|
|
97
105
|
function Registry.all(): { Types.Registration }
|
|
98
106
|
local result = table.create(_nextId)
|
|
99
|
-
local n = 0
|
|
100
107
|
for i = 1, _nextId do
|
|
101
|
-
|
|
102
|
-
if reg then
|
|
103
|
-
n += 1
|
|
104
|
-
result[n] = reg
|
|
105
|
-
end
|
|
108
|
+
result[i] = _registrationsById[i]
|
|
106
109
|
end
|
|
107
110
|
return result
|
|
108
111
|
end
|
|
109
112
|
|
|
113
|
+
function Registry.reset(): ()
|
|
114
|
+
table.clear(_registrationsById)
|
|
115
|
+
table.clear(_registrationsByName)
|
|
116
|
+
_nextId = 0
|
|
117
|
+
end
|
|
118
|
+
|
|
110
119
|
return table.freeze(Registry)
|