@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
package/src/api/Signal.luau
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
--!strict
|
|
2
2
|
--!optimize 2
|
|
3
|
-
-- O(1) disconnect signal
|
|
3
|
+
-- O(1) disconnect signal with thread reuse and reentrancy-safe fire.
|
|
4
4
|
|
|
5
5
|
local Types = require(script.Parent.Parent.Types)
|
|
6
6
|
|
|
@@ -12,6 +12,7 @@ local STATE_ONCE = 2
|
|
|
12
12
|
|
|
13
13
|
-- State ------------------------------------------------------------------
|
|
14
14
|
|
|
15
|
+
-- Single recyclable thread for async dispatch; saves coroutine.create per fire.
|
|
15
16
|
local _freeThread: thread? = nil
|
|
16
17
|
|
|
17
18
|
-- Private ----------------------------------------------------------------
|
|
@@ -41,14 +42,19 @@ type Entry = {
|
|
|
41
42
|
fn: (...any) -> (),
|
|
42
43
|
_state: number,
|
|
43
44
|
connected: boolean,
|
|
44
|
-
|
|
45
|
-
_signalCount: { number },
|
|
45
|
+
_signal: SignalFields,
|
|
46
46
|
_index: number,
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
type SignalFields = {
|
|
50
|
+
_entries: { Entry },
|
|
51
|
+
_count: number,
|
|
52
|
+
}
|
|
53
|
+
|
|
49
54
|
local EntryImpl = {}
|
|
50
55
|
EntryImpl.__index = EntryImpl
|
|
51
56
|
|
|
57
|
+
-- Swap-with-last for O(1) disconnect; the moved entry's _index is patched too.
|
|
52
58
|
local function disconnectEntry(entry: Entry): ()
|
|
53
59
|
if entry._state == STATE_DISCONNECTED then
|
|
54
60
|
return
|
|
@@ -57,9 +63,10 @@ local function disconnectEntry(entry: Entry): ()
|
|
|
57
63
|
entry._state = STATE_DISCONNECTED
|
|
58
64
|
entry.connected = false
|
|
59
65
|
|
|
60
|
-
local
|
|
61
|
-
local
|
|
62
|
-
local idx
|
|
66
|
+
local signal = entry._signal
|
|
67
|
+
local entries = signal._entries
|
|
68
|
+
local idx = entry._index
|
|
69
|
+
local last = signal._count
|
|
63
70
|
|
|
64
71
|
if idx < last then
|
|
65
72
|
local moved = entries[last]
|
|
@@ -67,31 +74,40 @@ local function disconnectEntry(entry: Entry): ()
|
|
|
67
74
|
moved._index = idx
|
|
68
75
|
end
|
|
69
76
|
entries[last] = nil
|
|
70
|
-
|
|
77
|
+
signal._count = last - 1
|
|
71
78
|
end
|
|
72
79
|
|
|
73
80
|
EntryImpl.disconnect = disconnectEntry
|
|
74
81
|
table.freeze(EntryImpl)
|
|
75
82
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
-- Auto-disconnects ONCE entries before invoking; chooses sync vs spawned thread.
|
|
84
|
+
local function invokeEntry(entry: Entry, sync: boolean, ...: any): ()
|
|
85
|
+
local state = entry._state
|
|
86
|
+
if state == STATE_DISCONNECTED then
|
|
87
|
+
return
|
|
88
|
+
end
|
|
89
|
+
if state == STATE_ONCE then
|
|
90
|
+
disconnectEntry(entry)
|
|
91
|
+
end
|
|
92
|
+
if sync then
|
|
93
|
+
entry.fn(...)
|
|
94
|
+
else
|
|
95
|
+
spawnCallback(entry.fn, ...)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
80
98
|
|
|
81
99
|
local SignalImpl = {}
|
|
82
100
|
SignalImpl.__index = SignalImpl
|
|
83
101
|
|
|
84
102
|
local function newEntry(self: SignalFields, callback: (...any) -> (), state: number): Entry
|
|
85
|
-
local
|
|
86
|
-
|
|
87
|
-
countRef[1] = idx
|
|
103
|
+
local idx = self._count + 1
|
|
104
|
+
self._count = idx
|
|
88
105
|
|
|
89
106
|
local entry: Entry = setmetatable({
|
|
90
107
|
fn = callback,
|
|
91
108
|
_state = state,
|
|
92
109
|
connected = true,
|
|
93
|
-
|
|
94
|
-
_signalCount = countRef,
|
|
110
|
+
_signal = self,
|
|
95
111
|
_index = idx,
|
|
96
112
|
}, EntryImpl) :: any
|
|
97
113
|
|
|
@@ -118,53 +134,28 @@ function SignalImpl.wait(self: SignalFields): ...any
|
|
|
118
134
|
end
|
|
119
135
|
|
|
120
136
|
local function dispatch(self: SignalFields, sync: boolean, ...: any): ()
|
|
121
|
-
local count = self._count
|
|
137
|
+
local count = self._count
|
|
122
138
|
if count == 0 then
|
|
123
139
|
return
|
|
124
140
|
end
|
|
125
141
|
|
|
126
142
|
local entries = self._entries
|
|
127
143
|
|
|
144
|
+
-- Single-listener fast path skips the snapshot allocation.
|
|
128
145
|
if count == 1 then
|
|
129
|
-
|
|
130
|
-
local state = entry._state
|
|
131
|
-
if state == STATE_ONCE then
|
|
132
|
-
disconnectEntry(entry)
|
|
133
|
-
if sync then
|
|
134
|
-
entry.fn(...)
|
|
135
|
-
else
|
|
136
|
-
spawnCallback(entry.fn, ...)
|
|
137
|
-
end
|
|
138
|
-
elseif state == STATE_CONNECTED then
|
|
139
|
-
if sync then
|
|
140
|
-
entry.fn(...)
|
|
141
|
-
else
|
|
142
|
-
spawnCallback(entry.fn, ...)
|
|
143
|
-
end
|
|
144
|
-
end
|
|
146
|
+
invokeEntry(entries[1], sync, ...)
|
|
145
147
|
return
|
|
146
148
|
end
|
|
147
149
|
|
|
150
|
+
--[[
|
|
151
|
+
Re-entrancy: a handler may disconnect or add others mid-fire.
|
|
152
|
+
Snapshot the entry list so the iteration view is stable.
|
|
153
|
+
]]
|
|
148
154
|
local snapshot = table.create(count)
|
|
149
155
|
table.move(entries, 1, count, 1, snapshot)
|
|
150
156
|
|
|
151
157
|
for i = 1, count do
|
|
152
|
-
|
|
153
|
-
local state = entry._state
|
|
154
|
-
if state == STATE_ONCE then
|
|
155
|
-
disconnectEntry(entry)
|
|
156
|
-
if sync then
|
|
157
|
-
entry.fn(...)
|
|
158
|
-
else
|
|
159
|
-
spawnCallback(entry.fn, ...)
|
|
160
|
-
end
|
|
161
|
-
elseif state == STATE_CONNECTED then
|
|
162
|
-
if sync then
|
|
163
|
-
entry.fn(...)
|
|
164
|
-
else
|
|
165
|
-
spawnCallback(entry.fn, ...)
|
|
166
|
-
end
|
|
167
|
-
end
|
|
158
|
+
invokeEntry(snapshot[i], sync, ...)
|
|
168
159
|
end
|
|
169
160
|
end
|
|
170
161
|
|
|
@@ -177,11 +168,11 @@ function SignalImpl.fireSync(self: SignalFields, ...: any): ()
|
|
|
177
168
|
end
|
|
178
169
|
|
|
179
170
|
function SignalImpl.count(self: SignalFields): number
|
|
180
|
-
return self._count
|
|
171
|
+
return self._count
|
|
181
172
|
end
|
|
182
173
|
|
|
183
174
|
function SignalImpl.hasListeners(self: SignalFields): boolean
|
|
184
|
-
return self._count
|
|
175
|
+
return self._count > 0
|
|
185
176
|
end
|
|
186
177
|
|
|
187
178
|
table.freeze(SignalImpl)
|
|
@@ -191,7 +182,7 @@ table.freeze(SignalImpl)
|
|
|
191
182
|
local SignalModule = {}
|
|
192
183
|
|
|
193
184
|
function SignalModule.create(): Types.SignalLike
|
|
194
|
-
return (setmetatable({ _entries = {}, _count =
|
|
185
|
+
return (setmetatable({ _entries = {}, _count = 0 }, SignalImpl) :: any) :: Types.SignalLike
|
|
195
186
|
end
|
|
196
187
|
|
|
197
188
|
return table.freeze(SignalModule)
|
package/src/codec/Base.luau
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
--!strict
|
|
2
2
|
--!optimize 2
|
|
3
|
-
-- Fixed-size codec factory
|
|
3
|
+
-- Fixed-size codec factory + channel buffer-grow primitive.
|
|
4
4
|
|
|
5
|
+
local Log = require(script.Parent.Parent.util.Log)
|
|
5
6
|
local Types = require(script.Parent.Parent.Types)
|
|
6
7
|
|
|
7
8
|
-- Constants --------------------------------------------------------------
|
|
@@ -19,9 +20,9 @@ local countlz = bit32.countlz
|
|
|
19
20
|
local lshift = bit32.lshift
|
|
20
21
|
|
|
21
22
|
--[[
|
|
22
|
-
Grow `ch.buff` so
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
Grow `ch.buff` so cursor + bytes fits. New size is the next power of two
|
|
24
|
+
>= needed, capped by _maxSize. Callers MUST re-read `ch.buff` after
|
|
25
|
+
return: the buffer reference is replaced.
|
|
25
26
|
]]
|
|
26
27
|
local function alloc(ch: Types.ChannelState, bytes: number): ()
|
|
27
28
|
if bytes == 0 then
|
|
@@ -36,9 +37,9 @@ local function alloc(ch: Types.ChannelState, bytes: number): ()
|
|
|
36
37
|
if needed > _maxSize then
|
|
37
38
|
local name = ch.currentPacket
|
|
38
39
|
if name then
|
|
39
|
-
error(`
|
|
40
|
+
Log.error(`packet "{name}" overflowed at {needed}B (max {_maxSize}B)`)
|
|
40
41
|
end
|
|
41
|
-
error(`
|
|
42
|
+
Log.error(`overflowed at {needed}B (max {_maxSize}B)`)
|
|
42
43
|
end
|
|
43
44
|
|
|
44
45
|
local newSize = lshift(1, 32 - countlz(needed - 1))
|
|
@@ -53,18 +54,24 @@ end
|
|
|
53
54
|
local Base = {}
|
|
54
55
|
|
|
55
56
|
Base.INITIAL_BUF = INITIAL_BUF
|
|
56
|
-
|
|
57
57
|
Base.alloc = alloc
|
|
58
58
|
|
|
59
|
+
-- Floor is INITIAL_BUF: anything smaller errors on the very first write.
|
|
59
60
|
function Base.setMaxSize(bytes: number): ()
|
|
61
|
+
if bytes < INITIAL_BUF then
|
|
62
|
+
Log.error(`bytes ({bytes}) must be >= {INITIAL_BUF}`)
|
|
63
|
+
end
|
|
60
64
|
_maxSize = bytes
|
|
61
65
|
end
|
|
62
66
|
|
|
67
|
+
function Base.reset(): ()
|
|
68
|
+
_maxSize = DEFAULT_MAX
|
|
69
|
+
end
|
|
70
|
+
|
|
63
71
|
--[[
|
|
64
|
-
Build a fixed-size codec.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
pre-sized fast paths.
|
|
72
|
+
Build a fixed-size codec. Exposes channel-level write/read (handles
|
|
73
|
+
buffer growth + cursor) plus raw _directWrite/_directRead for composite
|
|
74
|
+
fast paths that pre-size the channel themselves.
|
|
68
75
|
]]
|
|
69
76
|
function Base.define<T>(
|
|
70
77
|
size: number,
|
|
@@ -101,9 +108,9 @@ function Base.define<T>(
|
|
|
101
108
|
end
|
|
102
109
|
|
|
103
110
|
--[[
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
111
|
+
Zero-byte codec: writes nothing, reads always return `value`. Used by
|
|
112
|
+
quantizers when min == max so the wire emits nothing yet decode produces
|
|
113
|
+
a type-faithful value.
|
|
107
114
|
]]
|
|
108
115
|
function Base.constant<T>(value: T): Types.InternalCodec<T>
|
|
109
116
|
return table.freeze({
|
|
@@ -1,98 +1,62 @@
|
|
|
1
1
|
--!strict
|
|
2
2
|
--!native
|
|
3
|
-
--
|
|
3
|
+
-- array() and deltaArray(). Bool elements are bit-packed.
|
|
4
4
|
|
|
5
5
|
local Base = require(script.Parent.Parent.Base)
|
|
6
|
-
local
|
|
7
|
-
local
|
|
6
|
+
local Baseline = require(script.Parent.Parent.Parent.internal.Baseline)
|
|
7
|
+
local Log = require(script.Parent.Parent.Parent.util.Log)
|
|
8
|
+
local Shared = require(script.Parent.Parent.composite.Shared)
|
|
8
9
|
local Types = require(script.Parent.Parent.Parent.Types)
|
|
9
10
|
local Varint = require(script.Parent.Parent.primitive.Varint)
|
|
10
11
|
|
|
11
12
|
-- Private ----------------------------------------------------------------
|
|
12
13
|
|
|
13
14
|
local alloc = Base.alloc
|
|
14
|
-
local writeByte = Channel.writeByte
|
|
15
|
-
local writeu8 = buffer.writeu8
|
|
16
|
-
local readu8 = buffer.readu8
|
|
17
|
-
local band = bit32.band
|
|
18
|
-
local bor = bit32.bor
|
|
19
|
-
local lshift = bit32.lshift
|
|
20
|
-
|
|
21
|
-
local acquireScratch = Shared.acquireScratch
|
|
22
|
-
local releaseScratch = Shared.releaseScratch
|
|
23
|
-
local rangeEqual = Shared.rangeEqual
|
|
24
|
-
local snapshot = Shared.snapshot
|
|
25
15
|
local allocDeltaId = Shared.allocDeltaId
|
|
26
|
-
local
|
|
27
|
-
local
|
|
16
|
+
local boolBytes = Shared.boolBytes
|
|
17
|
+
local packBoolArray = Shared.packBoolArray
|
|
18
|
+
local unpackBoolArray = Shared.unpackBoolArray
|
|
19
|
+
local varintRead = Varint.read
|
|
20
|
+
local varintWrite = Varint.write
|
|
28
21
|
|
|
22
|
+
-- Bit-packed bool element fast path: 1 bit per entry instead of 1 byte.
|
|
29
23
|
local function makeBoolArray(
|
|
30
24
|
element: Types.InternalCodec<any>,
|
|
31
25
|
maxCount: number?
|
|
32
26
|
): Types.InternalCodec<any>
|
|
33
|
-
return
|
|
27
|
+
return {
|
|
34
28
|
_isArray = true,
|
|
35
29
|
_element = element,
|
|
36
30
|
_maxCount = maxCount,
|
|
37
31
|
|
|
38
32
|
write = function(ch: Types.ChannelState, value: { boolean }): ()
|
|
39
33
|
local len = #value
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
local byteCount = (len + 7) // 8
|
|
46
|
-
local cursor = ch.cursor
|
|
47
|
-
if cursor + byteCount > ch.size then
|
|
48
|
-
alloc(ch, byteCount)
|
|
49
|
-
end
|
|
50
|
-
local b = ch.buff
|
|
51
|
-
|
|
52
|
-
local idx = 1
|
|
53
|
-
for byteIdx = 0, byteCount - 1 do
|
|
54
|
-
local packed = 0
|
|
55
|
-
local remaining = len - idx + 1
|
|
56
|
-
local limit = if remaining >= 8 then 8 else remaining
|
|
57
|
-
for bit = 0, limit - 1 do
|
|
58
|
-
if value[idx] then
|
|
59
|
-
packed = bor(packed, lshift(1, bit))
|
|
60
|
-
end
|
|
61
|
-
idx += 1
|
|
62
|
-
end
|
|
63
|
-
writeu8(b, cursor + byteIdx, packed)
|
|
64
|
-
end
|
|
65
|
-
ch.cursor = cursor + byteCount
|
|
34
|
+
varintWrite(ch, len)
|
|
35
|
+
packBoolArray(ch, value, len)
|
|
66
36
|
end,
|
|
67
37
|
|
|
68
38
|
read = function(src: buffer, pos: number, _refs: { Instance }?): ({ boolean }, number)
|
|
69
|
-
local len, lenBytes =
|
|
39
|
+
local len, lenBytes = varintRead(src, pos)
|
|
70
40
|
if maxCount and len > maxCount then
|
|
71
|
-
error(`
|
|
41
|
+
Log.error(`count {len} exceeds max {maxCount}`)
|
|
72
42
|
end
|
|
73
43
|
if len == 0 then
|
|
74
44
|
return {}, lenBytes
|
|
75
45
|
end
|
|
76
46
|
|
|
77
|
-
local byteCount = (len
|
|
47
|
+
local byteCount = boolBytes(len)
|
|
78
48
|
local dataStart = pos + lenBytes
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
local idx = 1
|
|
82
|
-
for byteIdx = 0, byteCount - 1 do
|
|
83
|
-
local byte = readu8(src, dataStart + byteIdx)
|
|
84
|
-
local remaining = len - idx + 1
|
|
85
|
-
local limit = if remaining >= 8 then 8 else remaining
|
|
86
|
-
for bit = 0, limit - 1 do
|
|
87
|
-
result[idx] = band(byte, lshift(1, bit)) ~= 0
|
|
88
|
-
idx += 1
|
|
89
|
-
end
|
|
49
|
+
if dataStart + byteCount > buffer.len(src) then
|
|
50
|
+
Log.error(`payload {byteCount}B exceeds remaining buffer`)
|
|
90
51
|
end
|
|
52
|
+
local result = table.create(len, false)
|
|
53
|
+
unpackBoolArray(src, dataStart, len, result)
|
|
91
54
|
return result, lenBytes + byteCount
|
|
92
55
|
end,
|
|
93
|
-
}
|
|
56
|
+
}
|
|
94
57
|
end
|
|
95
58
|
|
|
59
|
+
-- Fixed-size element fast path: single alloc + tight loop calling _direct fns.
|
|
96
60
|
local function makeDirectArray(
|
|
97
61
|
element: Types.InternalCodec<any>,
|
|
98
62
|
elemSize: number,
|
|
@@ -100,14 +64,14 @@ local function makeDirectArray(
|
|
|
100
64
|
elemDR: (b: buffer, off: number) -> any,
|
|
101
65
|
maxCount: number?
|
|
102
66
|
): Types.InternalCodec<any>
|
|
103
|
-
return
|
|
67
|
+
return {
|
|
104
68
|
_isArray = true,
|
|
105
69
|
_element = element,
|
|
106
70
|
_maxCount = maxCount,
|
|
107
71
|
|
|
108
72
|
write = function(ch: Types.ChannelState, value: { any }): ()
|
|
109
73
|
local len = #value
|
|
110
|
-
|
|
74
|
+
varintWrite(ch, len)
|
|
111
75
|
if len == 0 then
|
|
112
76
|
return
|
|
113
77
|
end
|
|
@@ -118,7 +82,6 @@ local function makeDirectArray(
|
|
|
118
82
|
alloc(ch, payloadBytes)
|
|
119
83
|
end
|
|
120
84
|
local b = ch.buff
|
|
121
|
-
|
|
122
85
|
local off = cursor
|
|
123
86
|
for i = 1, len do
|
|
124
87
|
elemDW(b, off, value[i])
|
|
@@ -128,9 +91,9 @@ local function makeDirectArray(
|
|
|
128
91
|
end,
|
|
129
92
|
|
|
130
93
|
read = function(src: buffer, pos: number, _refs: { Instance }?): ({ any }, number)
|
|
131
|
-
local len, lenBytes =
|
|
94
|
+
local len, lenBytes = varintRead(src, pos)
|
|
132
95
|
if maxCount and len > maxCount then
|
|
133
|
-
error(`
|
|
96
|
+
Log.error(`count {len} exceeds max {maxCount}`)
|
|
134
97
|
end
|
|
135
98
|
if len == 0 then
|
|
136
99
|
return {}, lenBytes
|
|
@@ -138,10 +101,8 @@ local function makeDirectArray(
|
|
|
138
101
|
|
|
139
102
|
local payloadBytes = len * elemSize
|
|
140
103
|
local dataStart = pos + lenBytes
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
len = remaining // elemSize
|
|
144
|
-
payloadBytes = len * elemSize
|
|
104
|
+
if payloadBytes > buffer.len(src) - dataStart then
|
|
105
|
+
Log.error(`payload {payloadBytes}B exceeds remaining buffer`)
|
|
145
106
|
end
|
|
146
107
|
|
|
147
108
|
local result = table.create(len)
|
|
@@ -152,30 +113,31 @@ local function makeDirectArray(
|
|
|
152
113
|
end
|
|
153
114
|
return result, lenBytes + payloadBytes
|
|
154
115
|
end,
|
|
155
|
-
}
|
|
116
|
+
}
|
|
156
117
|
end
|
|
157
118
|
|
|
119
|
+
-- Generic fallback: variable-size or composite element codec.
|
|
158
120
|
local function makeGenericArray(
|
|
159
121
|
element: Types.InternalCodec<any>,
|
|
160
122
|
maxCount: number?
|
|
161
123
|
): Types.InternalCodec<any>
|
|
162
|
-
return
|
|
124
|
+
return {
|
|
163
125
|
_isArray = true,
|
|
164
126
|
_element = element,
|
|
165
127
|
_maxCount = maxCount,
|
|
166
128
|
|
|
167
129
|
write = function(ch: Types.ChannelState, value: { any }): ()
|
|
168
130
|
local len = #value
|
|
169
|
-
|
|
131
|
+
varintWrite(ch, len)
|
|
170
132
|
for i = 1, len do
|
|
171
133
|
element.write(ch, value[i])
|
|
172
134
|
end
|
|
173
135
|
end,
|
|
174
136
|
|
|
175
137
|
read = function(src: buffer, pos: number, refs: { Instance }?): ({ any }, number)
|
|
176
|
-
local len, lenBytes =
|
|
138
|
+
local len, lenBytes = varintRead(src, pos)
|
|
177
139
|
if maxCount and len > maxCount then
|
|
178
|
-
error(`
|
|
140
|
+
Log.error(`count {len} exceeds max {maxCount}`)
|
|
179
141
|
end
|
|
180
142
|
if len == 0 then
|
|
181
143
|
return {}, lenBytes
|
|
@@ -186,14 +148,14 @@ local function makeGenericArray(
|
|
|
186
148
|
for i = 1, len do
|
|
187
149
|
local v, consumed = element.read(src, pos + total, refs)
|
|
188
150
|
if consumed == 0 then
|
|
189
|
-
error(`
|
|
151
|
+
Log.error(`element codec consumed 0 bytes at index {i}`)
|
|
190
152
|
end
|
|
191
153
|
result[i] = v
|
|
192
154
|
total += consumed
|
|
193
155
|
end
|
|
194
156
|
return result, total
|
|
195
157
|
end,
|
|
196
|
-
}
|
|
158
|
+
}
|
|
197
159
|
end
|
|
198
160
|
|
|
199
161
|
-- Public -----------------------------------------------------------------
|
|
@@ -201,75 +163,35 @@ end
|
|
|
201
163
|
local Array = {}
|
|
202
164
|
|
|
203
165
|
function Array.array(element: Types.InternalCodec<any>, maxCount: number?): Types.InternalCodec<any>
|
|
166
|
+
local codec: Types.InternalCodec<any>
|
|
204
167
|
if element._isBool then
|
|
205
|
-
|
|
168
|
+
codec = makeBoolArray(element, maxCount)
|
|
169
|
+
else
|
|
170
|
+
local elemSize = element._size
|
|
171
|
+
local elemDW = element._directWrite
|
|
172
|
+
local elemDR = element._directRead
|
|
173
|
+
if elemSize and elemDW and elemDR then
|
|
174
|
+
codec = makeDirectArray(element, elemSize, elemDW, elemDR, maxCount)
|
|
175
|
+
else
|
|
176
|
+
codec = makeGenericArray(element, maxCount)
|
|
177
|
+
end
|
|
206
178
|
end
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
local elemDW = element._directWrite
|
|
210
|
-
local elemDR = element._directRead
|
|
211
|
-
if elemSize and elemDW and elemDR then
|
|
212
|
-
return makeDirectArray(element, elemSize, elemDW, elemDR, maxCount)
|
|
179
|
+
if element._hasUnknown then
|
|
180
|
+
codec._hasUnknown = true
|
|
213
181
|
end
|
|
214
|
-
|
|
215
|
-
return makeGenericArray(element, maxCount)
|
|
182
|
+
return table.freeze(codec)
|
|
216
183
|
end
|
|
217
184
|
|
|
185
|
+
--[[
|
|
186
|
+
UNCHANGED-or-FULL delta. Useful only when contents AND order are stable
|
|
187
|
+
across frames; for unstable lists, plain array() avoids a 1-byte tax on
|
|
188
|
+
every frame the byte-equality cache misses.
|
|
189
|
+
]]
|
|
218
190
|
function Array.deltaArray(
|
|
219
191
|
element: Types.InternalCodec<any>,
|
|
220
192
|
maxCount: number?
|
|
221
193
|
): Types.InternalCodec<any>
|
|
222
|
-
|
|
223
|
-
local innerArray = Array.array(element, maxCount)
|
|
224
|
-
|
|
225
|
-
return table.freeze({
|
|
226
|
-
_isDelta = true,
|
|
227
|
-
|
|
228
|
-
write = function(ch: Types.ChannelState, value: { any }): ()
|
|
229
|
-
local cache = ch.deltas[deltaId]
|
|
230
|
-
|
|
231
|
-
if not cache then
|
|
232
|
-
writeByte(ch, DELTA_FULL)
|
|
233
|
-
|
|
234
|
-
local scratch = acquireScratch()
|
|
235
|
-
innerArray.write(scratch, value)
|
|
236
|
-
local snapLen = scratch.cursor
|
|
237
|
-
|
|
238
|
-
ch.deltas[deltaId] = { raw = snapshot(scratch.buff, snapLen), len = snapLen }
|
|
239
|
-
innerArray.write(ch, value)
|
|
240
|
-
releaseScratch()
|
|
241
|
-
return
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
local scratch = acquireScratch()
|
|
245
|
-
innerArray.write(scratch, value)
|
|
246
|
-
local scratchLen = scratch.cursor
|
|
247
|
-
|
|
248
|
-
if
|
|
249
|
-
scratchLen == cache.len
|
|
250
|
-
and rangeEqual(scratch.buff, 0, cache.raw, 0, scratchLen)
|
|
251
|
-
then
|
|
252
|
-
writeByte(ch, DELTA_UNCHANGED)
|
|
253
|
-
releaseScratch()
|
|
254
|
-
return
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
writeByte(ch, DELTA_FULL)
|
|
258
|
-
innerArray.write(ch, value)
|
|
259
|
-
|
|
260
|
-
cache.raw = snapshot(scratch.buff, scratchLen)
|
|
261
|
-
cache.len = scratchLen
|
|
262
|
-
releaseScratch()
|
|
263
|
-
end,
|
|
264
|
-
|
|
265
|
-
read = function(src: buffer, pos: number, refs: { Instance }?): ({ any }, number)
|
|
266
|
-
if readu8(src, pos) == DELTA_UNCHANGED then
|
|
267
|
-
return {}, 1
|
|
268
|
-
end
|
|
269
|
-
local value, consumed = innerArray.read(src, pos + 1, refs)
|
|
270
|
-
return value, 1 + consumed
|
|
271
|
-
end,
|
|
272
|
-
})
|
|
194
|
+
return Shared.makeUnchangedOrFullDelta(Array.array(element, maxCount), allocDeltaId(), Baseline)
|
|
273
195
|
end
|
|
274
196
|
|
|
275
197
|
return table.freeze(Array)
|