@axpecter/lync 2.2.0 → 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 +95 -72
- package/src/api/Query.luau +128 -79
- 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 +123 -102
- 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
|
@@ -1,26 +1,48 @@
|
|
|
1
1
|
--!strict
|
|
2
2
|
--!native
|
|
3
|
-
--
|
|
3
|
+
-- map() and deltaMap(). Keys sorted at encode for stable wire bytes.
|
|
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
|
|
|
12
|
+
-- State ------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
--[[
|
|
15
|
+
Depth-stacked key scratch. A nested Map.write (Map<K, Map<K2, V>>) would
|
|
16
|
+
clobber a single shared array mid-iteration; the stack gives each frame
|
|
17
|
+
its own slot. Mirrors Shared.acquireScratch / releaseScratch.
|
|
18
|
+
]]
|
|
19
|
+
local _keyStacks: { { any } } = {}
|
|
20
|
+
local _keyDepth = 0
|
|
21
|
+
|
|
11
22
|
-- Private ----------------------------------------------------------------
|
|
12
23
|
|
|
13
24
|
local alloc = Base.alloc
|
|
14
|
-
local writeByte = Channel.writeByte
|
|
15
|
-
local readu8 = buffer.readu8
|
|
16
|
-
|
|
17
|
-
local acquireScratch = Shared.acquireScratch
|
|
18
|
-
local releaseScratch = Shared.releaseScratch
|
|
19
|
-
local rangeEqual = Shared.rangeEqual
|
|
20
|
-
local snapshot = Shared.snapshot
|
|
21
25
|
local allocDeltaId = Shared.allocDeltaId
|
|
22
|
-
|
|
23
|
-
local
|
|
26
|
+
|
|
27
|
+
local function acquireKeys(): { any }
|
|
28
|
+
_keyDepth += 1
|
|
29
|
+
local keys = _keyStacks[_keyDepth]
|
|
30
|
+
if keys then
|
|
31
|
+
return keys
|
|
32
|
+
end
|
|
33
|
+
keys = {}
|
|
34
|
+
_keyStacks[_keyDepth] = keys
|
|
35
|
+
return keys
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
local function releaseKeys(): ()
|
|
39
|
+
_keyDepth -= 1
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
-- Module-level so the mixed-type sort path doesn't allocate a closure per write.
|
|
43
|
+
local function tostringLess(a: any, b: any): boolean
|
|
44
|
+
return tostring(a) < tostring(b)
|
|
45
|
+
end
|
|
24
46
|
|
|
25
47
|
-- Public -----------------------------------------------------------------
|
|
26
48
|
|
|
@@ -33,24 +55,59 @@ function Map.map(
|
|
|
33
55
|
): Types.InternalCodec<any>
|
|
34
56
|
local fixedKey = keyCodec._size
|
|
35
57
|
local fixedValue = valueCodec._size
|
|
58
|
+
local hasUnknown = keyCodec._hasUnknown or valueCodec._hasUnknown
|
|
36
59
|
|
|
37
|
-
|
|
60
|
+
--[[
|
|
61
|
+
Numbers and strings sort by `<` directly; everything else (Instance,
|
|
62
|
+
vector, mixed-type via Auto) routes through tostring. Pinned at codec
|
|
63
|
+
creation so writes don't re-sniff the key type.
|
|
64
|
+
]]
|
|
65
|
+
local typeCheck = keyCodec._typeCheck
|
|
66
|
+
local canSortDirect = typeCheck == "number" or typeCheck == "string"
|
|
67
|
+
|
|
68
|
+
local codec: Types.InternalCodec<any> = {
|
|
38
69
|
_isMap = true,
|
|
39
70
|
_keyCodec = keyCodec,
|
|
40
71
|
_valueCodec = valueCodec,
|
|
41
72
|
_maxCount = maxCount,
|
|
42
73
|
|
|
74
|
+
--[[
|
|
75
|
+
Sort keys for deterministic wire layout. Without this, two
|
|
76
|
+
structurally-equal maps yield different byte sequences across
|
|
77
|
+
frames, which breaks every downstream byte-equality cache (the
|
|
78
|
+
XOR baseline and deltaMap).
|
|
79
|
+
]]
|
|
43
80
|
write = function(ch: Types.ChannelState, value: { [any]: any }): ()
|
|
81
|
+
if typeof(value) ~= "table" then
|
|
82
|
+
Log.error(`expected table, got {typeof(value)}`)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
local keys = acquireKeys()
|
|
44
86
|
local count = 0
|
|
45
|
-
for
|
|
87
|
+
for k in value do
|
|
46
88
|
count += 1
|
|
89
|
+
keys[count] = k
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
-- Drop tail entries from the prior reuse so table.sort sees only `count` items.
|
|
93
|
+
local prevTail = #keys
|
|
94
|
+
for i = count + 1, prevTail do
|
|
95
|
+
keys[i] = nil
|
|
47
96
|
end
|
|
48
97
|
|
|
49
98
|
Varint.write(ch, count)
|
|
50
99
|
if count == 0 then
|
|
100
|
+
releaseKeys()
|
|
51
101
|
return
|
|
52
102
|
end
|
|
53
103
|
|
|
104
|
+
if canSortDirect then
|
|
105
|
+
table.sort(keys :: { any })
|
|
106
|
+
else
|
|
107
|
+
table.sort(keys, tostringLess)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
-- Single alloc when both halves are fixed-size.
|
|
54
111
|
if fixedKey and fixedValue then
|
|
55
112
|
local entrySize = fixedKey + fixedValue
|
|
56
113
|
local cursor = ch.cursor
|
|
@@ -59,33 +116,53 @@ function Map.map(
|
|
|
59
116
|
end
|
|
60
117
|
end
|
|
61
118
|
|
|
62
|
-
for
|
|
119
|
+
for i = 1, count do
|
|
120
|
+
local k = keys[i]
|
|
63
121
|
keyCodec.write(ch, k)
|
|
64
|
-
valueCodec.write(ch,
|
|
122
|
+
valueCodec.write(ch, value[k])
|
|
123
|
+
keys[i] = nil
|
|
65
124
|
end
|
|
125
|
+
releaseKeys()
|
|
66
126
|
end,
|
|
67
127
|
|
|
68
128
|
read = function(src: buffer, pos: number, refs: { Instance }?): ({ [any]: any }, number)
|
|
69
129
|
local count, lenBytes = Varint.read(src, pos)
|
|
70
130
|
if maxCount and count > maxCount then
|
|
71
|
-
error(`
|
|
131
|
+
Log.error(`count {count} exceeds max {maxCount}`)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
-- Fixed-size: reject corrupt counts before per-entry decode.
|
|
135
|
+
if fixedKey and fixedValue and count > 0 then
|
|
136
|
+
local entrySize = fixedKey + fixedValue
|
|
137
|
+
local dataStart = pos + lenBytes
|
|
138
|
+
local payloadBytes = count * entrySize
|
|
139
|
+
if payloadBytes > buffer.len(src) - dataStart then
|
|
140
|
+
Log.error(`payload {payloadBytes}B exceeds remaining buffer`)
|
|
141
|
+
end
|
|
72
142
|
end
|
|
73
143
|
|
|
74
144
|
local result: { [any]: any } = {}
|
|
75
145
|
local total = lenBytes
|
|
76
146
|
for i = 1, count do
|
|
77
147
|
local k, kc = keyCodec.read(src, pos + total, refs)
|
|
148
|
+
if kc == 0 then
|
|
149
|
+
Log.error(`truncated key at index {i}`)
|
|
150
|
+
end
|
|
78
151
|
total += kc
|
|
79
152
|
local v, vc = valueCodec.read(src, pos + total, refs)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
error(`[Lync] Map.read: zero-byte entry at index {i}`)
|
|
153
|
+
if vc == 0 then
|
|
154
|
+
Log.error(`truncated value at index {i}`)
|
|
83
155
|
end
|
|
156
|
+
total += vc
|
|
84
157
|
result[k] = v
|
|
85
158
|
end
|
|
86
159
|
return result, total
|
|
87
160
|
end,
|
|
88
|
-
}
|
|
161
|
+
}
|
|
162
|
+
if hasUnknown then
|
|
163
|
+
codec._hasUnknown = true
|
|
164
|
+
end
|
|
165
|
+
return table.freeze(codec)
|
|
89
166
|
end
|
|
90
167
|
|
|
91
168
|
function Map.deltaMap(
|
|
@@ -93,57 +170,11 @@ function Map.deltaMap(
|
|
|
93
170
|
valueCodec: Types.InternalCodec<any>,
|
|
94
171
|
maxCount: number?
|
|
95
172
|
): Types.InternalCodec<any>
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
write = function(ch: Types.ChannelState, value: { [any]: any }): ()
|
|
103
|
-
local cache = ch.deltas[deltaId]
|
|
104
|
-
|
|
105
|
-
if not cache then
|
|
106
|
-
writeByte(ch, DELTA_FULL)
|
|
107
|
-
|
|
108
|
-
local scratch = acquireScratch()
|
|
109
|
-
innerMap.write(scratch, value)
|
|
110
|
-
local snapLen = scratch.cursor
|
|
111
|
-
|
|
112
|
-
ch.deltas[deltaId] = { raw = snapshot(scratch.buff, snapLen), len = snapLen }
|
|
113
|
-
innerMap.write(ch, value)
|
|
114
|
-
releaseScratch()
|
|
115
|
-
return
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
local scratch = acquireScratch()
|
|
119
|
-
innerMap.write(scratch, value)
|
|
120
|
-
local scratchLen = scratch.cursor
|
|
121
|
-
|
|
122
|
-
if
|
|
123
|
-
scratchLen == cache.len
|
|
124
|
-
and rangeEqual(scratch.buff, 0, cache.raw, 0, scratchLen)
|
|
125
|
-
then
|
|
126
|
-
writeByte(ch, DELTA_UNCHANGED)
|
|
127
|
-
releaseScratch()
|
|
128
|
-
return
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
writeByte(ch, DELTA_FULL)
|
|
132
|
-
innerMap.write(ch, value)
|
|
133
|
-
|
|
134
|
-
cache.raw = snapshot(scratch.buff, scratchLen)
|
|
135
|
-
cache.len = scratchLen
|
|
136
|
-
releaseScratch()
|
|
137
|
-
end,
|
|
138
|
-
|
|
139
|
-
read = function(src: buffer, pos: number, refs: { Instance }?): ({ [any]: any }, number)
|
|
140
|
-
if readu8(src, pos) == DELTA_UNCHANGED then
|
|
141
|
-
return {}, 1
|
|
142
|
-
end
|
|
143
|
-
local value, consumed = innerMap.read(src, pos + 1, refs)
|
|
144
|
-
return value, 1 + consumed
|
|
145
|
-
end,
|
|
146
|
-
})
|
|
173
|
+
return Shared.makeUnchangedOrFullDelta(
|
|
174
|
+
Map.map(keyCodec, valueCodec, maxCount),
|
|
175
|
+
allocDeltaId(),
|
|
176
|
+
Baseline
|
|
177
|
+
)
|
|
147
178
|
end
|
|
148
179
|
|
|
149
180
|
return table.freeze(Map)
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
--!strict
|
|
2
|
-
--!
|
|
3
|
-
-- Optional codec. 1-byte present/absent flag, value only
|
|
2
|
+
--!native
|
|
3
|
+
-- Optional codec. 1-byte present/absent flag, value only when present.
|
|
4
4
|
|
|
5
5
|
local Channel = require(script.Parent.Parent.Parent.internal.Channel)
|
|
6
|
+
local Log = require(script.Parent.Parent.Parent.util.Log)
|
|
6
7
|
local Types = require(script.Parent.Parent.Parent.Types)
|
|
7
8
|
|
|
8
9
|
-- Private ----------------------------------------------------------------
|
|
9
10
|
|
|
10
11
|
local writeByte = Channel.writeByte
|
|
11
12
|
local readu8 = buffer.readu8
|
|
13
|
+
local bufferLen = buffer.len
|
|
12
14
|
|
|
13
15
|
-- Public -----------------------------------------------------------------
|
|
14
16
|
|
|
@@ -29,6 +31,7 @@ function Optional.optional(inner: Types.InternalCodec<any>): Types.InternalCodec
|
|
|
29
31
|
end,
|
|
30
32
|
|
|
31
33
|
read = function(src: buffer, pos: number, refs: { Instance }?): (any, number)
|
|
34
|
+
Log.assert(pos < bufferLen(src), "presence byte missing at end of buffer")
|
|
32
35
|
if readu8(src, pos) == 0 then
|
|
33
36
|
return nil, 1
|
|
34
37
|
end
|
|
@@ -37,6 +40,7 @@ function Optional.optional(inner: Types.InternalCodec<any>): Types.InternalCodec
|
|
|
37
40
|
end,
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
-- Propagate _hasUnknown so Gate skips schema validation upstream.
|
|
40
44
|
if inner._hasUnknown then
|
|
41
45
|
codec._hasUnknown = true
|
|
42
46
|
end
|
|
@@ -1,29 +1,27 @@
|
|
|
1
1
|
--!strict
|
|
2
2
|
--!native
|
|
3
|
-
-- Shared
|
|
3
|
+
-- Shared composite-codec helpers: delta flags, scratch pool, bool packing.
|
|
4
4
|
|
|
5
5
|
local Base = require(script.Parent.Parent.Base)
|
|
6
|
+
local Buf = require(script.Parent.Parent.Parent.util.Buffer)
|
|
7
|
+
local Channel = require(script.Parent.Parent.Parent.internal.Channel)
|
|
8
|
+
local Log = require(script.Parent.Parent.Parent.util.Log)
|
|
6
9
|
local Types = require(script.Parent.Parent.Parent.Types)
|
|
7
10
|
|
|
8
11
|
-- Constants --------------------------------------------------------------
|
|
9
12
|
|
|
10
13
|
--[[
|
|
11
|
-
Delta
|
|
12
|
-
0 UNCHANGED
|
|
13
|
-
1 FULL
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Struct's per-segment scheme uses [1..segCount] as "single dirty
|
|
17
|
-
segment index" then segCount + DELTA_FULL_OFFSET for full and
|
|
18
|
-
segCount + DELTA_MULTI_OFFSET for multi-dirty bitmap.
|
|
14
|
+
Delta flag space, shared across deltaStruct/deltaArray/deltaMap:
|
|
15
|
+
0 UNCHANGED (decoder reuses cache)
|
|
16
|
+
1 FULL (full payload follows)
|
|
17
|
+
Struct's per-segment scheme uses [1..segCount] as a single-dirty-segment
|
|
18
|
+
index, then segCount + DELTA_FULL_OFFSET / + DELTA_MULTI_OFFSET.
|
|
19
19
|
]]
|
|
20
20
|
local DELTA_UNCHANGED = 0
|
|
21
21
|
local DELTA_FULL = 1
|
|
22
22
|
local DELTA_FULL_OFFSET = 1
|
|
23
23
|
local DELTA_MULTI_OFFSET = 2
|
|
24
24
|
|
|
25
|
-
local INITIAL_SCRATCH = 1024
|
|
26
|
-
|
|
27
25
|
-- State ------------------------------------------------------------------
|
|
28
26
|
|
|
29
27
|
local _nextDeltaId = 0
|
|
@@ -35,49 +33,37 @@ local _scratchStack: { Types.ChannelState } = {}
|
|
|
35
33
|
local alloc = Base.alloc
|
|
36
34
|
local band = bit32.band
|
|
37
35
|
local bor = bit32.bor
|
|
38
|
-
local bxor = bit32.bxor
|
|
39
36
|
local lshift = bit32.lshift
|
|
40
37
|
local readu8 = buffer.readu8
|
|
41
|
-
local readu32 = buffer.readu32
|
|
42
38
|
local writeu8 = buffer.writeu8
|
|
43
|
-
|
|
44
|
-
local
|
|
45
|
-
|
|
46
|
-
buff = buffer.create(INITIAL_SCRATCH),
|
|
47
|
-
cursor = 0,
|
|
48
|
-
size = INITIAL_SCRATCH,
|
|
49
|
-
refs = {},
|
|
50
|
-
refCount = 0,
|
|
51
|
-
lastId = -1,
|
|
52
|
-
countPos = -1,
|
|
53
|
-
itemCount = 0,
|
|
54
|
-
singleMode = false,
|
|
55
|
-
singlePos = 0,
|
|
56
|
-
deltas = {},
|
|
57
|
-
prevDump = nil,
|
|
58
|
-
prevDumpLen = 0,
|
|
59
|
-
currentPacket = nil,
|
|
60
|
-
}
|
|
61
|
-
end
|
|
39
|
+
local bufCopy = buffer.copy
|
|
40
|
+
local rangeEqual = Buf.rangeEqual
|
|
41
|
+
local snapshot = Buf.snapshot
|
|
62
42
|
|
|
63
43
|
-- Public -----------------------------------------------------------------
|
|
64
44
|
|
|
65
45
|
local Shared = {}
|
|
66
46
|
|
|
67
47
|
Shared.DELTA_UNCHANGED = DELTA_UNCHANGED
|
|
68
|
-
Shared.DELTA_FULL = DELTA_FULL
|
|
69
48
|
Shared.DELTA_FULL_OFFSET = DELTA_FULL_OFFSET
|
|
70
49
|
Shared.DELTA_MULTI_OFFSET = DELTA_MULTI_OFFSET
|
|
50
|
+
Shared.rangeEqual = rangeEqual
|
|
51
|
+
Shared.snapshot = snapshot
|
|
71
52
|
|
|
72
53
|
function Shared.allocDeltaId(): number
|
|
73
54
|
_nextDeltaId += 1
|
|
74
55
|
return _nextDeltaId
|
|
75
56
|
end
|
|
76
57
|
|
|
58
|
+
-- 8 bools per byte.
|
|
59
|
+
function Shared.boolBytes(n: number): number
|
|
60
|
+
return (n + 7) // 8
|
|
61
|
+
end
|
|
62
|
+
|
|
77
63
|
--[[
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
64
|
+
Stack-based scratch ChannelState pool. Re-entrant codec serialization
|
|
65
|
+
(a codec writing into scratch that triggers another codec's scratch
|
|
66
|
+
acquire) works without aliasing.
|
|
81
67
|
]]
|
|
82
68
|
function Shared.acquireScratch(): Types.ChannelState
|
|
83
69
|
_scratchDepth += 1
|
|
@@ -87,7 +73,7 @@ function Shared.acquireScratch(): Types.ChannelState
|
|
|
87
73
|
ch.refCount = 0
|
|
88
74
|
return ch
|
|
89
75
|
end
|
|
90
|
-
ch =
|
|
76
|
+
ch = Channel.create()
|
|
91
77
|
_scratchStack[_scratchDepth] = ch
|
|
92
78
|
return ch
|
|
93
79
|
end
|
|
@@ -96,35 +82,9 @@ function Shared.releaseScratch(): ()
|
|
|
96
82
|
_scratchDepth -= 1
|
|
97
83
|
end
|
|
98
84
|
|
|
99
|
-
function Shared.rangeEqual(a: buffer, offA: number, b: buffer, offB: number, len: number): boolean
|
|
100
|
-
local aligned = band(len, -4)
|
|
101
|
-
local i = 0
|
|
102
|
-
while i < aligned do
|
|
103
|
-
if bxor(readu32(a, offA + i), readu32(b, offB + i)) ~= 0 then
|
|
104
|
-
return false
|
|
105
|
-
end
|
|
106
|
-
i += 4
|
|
107
|
-
end
|
|
108
|
-
while i < len do
|
|
109
|
-
if readu8(a, offA + i) ~= readu8(b, offB + i) then
|
|
110
|
-
return false
|
|
111
|
-
end
|
|
112
|
-
i += 1
|
|
113
|
-
end
|
|
114
|
-
return true
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
function Shared.snapshot(src: buffer, len: number): buffer
|
|
118
|
-
local out = buffer.create(len)
|
|
119
|
-
if len > 0 then
|
|
120
|
-
buffer.copy(out, 0, src, 0, len)
|
|
121
|
-
end
|
|
122
|
-
return out
|
|
123
|
-
end
|
|
124
|
-
|
|
125
85
|
--[[
|
|
126
|
-
Split a struct schema into (dataKeys, dataCodecs, boolKeys),
|
|
127
|
-
|
|
86
|
+
Split a struct schema into (dataKeys, dataCodecs, boolKeys), each sorted
|
|
87
|
+
alphabetically for deterministic wire layout.
|
|
128
88
|
]]
|
|
129
89
|
function Shared.extractFields(
|
|
130
90
|
schema: { [string]: Types.InternalCodec<any> }
|
|
@@ -140,9 +100,7 @@ function Shared.extractFields(
|
|
|
140
100
|
end
|
|
141
101
|
end
|
|
142
102
|
|
|
143
|
-
|
|
144
|
-
error("[Lync] Lync.struct: requires at least one field")
|
|
145
|
-
end
|
|
103
|
+
Log.assert(#dataKeys > 0 or #boolKeys > 0, "requires at least one field")
|
|
146
104
|
|
|
147
105
|
table.sort(dataKeys)
|
|
148
106
|
table.sort(boolKeys)
|
|
@@ -174,9 +132,9 @@ function Shared.packBools(
|
|
|
174
132
|
if cursor + byteCount > ch.size then
|
|
175
133
|
alloc(ch, byteCount)
|
|
176
134
|
end
|
|
177
|
-
local b = ch.buff
|
|
178
135
|
|
|
179
136
|
local idx = 1
|
|
137
|
+
local b = ch.buff
|
|
180
138
|
for byteIdx = 0, byteCount - 1 do
|
|
181
139
|
local packed = 0
|
|
182
140
|
local remaining = boolCount - idx + 1
|
|
@@ -193,6 +151,39 @@ function Shared.packBools(
|
|
|
193
151
|
ch.cursor = cursor + byteCount
|
|
194
152
|
end
|
|
195
153
|
|
|
154
|
+
--[[
|
|
155
|
+
Integer-indexed bool-array pack. Split from packBools to skip the
|
|
156
|
+
per-bit string-key getter on the bool-array hot path.
|
|
157
|
+
]]
|
|
158
|
+
function Shared.packBoolArray(ch: Types.ChannelState, value: { boolean }, count: number): ()
|
|
159
|
+
if count == 0 then
|
|
160
|
+
return
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
local byteCount = (count + 7) // 8
|
|
164
|
+
local cursor = ch.cursor
|
|
165
|
+
if cursor + byteCount > ch.size then
|
|
166
|
+
alloc(ch, byteCount)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
local idx = 1
|
|
170
|
+
local b = ch.buff
|
|
171
|
+
for byteIdx = 0, byteCount - 1 do
|
|
172
|
+
local packed = 0
|
|
173
|
+
local remaining = count - idx + 1
|
|
174
|
+
local limit = if remaining >= 8 then 8 else remaining
|
|
175
|
+
for bit = 0, limit - 1 do
|
|
176
|
+
if value[idx] then
|
|
177
|
+
packed = bor(packed, lshift(1, bit))
|
|
178
|
+
end
|
|
179
|
+
idx += 1
|
|
180
|
+
end
|
|
181
|
+
writeu8(b, cursor + byteIdx, packed)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
ch.cursor = cursor + byteCount
|
|
185
|
+
end
|
|
186
|
+
|
|
196
187
|
function Shared.unpackBools(
|
|
197
188
|
src: buffer,
|
|
198
189
|
pos: number,
|
|
@@ -218,4 +209,104 @@ function Shared.unpackBools(
|
|
|
218
209
|
return byteCount
|
|
219
210
|
end
|
|
220
211
|
|
|
212
|
+
-- Fills `result[1..count]` in place.
|
|
213
|
+
function Shared.unpackBoolArray(
|
|
214
|
+
src: buffer,
|
|
215
|
+
pos: number,
|
|
216
|
+
count: number,
|
|
217
|
+
result: { boolean }
|
|
218
|
+
): number
|
|
219
|
+
if count == 0 then
|
|
220
|
+
return 0
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
local byteCount = (count + 7) // 8
|
|
224
|
+
local idx = 1
|
|
225
|
+
for byteIdx = 0, byteCount - 1 do
|
|
226
|
+
local byte = readu8(src, pos + byteIdx)
|
|
227
|
+
local remaining = count - idx + 1
|
|
228
|
+
local limit = if remaining >= 8 then 8 else remaining
|
|
229
|
+
for bit = 0, limit - 1 do
|
|
230
|
+
result[idx] = band(byte, lshift(1, bit)) ~= 0
|
|
231
|
+
idx += 1
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
return byteCount
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
--[[
|
|
238
|
+
Build a byte-equality delta codec around any inner codec. Wire:
|
|
239
|
+
[DELTA_UNCHANGED] 1 byte
|
|
240
|
+
[DELTA_FULL] <inner-encoded payload> 1 + N bytes
|
|
241
|
+
Used by deltaArray and deltaMap. The FULL path encodes once into scratch
|
|
242
|
+
then bufCopies into the channel — avoids a double encode.
|
|
243
|
+
]]
|
|
244
|
+
function Shared.makeUnchangedOrFullDelta(
|
|
245
|
+
inner: Types.InternalCodec<any>,
|
|
246
|
+
deltaId: number,
|
|
247
|
+
Baseline: {
|
|
248
|
+
getCache: (number) -> any,
|
|
249
|
+
setCache: (number, any) -> (),
|
|
250
|
+
}
|
|
251
|
+
): Types.InternalCodec<any>
|
|
252
|
+
return table.freeze({
|
|
253
|
+
_isDelta = true,
|
|
254
|
+
|
|
255
|
+
write = function(ch: Types.ChannelState, value: any): ()
|
|
256
|
+
local scratch = Shared.acquireScratch()
|
|
257
|
+
inner.write(scratch, value)
|
|
258
|
+
local scratchLen = scratch.cursor
|
|
259
|
+
|
|
260
|
+
local cache = ch.deltas[deltaId]
|
|
261
|
+
if
|
|
262
|
+
cache
|
|
263
|
+
and scratchLen == cache.len
|
|
264
|
+
and rangeEqual(scratch.buff, 0, cache.raw, 0, scratchLen)
|
|
265
|
+
then
|
|
266
|
+
local cursor = ch.cursor
|
|
267
|
+
if cursor + 1 > ch.size then
|
|
268
|
+
alloc(ch, 1)
|
|
269
|
+
end
|
|
270
|
+
writeu8(ch.buff, cursor, DELTA_UNCHANGED)
|
|
271
|
+
ch.cursor = cursor + 1
|
|
272
|
+
Shared.releaseScratch()
|
|
273
|
+
return
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
local cursor = ch.cursor
|
|
277
|
+
if cursor + 1 + scratchLen > ch.size then
|
|
278
|
+
alloc(ch, 1 + scratchLen)
|
|
279
|
+
end
|
|
280
|
+
local b = ch.buff
|
|
281
|
+
writeu8(b, cursor, DELTA_FULL)
|
|
282
|
+
if scratchLen > 0 then
|
|
283
|
+
bufCopy(b, cursor + 1, scratch.buff, 0, scratchLen)
|
|
284
|
+
end
|
|
285
|
+
ch.cursor = cursor + 1 + scratchLen
|
|
286
|
+
|
|
287
|
+
local snap = snapshot(scratch.buff, scratchLen)
|
|
288
|
+
if cache then
|
|
289
|
+
cache.raw = snap
|
|
290
|
+
cache.len = scratchLen
|
|
291
|
+
else
|
|
292
|
+
ch.deltas[deltaId] = { raw = snap, len = scratchLen }
|
|
293
|
+
end
|
|
294
|
+
Shared.releaseScratch()
|
|
295
|
+
end,
|
|
296
|
+
|
|
297
|
+
read = function(src: buffer, pos: number, refs: { Instance }?): (any, number)
|
|
298
|
+
if readu8(src, pos) == DELTA_UNCHANGED then
|
|
299
|
+
local cached = Baseline.getCache(deltaId)
|
|
300
|
+
if not cached then
|
|
301
|
+
Log.error("UNCHANGED flag before any FULL frame; baseline missing")
|
|
302
|
+
end
|
|
303
|
+
return cached, 1
|
|
304
|
+
end
|
|
305
|
+
local value, consumed = inner.read(src, pos + 1, refs)
|
|
306
|
+
Baseline.setCache(deltaId, value)
|
|
307
|
+
return value, 1 + consumed
|
|
308
|
+
end,
|
|
309
|
+
})
|
|
310
|
+
end
|
|
311
|
+
|
|
221
312
|
return table.freeze(Shared)
|